feat(genre): add 30-day localStorage cache for genres
This commit is contained in:
1 parent
586dfb5754
commit
e599a7b803
1 file changed
+87
-2
@@ -42,6 +42,48 @@ const STORAGE_KEYS: Record<GenreFilterContext, string> = {
|
|||||||
favorites: 'qbz_genre_filter_favorites',
|
favorites: 'qbz_genre_filter_favorites',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const GENRE_CACHE_KEY = 'qbz_genre_cache';
|
||||||
|
const GENRE_CACHE_TTL_DAYS = 30;
|
||||||
|
|
||||||
|
interface GenreCache {
|
||||||
|
timestamp: number;
|
||||||
|
parentGenres: GenreInfo[];
|
||||||
|
childrenByParent: Record<number, GenreInfo[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveGenreCache() {
|
||||||
|
try {
|
||||||
|
const cache: GenreCache = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
parentGenres: state.parentGenres,
|
||||||
|
childrenByParent: Object.fromEntries(state.childrenByParent),
|
||||||
|
};
|
||||||
|
localStorage.setItem(GENRE_CACHE_KEY, JSON.stringify(cache));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save genre cache:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadGenreCache(): GenreCache | null {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(GENRE_CACHE_KEY);
|
||||||
|
if (!stored) return null;
|
||||||
|
|
||||||
|
const cache: GenreCache = JSON.parse(stored);
|
||||||
|
const ageInDays = (Date.now() - cache.timestamp) / (1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
|
if (ageInDays > GENRE_CACHE_TTL_DAYS) {
|
||||||
|
localStorage.removeItem(GENRE_CACHE_KEY);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load genre cache:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let currentContext: GenreFilterContext = 'home';
|
let currentContext: GenreFilterContext = 'home';
|
||||||
|
|
||||||
const state: GenreFilterState = {
|
const state: GenreFilterState = {
|
||||||
@@ -114,10 +156,24 @@ export function getContext(): GenreFilterContext {
|
|||||||
return currentContext;
|
return currentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Initial load: only fetch top-level genres (fast) */
|
/** Initial load: use cache if valid, otherwise fetch from API */
|
||||||
export async function loadGenres(): Promise<void> {
|
export async function loadGenres(): Promise<void> {
|
||||||
if (state.parentGenres.length > 0) return;
|
if (state.parentGenres.length > 0) return;
|
||||||
|
|
||||||
|
// Try loading from cache first
|
||||||
|
const cache = loadGenreCache();
|
||||||
|
if (cache) {
|
||||||
|
state.parentGenres = cache.parentGenres;
|
||||||
|
state.childrenByParent = new Map(Object.entries(cache.childrenByParent).map(([k, v]) => [Number(k), v]));
|
||||||
|
state.allGenres = [...cache.parentGenres, ...Object.values(cache.childrenByParent).flat()];
|
||||||
|
state.genreTree = buildTreeFromCache(cache);
|
||||||
|
loadFromStorage('home');
|
||||||
|
loadFromStorage('favorites');
|
||||||
|
notifyAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from API
|
||||||
state.isLoading = true;
|
state.isLoading = true;
|
||||||
notifyAll();
|
notifyAll();
|
||||||
|
|
||||||
@@ -125,7 +181,6 @@ export async function loadGenres(): Promise<void> {
|
|||||||
const parentGenres = await invoke<GenreInfo[]>('get_genres', {});
|
const parentGenres = await invoke<GenreInfo[]>('get_genres', {});
|
||||||
parentGenres.sort((a, b) => a.name.localeCompare(b.name));
|
parentGenres.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
// Create tree nodes without children (lazy loaded)
|
|
||||||
const genreTree: GenreTreeNode[] = parentGenres.map(genre => ({
|
const genreTree: GenreTreeNode[] = parentGenres.map(genre => ({
|
||||||
genre: { ...genre, childrenLoaded: false },
|
genre: { ...genre, childrenLoaded: false },
|
||||||
children: [],
|
children: [],
|
||||||
@@ -135,6 +190,7 @@ export async function loadGenres(): Promise<void> {
|
|||||||
state.allGenres = [...parentGenres];
|
state.allGenres = [...parentGenres];
|
||||||
state.genreTree = genreTree;
|
state.genreTree = genreTree;
|
||||||
|
|
||||||
|
saveGenreCache();
|
||||||
loadFromStorage('home');
|
loadFromStorage('home');
|
||||||
loadFromStorage('favorites');
|
loadFromStorage('favorites');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -148,6 +204,32 @@ export async function loadGenres(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildTreeFromCache(cache: GenreCache): GenreTreeNode[] {
|
||||||
|
return cache.parentGenres.map(parent => {
|
||||||
|
const children = cache.childrenByParent[parent.id] || [];
|
||||||
|
return {
|
||||||
|
genre: { ...parent, childrenLoaded: children.length > 0 },
|
||||||
|
children: children.map(child => {
|
||||||
|
const grandchildren = cache.childrenByParent[child.id] || [];
|
||||||
|
return {
|
||||||
|
genre: { ...child, childrenLoaded: grandchildren.length > 0 },
|
||||||
|
children: grandchildren.map(gc => ({ genre: gc, children: [] })),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Force refresh genres from API (clears cache) */
|
||||||
|
export async function forceRefreshGenres(): Promise<void> {
|
||||||
|
localStorage.removeItem(GENRE_CACHE_KEY);
|
||||||
|
state.parentGenres = [];
|
||||||
|
state.allGenres = [];
|
||||||
|
state.genreTree = [];
|
||||||
|
state.childrenByParent = new Map();
|
||||||
|
await loadGenres();
|
||||||
|
}
|
||||||
|
|
||||||
/** Lazy load children for a specific genre (silent = don't notify) */
|
/** Lazy load children for a specific genre (silent = don't notify) */
|
||||||
export async function loadChildren(genreId: number, silent = false): Promise<GenreInfo[]> {
|
export async function loadChildren(genreId: number, silent = false): Promise<GenreInfo[]> {
|
||||||
// Check if already loaded
|
// Check if already loaded
|
||||||
@@ -171,6 +253,9 @@ export async function loadChildren(genreId: number, silent = false): Promise<Gen
|
|||||||
// Update tree node
|
// Update tree node
|
||||||
updateTreeNode(genreId, taggedChildren);
|
updateTreeNode(genreId, taggedChildren);
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
saveGenreCache();
|
||||||
|
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
notifyAll();
|
notifyAll();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user