feat(genre): separate persistence for Home and Favorites contexts
- Refactor genreFilterStore to support multiple contexts - Each context (home, favorites) has independent selection state - Separate localStorage keys: qbz_genre_filter_home, qbz_genre_filter_favorites - GenreFilterButton/Popup accept context prop - FavoritesView uses 'favorites' context
This commit is contained in:
1 parent
65b42295e9
commit
2c886fae7a
4 files changed
+164
-98
No files matched your search
@@ -9,14 +9,16 @@
|
|||||||
getSelectedGenreNames,
|
getSelectedGenreNames,
|
||||||
loadGenres,
|
loadGenres,
|
||||||
subscribe as subscribeGenre,
|
subscribe as subscribeGenre,
|
||||||
type GenreInfo
|
type GenreInfo,
|
||||||
|
type GenreFilterContext
|
||||||
} from '$lib/stores/genreFilterStore';
|
} from '$lib/stores/genreFilterStore';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onFilterChange?: () => void;
|
onFilterChange?: () => void;
|
||||||
|
context?: GenreFilterContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { onFilterChange }: Props = $props();
|
let { onFilterChange, context = 'home' }: Props = $props();
|
||||||
|
|
||||||
let isOpen = $state(false);
|
let isOpen = $state(false);
|
||||||
let buttonEl: HTMLButtonElement | null = null;
|
let buttonEl: HTMLButtonElement | null = null;
|
||||||
@@ -29,25 +31,25 @@
|
|||||||
loadGenres();
|
loadGenres();
|
||||||
|
|
||||||
// Initialize state
|
// Initialize state
|
||||||
hasFilter = hasActiveFilter();
|
hasFilter = hasActiveFilter(context);
|
||||||
updateSelectedName();
|
updateSelectedName();
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
// Subscribe to filter changes
|
// Subscribe to filter changes for this context
|
||||||
const unsubscribe = subscribeGenre(() => {
|
const unsubscribe = subscribeGenre(() => {
|
||||||
hasFilter = hasActiveFilter();
|
hasFilter = hasActiveFilter(context);
|
||||||
updateSelectedName();
|
updateSelectedName();
|
||||||
// Only notify parent after initialization to prevent infinite loops
|
// Only notify parent after initialization to prevent infinite loops
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
onFilterChange?.();
|
onFilterChange?.();
|
||||||
}
|
}
|
||||||
});
|
}, context);
|
||||||
|
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateSelectedName() {
|
function updateSelectedName() {
|
||||||
const selectedNames = getSelectedGenreNames();
|
const selectedNames = getSelectedGenreNames(context);
|
||||||
if (selectedNames.length === 1) {
|
if (selectedNames.length === 1) {
|
||||||
selectedGenreName = selectedNames[0];
|
selectedGenreName = selectedNames[0];
|
||||||
} else if (selectedNames.length > 1) {
|
} else if (selectedNames.length > 1) {
|
||||||
@@ -84,6 +86,7 @@
|
|||||||
|
|
||||||
<GenreFilterPopup
|
<GenreFilterPopup
|
||||||
{isOpen}
|
{isOpen}
|
||||||
|
{context}
|
||||||
onClose={closePopup}
|
onClose={closePopup}
|
||||||
anchorEl={buttonEl}
|
anchorEl={buttonEl}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,16 +11,18 @@
|
|||||||
setRememberSelection,
|
setRememberSelection,
|
||||||
getGenreFilterState,
|
getGenreFilterState,
|
||||||
subscribe as subscribeGenre,
|
subscribe as subscribeGenre,
|
||||||
type GenreInfo
|
type GenreInfo,
|
||||||
|
type GenreFilterContext
|
||||||
} from '$lib/stores/genreFilterStore';
|
} from '$lib/stores/genreFilterStore';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
anchorEl?: HTMLElement | null;
|
anchorEl?: HTMLElement | null;
|
||||||
|
context?: GenreFilterContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { isOpen, onClose, anchorEl = null }: Props = $props();
|
let { isOpen, onClose, anchorEl = null, context = 'home' }: Props = $props();
|
||||||
|
|
||||||
let genres = $state<GenreInfo[]>([]);
|
let genres = $state<GenreInfo[]>([]);
|
||||||
let selectedIds = $state<Set<number>>(new Set());
|
let selectedIds = $state<Set<number>>(new Set());
|
||||||
@@ -28,17 +30,17 @@
|
|||||||
let popupEl: HTMLDivElement | null = null;
|
let popupEl: HTMLDivElement | null = null;
|
||||||
let popupStyle = $state('');
|
let popupStyle = $state('');
|
||||||
|
|
||||||
// Subscribe to store changes
|
// Subscribe to store changes for this context
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const unsubscribe = subscribeGenre(() => {
|
const unsubscribe = subscribeGenre(() => {
|
||||||
const state = getGenreFilterState();
|
const state = getGenreFilterState(context);
|
||||||
genres = state.availableGenres;
|
genres = state.availableGenres;
|
||||||
selectedIds = state.selectedGenreIds;
|
selectedIds = state.selectedGenreIds;
|
||||||
rememberSelection = state.rememberSelection;
|
rememberSelection = state.rememberSelection;
|
||||||
});
|
}, context);
|
||||||
|
|
||||||
// Initial load
|
// Initial load
|
||||||
const state = getGenreFilterState();
|
const state = getGenreFilterState(context);
|
||||||
genres = state.availableGenres;
|
genres = state.availableGenres;
|
||||||
selectedIds = state.selectedGenreIds;
|
selectedIds = state.selectedGenreIds;
|
||||||
rememberSelection = state.rememberSelection;
|
rememberSelection = state.rememberSelection;
|
||||||
@@ -76,16 +78,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleGenreClick(genreId: number) {
|
function handleGenreClick(genreId: number) {
|
||||||
toggleGenre(genreId);
|
toggleGenre(genreId, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClearAll() {
|
function handleClearAll() {
|
||||||
clearSelection();
|
clearSelection(context);
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRememberToggle() {
|
function handleRememberToggle() {
|
||||||
setRememberSelection(!rememberSelection);
|
setRememberSelection(!rememberSelection, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClickOutside(event: MouseEvent) {
|
function handleClickOutside(event: MouseEvent) {
|
||||||
@@ -163,7 +165,7 @@
|
|||||||
class="clear-btn"
|
class="clear-btn"
|
||||||
onclick={handleClearAll}
|
onclick={handleClearAll}
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!hasActiveFilter()}
|
disabled={!hasActiveFilter(context)}
|
||||||
>
|
>
|
||||||
Clear filter
|
Clear filter
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -19,10 +19,13 @@
|
|||||||
import GenreFilterButton from '../GenreFilterButton.svelte';
|
import GenreFilterButton from '../GenreFilterButton.svelte';
|
||||||
import {
|
import {
|
||||||
hasActiveFilter as hasGenreFilter,
|
hasActiveFilter as hasGenreFilter,
|
||||||
getSelectedGenreNames
|
getSelectedGenreNames,
|
||||||
|
type GenreFilterContext
|
||||||
} from '$lib/stores/genreFilterStore';
|
} from '$lib/stores/genreFilterStore';
|
||||||
import type { FavoritesPreferences, QobuzAlbum } from '$lib/types';
|
import type { FavoritesPreferences, QobuzAlbum } from '$lib/types';
|
||||||
|
|
||||||
|
const GENRE_CONTEXT: GenreFilterContext = 'favorites';
|
||||||
|
|
||||||
interface FavoriteAlbum {
|
interface FavoriteAlbum {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -415,8 +418,8 @@
|
|||||||
|
|
||||||
let albums = favoriteAlbums;
|
let albums = favoriteAlbums;
|
||||||
|
|
||||||
// Filter by genre
|
// Filter by genre (using favorites context)
|
||||||
const selectedGenreNames = getSelectedGenreNames();
|
const selectedGenreNames = getSelectedGenreNames(GENRE_CONTEXT);
|
||||||
if (selectedGenreNames.length > 0) {
|
if (selectedGenreNames.length > 0) {
|
||||||
albums = albums.filter(a =>
|
albums = albums.filter(a =>
|
||||||
a.genre && selectedGenreNames.some(genreName =>
|
a.genre && selectedGenreNames.some(genreName =>
|
||||||
@@ -1252,7 +1255,7 @@
|
|||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
{#if activeTab === 'albums'}
|
{#if activeTab === 'albums'}
|
||||||
<div class="toolbar-controls">
|
<div class="toolbar-controls">
|
||||||
<GenreFilterButton onFilterChange={handleGenreFilterChange} />
|
<GenreFilterButton context={GENRE_CONTEXT} onFilterChange={handleGenreFilterChange} />
|
||||||
<div class="dropdown-container">
|
<div class="dropdown-container">
|
||||||
<button class="control-btn" onclick={() => (showAlbumGroupMenu = !showAlbumGroupMenu)}>
|
<button class="control-btn" onclick={() => (showAlbumGroupMenu = !showAlbumGroupMenu)}>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Genre filter store for Home page filtering
|
* Genre filter store with context support
|
||||||
* Persists selected genres to localStorage
|
* Each context (home, favorites) has independent persistence
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
@@ -12,162 +12,220 @@ export interface GenreInfo {
|
|||||||
slug?: string;
|
slug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GenreFilterContext = 'home' | 'favorites';
|
||||||
|
|
||||||
|
interface ContextState {
|
||||||
|
selectedGenreIds: Set<number>;
|
||||||
|
rememberSelection: boolean;
|
||||||
|
listeners: Set<() => void>;
|
||||||
|
}
|
||||||
|
|
||||||
interface GenreFilterState {
|
interface GenreFilterState {
|
||||||
availableGenres: GenreInfo[];
|
availableGenres: GenreInfo[];
|
||||||
selectedGenreIds: Set<number>;
|
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
rememberSelection: boolean;
|
contexts: Record<GenreFilterContext, ContextState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = 'qbz_genre_filter';
|
const STORAGE_KEYS: Record<GenreFilterContext, string> = {
|
||||||
|
home: 'qbz_genre_filter_home',
|
||||||
let state: GenreFilterState = {
|
favorites: 'qbz_genre_filter_favorites',
|
||||||
availableGenres: [],
|
|
||||||
selectedGenreIds: new Set(),
|
|
||||||
isLoading: false,
|
|
||||||
rememberSelection: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const listeners: Set<() => void> = new Set();
|
// Default context for backwards compatibility
|
||||||
|
let currentContext: GenreFilterContext = 'home';
|
||||||
|
|
||||||
function notify() {
|
const state: GenreFilterState = {
|
||||||
listeners.forEach((fn) => fn());
|
availableGenres: [],
|
||||||
|
isLoading: false,
|
||||||
|
contexts: {
|
||||||
|
home: {
|
||||||
|
selectedGenreIds: new Set(),
|
||||||
|
rememberSelection: true,
|
||||||
|
listeners: new Set(),
|
||||||
|
},
|
||||||
|
favorites: {
|
||||||
|
selectedGenreIds: new Set(),
|
||||||
|
rememberSelection: true,
|
||||||
|
listeners: new Set(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getContextState(context?: GenreFilterContext): ContextState {
|
||||||
|
return state.contexts[context ?? currentContext];
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveToStorage() {
|
function notify(context?: GenreFilterContext) {
|
||||||
if (!state.rememberSelection) return;
|
const ctx = getContextState(context);
|
||||||
|
ctx.listeners.forEach((fn) => fn());
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveToStorage(context?: GenreFilterContext) {
|
||||||
|
const ctx = context ?? currentContext;
|
||||||
|
const ctxState = getContextState(ctx);
|
||||||
|
if (!ctxState.rememberSelection) return;
|
||||||
try {
|
try {
|
||||||
const data = {
|
const data = {
|
||||||
selectedGenreIds: Array.from(state.selectedGenreIds),
|
selectedGenreIds: Array.from(ctxState.selectedGenreIds),
|
||||||
rememberSelection: state.rememberSelection,
|
rememberSelection: ctxState.rememberSelection,
|
||||||
};
|
};
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
|
localStorage.setItem(STORAGE_KEYS[ctx], JSON.stringify(data));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to save genre filter state:', e);
|
console.error(`Failed to save genre filter state for ${ctx}:`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadFromStorage() {
|
function loadFromStorage(context: GenreFilterContext) {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem(STORAGE_KEY);
|
const stored = localStorage.getItem(STORAGE_KEYS[context]);
|
||||||
if (stored) {
|
if (stored) {
|
||||||
const data = JSON.parse(stored);
|
const data = JSON.parse(stored);
|
||||||
state.selectedGenreIds = new Set(data.selectedGenreIds || []);
|
state.contexts[context].selectedGenreIds = new Set(data.selectedGenreIds || []);
|
||||||
state.rememberSelection = data.rememberSelection ?? true;
|
state.contexts[context].rememberSelection = data.rememberSelection ?? true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load genre filter state:', e);
|
console.error(`Failed to load genre filter state for ${context}:`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setContext(context: GenreFilterContext): void {
|
||||||
|
currentContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getContext(): GenreFilterContext {
|
||||||
|
return currentContext;
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadGenres(): Promise<void> {
|
export async function loadGenres(): Promise<void> {
|
||||||
if (state.availableGenres.length > 0) return; // Already loaded
|
if (state.availableGenres.length > 0) return; // Already loaded
|
||||||
|
|
||||||
state.isLoading = true;
|
state.isLoading = true;
|
||||||
notify();
|
// Notify all contexts
|
||||||
|
notify('home');
|
||||||
|
notify('favorites');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const genres = await invoke<GenreInfo[]>('get_genres', {});
|
const genres = await invoke<GenreInfo[]>('get_genres', {});
|
||||||
state.availableGenres = genres;
|
state.availableGenres = genres;
|
||||||
|
|
||||||
// Load saved selection after genres are available
|
// Load saved selections for all contexts
|
||||||
loadFromStorage();
|
loadFromStorage('home');
|
||||||
|
loadFromStorage('favorites');
|
||||||
|
|
||||||
// Validate saved selection against available genres
|
// Validate saved selections against available genres
|
||||||
const validIds = new Set(genres.map((g) => g.id));
|
const validIds = new Set(genres.map((g) => g.id));
|
||||||
const validSelection = new Set<number>();
|
for (const ctx of ['home', 'favorites'] as GenreFilterContext[]) {
|
||||||
state.selectedGenreIds.forEach((id) => {
|
const ctxState = state.contexts[ctx];
|
||||||
if (validIds.has(id)) {
|
const validSelection = new Set<number>();
|
||||||
validSelection.add(id);
|
ctxState.selectedGenreIds.forEach((id) => {
|
||||||
}
|
if (validIds.has(id)) {
|
||||||
});
|
validSelection.add(id);
|
||||||
state.selectedGenreIds = validSelection;
|
}
|
||||||
|
});
|
||||||
|
ctxState.selectedGenreIds = validSelection;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load genres:', e);
|
console.error('Failed to load genres:', e);
|
||||||
state.availableGenres = [];
|
state.availableGenres = [];
|
||||||
} finally {
|
} finally {
|
||||||
state.isLoading = false;
|
state.isLoading = false;
|
||||||
notify();
|
notify('home');
|
||||||
|
notify('favorites');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGenreFilterState(): GenreFilterState {
|
export function getGenreFilterState(context?: GenreFilterContext) {
|
||||||
return { ...state, selectedGenreIds: new Set(state.selectedGenreIds) };
|
const ctx = getContextState(context);
|
||||||
|
return {
|
||||||
|
availableGenres: state.availableGenres,
|
||||||
|
selectedGenreIds: new Set(ctx.selectedGenreIds),
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
rememberSelection: ctx.rememberSelection,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAvailableGenres(): GenreInfo[] {
|
export function getAvailableGenres(): GenreInfo[] {
|
||||||
return state.availableGenres;
|
return state.availableGenres;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedGenreIds(): Set<number> {
|
export function getSelectedGenreIds(context?: GenreFilterContext): Set<number> {
|
||||||
return new Set(state.selectedGenreIds);
|
return new Set(getContextState(context).selectedGenreIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedGenreId(): number | undefined {
|
export function getSelectedGenreId(context?: GenreFilterContext): number | undefined {
|
||||||
// For API calls that only support single genre_id
|
const ids = Array.from(getContextState(context).selectedGenreIds);
|
||||||
const ids = Array.from(state.selectedGenreIds);
|
|
||||||
return ids.length === 1 ? ids[0] : undefined;
|
return ids.length === 1 ? ids[0] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedGenreName(): string | undefined {
|
export function getSelectedGenreName(context?: GenreFilterContext): string | undefined {
|
||||||
const id = getSelectedGenreId();
|
const id = getSelectedGenreId(context);
|
||||||
if (!id) return undefined;
|
if (!id) return undefined;
|
||||||
const genre = state.availableGenres.find(g => g.id === id);
|
const genre = state.availableGenres.find(g => g.id === id);
|
||||||
return genre?.name;
|
return genre?.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedGenreNames(): string[] {
|
export function getSelectedGenreNames(context?: GenreFilterContext): string[] {
|
||||||
const ids = Array.from(state.selectedGenreIds);
|
const ids = Array.from(getContextState(context).selectedGenreIds);
|
||||||
return ids
|
return ids
|
||||||
.map(id => state.availableGenres.find(g => g.id === id)?.name)
|
.map(id => state.availableGenres.find(g => g.id === id)?.name)
|
||||||
.filter((name): name is string => !!name);
|
.filter((name): name is string => !!name);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isGenreSelected(genreId: number): boolean {
|
export function isGenreSelected(genreId: number, context?: GenreFilterContext): boolean {
|
||||||
return state.selectedGenreIds.has(genreId);
|
return getContextState(context).selectedGenreIds.has(genreId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasActiveFilter(): boolean {
|
export function hasActiveFilter(context?: GenreFilterContext): boolean {
|
||||||
return state.selectedGenreIds.size > 0;
|
return getContextState(context).selectedGenreIds.size > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toggleGenre(genreId: number): void {
|
export function toggleGenre(genreId: number, context?: GenreFilterContext): void {
|
||||||
if (state.selectedGenreIds.has(genreId)) {
|
const ctx = context ?? currentContext;
|
||||||
state.selectedGenreIds.delete(genreId);
|
const ctxState = getContextState(ctx);
|
||||||
|
if (ctxState.selectedGenreIds.has(genreId)) {
|
||||||
|
ctxState.selectedGenreIds.delete(genreId);
|
||||||
} else {
|
} else {
|
||||||
state.selectedGenreIds.add(genreId);
|
ctxState.selectedGenreIds.add(genreId);
|
||||||
}
|
}
|
||||||
saveToStorage();
|
saveToStorage(ctx);
|
||||||
notify();
|
notify(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectGenre(genreId: number): void {
|
export function selectGenre(genreId: number, context?: GenreFilterContext): void {
|
||||||
state.selectedGenreIds.clear();
|
const ctx = context ?? currentContext;
|
||||||
state.selectedGenreIds.add(genreId);
|
const ctxState = getContextState(ctx);
|
||||||
saveToStorage();
|
ctxState.selectedGenreIds.clear();
|
||||||
notify();
|
ctxState.selectedGenreIds.add(genreId);
|
||||||
|
saveToStorage(ctx);
|
||||||
|
notify(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearSelection(): void {
|
export function clearSelection(context?: GenreFilterContext): void {
|
||||||
state.selectedGenreIds.clear();
|
const ctx = context ?? currentContext;
|
||||||
saveToStorage();
|
const ctxState = getContextState(ctx);
|
||||||
notify();
|
ctxState.selectedGenreIds.clear();
|
||||||
|
saveToStorage(ctx);
|
||||||
|
notify(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setRememberSelection(remember: boolean): void {
|
export function setRememberSelection(remember: boolean, context?: GenreFilterContext): void {
|
||||||
state.rememberSelection = remember;
|
const ctx = context ?? currentContext;
|
||||||
|
const ctxState = getContextState(ctx);
|
||||||
|
ctxState.rememberSelection = remember;
|
||||||
if (remember) {
|
if (remember) {
|
||||||
saveToStorage();
|
saveToStorage(ctx);
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEYS[ctx]);
|
||||||
}
|
}
|
||||||
notify();
|
notify(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function subscribe(callback: () => void): () => void {
|
export function subscribe(callback: () => void, context?: GenreFilterContext): () => void {
|
||||||
listeners.add(callback);
|
const ctxState = getContextState(context);
|
||||||
return () => listeners.delete(callback);
|
ctxState.listeners.add(callback);
|
||||||
|
return () => ctxState.listeners.delete(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize by loading from storage
|
// Initialize by loading from storage for all contexts
|
||||||
loadFromStorage();
|
loadFromStorage('home');
|
||||||
|
loadFromStorage('favorites');
|
||||||
Reference in new issue
Block a user