import { Image, Trash2 } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { listen } from '@tauri-apps/api/event'; import SettingsSubSection from '../SettingsSubSection'; import { useCoverStrategyStore } from '../../store/coverStrategyStore'; import { useAuthStore } from '../../store/authStore'; import { coverCacheClearServer, coverCacheStatsServer, libraryCoverCatalogSize, libraryCoverProgress, } from '../../api/coverCache'; import { clearDiskSrcCacheForServer } from '../../cover/diskSrcCache'; import { serverListDisplayLabel } from '../../utils/server/serverDisplayName'; import { serverIndexKeyForProfile } from '../../utils/server/serverIndexKey'; import { showToast } from '../../utils/ui/toast'; import { formatBytes } from '../../utils/format/formatBytes'; import { wakeLibraryCoverBackfill } from '../../utils/library/coverBackfillWake'; import { COVER_CACHE_STRATEGIES, type CoverCacheStrategy, } from '../../utils/library/coverStrategy'; type ClearTarget = { serverId: string; indexKey: string; label: string; }; type ServerRowState = { bytes: number; entryCount: number; done: number; total: number; pending: number; }; export default function CoverCacheStrategySection() { const { t } = useTranslation(); const servers = useAuthStore(s => s.servers); const activeServerId = useAuthStore(s => s.activeServerId); const { strategyByServer, setServerStrategy, getStrategyForServer } = useCoverStrategyStore(); const [rowState, setRowState] = useState>({}); const [clearTarget, setClearTarget] = useState(null); const [clearingKey, setClearingKey] = useState(null); const activeIndexKeys = useMemo( () => new Set(servers.map(server => serverIndexKeyForProfile(server))), [servers], ); const removedServerKeys = useMemo(() => { const known = new Set(Object.keys(strategyByServer)); return Array.from(known).filter(key => !activeIndexKeys.has(key)); }, [strategyByServer, activeIndexKeys]); const refreshRow = useCallback(async (serverId: string, indexKey: string) => { const [stats, progress, catalog] = await Promise.all([ coverCacheStatsServer(indexKey).catch(() => ({ bytes: 0, entryCount: 0 })), libraryCoverProgress(indexKey, serverId).catch(() => ({ done: 0, totalDistinct: 0, pending: 0 })), libraryCoverCatalogSize(serverId).catch(() => 0), ]); const total = Math.max(progress.totalDistinct, catalog); setRowState(prev => ({ ...prev, [indexKey]: { bytes: stats.bytes, entryCount: stats.entryCount, done: progress.done, total, pending: Math.max(progress.pending, total > 0 ? total - progress.done : 0), }, })); }, []); const refreshAll = useCallback(() => { void Promise.all( servers.map(server => refreshRow(server.id, serverIndexKeyForProfile(server))), ); }, [servers, refreshRow]); useEffect(() => { refreshAll(); const id = window.setInterval(refreshAll, 15_000); return () => window.clearInterval(id); }, [refreshAll]); useEffect(() => { const unsubs: Array<() => void> = []; void (async () => { unsubs.push(await listen<{ serverIndexKey?: string; done?: number; total?: number; pending?: number; bytes?: number; entryCount?: number; }>('cover:library-progress', e => { const key = e.payload.serverIndexKey; if (!key) return; setRowState(prev => { const cur = prev[key]; if (!cur) return prev; const done = typeof e.payload.done === 'number' ? e.payload.done : cur.done; const total = typeof e.payload.total === 'number' ? e.payload.total : cur.total; return { ...prev, [key]: { ...cur, done, total, pending: e.payload.pending ?? Math.max(0, total - done), bytes: typeof e.payload.bytes === 'number' ? e.payload.bytes : cur.bytes, entryCount: typeof e.payload.entryCount === 'number' ? e.payload.entryCount : cur.entryCount, }, }; }); })); unsubs.push(await listen<{ serverIndexKey?: string }>('cover:cache-cleared', e => { const key = e.payload.serverIndexKey; if (key) { setRowState(prev => ({ ...prev, [key]: { bytes: 0, entryCount: 0, done: 0, total: prev[key]?.total ?? 0, pending: prev[key]?.total ?? 0 }, })); } else { refreshAll(); } })); unsubs.push(await listen('cover:tier-ready', () => { refreshAll(); })); })(); return () => { for (const u of unsubs) u(); }; }, [refreshAll]); const strategyLabel = (s: CoverCacheStrategy) => { switch (s) { case 'lazy': return t('settings.coverCacheStrategyLazy'); case 'aggressive': return t('settings.coverCacheStrategyAggressive'); } }; const progressLabel = (row: ServerRowState | undefined, strategy: CoverCacheStrategy) => { if (!row || strategy !== 'aggressive' || row.total <= 0) { return row ? t('settings.coverCacheStrategyDiskUsage', { size: formatBytes(row.bytes) }) : '—'; } const percent = Math.max(0, Math.min(100, Math.round((row.done / row.total) * 100))); return t('settings.coverCacheStrategyProgressValue', { percent, done: row.done.toLocaleString(), total: row.total.toLocaleString(), size: formatBytes(row.bytes), }); }; const handleStrategyChange = (serverId: string, strategy: CoverCacheStrategy) => { setServerStrategy(serverId, strategy); if (serverId === activeServerId) { wakeLibraryCoverBackfill(); } }; const handleClearCoverCache = async () => { if (!clearTarget) return; setClearingKey(clearTarget.indexKey); try { await coverCacheClearServer(clearTarget.indexKey); clearDiskSrcCacheForServer(clearTarget.indexKey); await refreshRow(clearTarget.serverId, clearTarget.indexKey); showToast(t('settings.coverCacheStrategyClearSuccess'), 4000, 'success'); } catch { showToast(t('settings.coverCacheStrategyClearError'), 5000, 'error'); } finally { setClearingKey(null); setClearTarget(null); } }; return ( }>
{servers.map(server => { const strategy = getStrategyForServer(server.id); const key = serverIndexKeyForProfile(server); const row = rowState[key]; const label = serverListDisplayLabel(server, servers); return ( ); })} {removedServerKeys.map(key => ( ))}
{t('settings.coverCacheStrategyServerLabel')} {t('settings.coverCacheStrategyLabel')} {t('settings.coverCacheStrategyProgressLabel')} {t('settings.coverCacheStrategyActionsLabel')}
{label}
{COVER_CACHE_STRATEGIES.map(s => ( ))}
{strategy === 'aggressive' ? t('settings.coverCacheStrategyAggressiveDesc') : t('settings.coverCacheStrategyLazyDesc')}
{progressLabel(row, strategy)}
{key} ({t('settings.coverCacheStrategyServerRemoved')})

{t('settings.coverCacheStrategyDesc')}

{clearTarget && (
{t('settings.coverCacheStrategyClearTitle')}
{t('settings.coverCacheStrategyClearDesc', { server: clearTarget.label })}
)}
); }