import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { invoke } from '@tauri-apps/api/core'; import { open as openDialog } from '@tauri-apps/plugin-dialog'; import { Download, FolderOpen, Trash2, X } from 'lucide-react'; import { useAuthStore } from '../../store/authStore'; import { useHotCacheStore } from '../../store/hotCacheStore'; import { useOfflineStore } from '../../store/offlineStore'; import { clearImageCache, getImageCacheSize } from '../../utils/imageCache'; import { formatBytes, snapHotCacheMb } from '../../utils/format/formatBytes'; import SettingsSubSection from '../SettingsSubSection'; import CoverCacheStrategySection from './CoverCacheStrategySection'; export function StorageTab() { const { t } = useTranslation(); const auth = useAuthStore(); const serverId = auth.activeServerId ?? ''; const clearAllOffline = useOfflineStore(s => s.clearAll); const clearHotCacheDisk = useHotCacheStore(s => s.clearAllDisk); const hotCacheEntries = useHotCacheStore(s => s.entries); const [imageCacheBytes, setImageCacheBytes] = useState(null); const [offlineCacheBytes, setOfflineCacheBytes] = useState(null); const [hotCacheBytes, setHotCacheBytes] = useState(null); const [showClearConfirm, setShowClearConfirm] = useState(false); const [clearing, setClearing] = useState(false); const hotCacheTrackCount = useMemo(() => { const prefix = `${serverId}:`; return Object.keys(hotCacheEntries).filter(k => k.startsWith(prefix)).length; }, [hotCacheEntries, serverId]); // Load all three size readouts on mount. useEffect(() => { getImageCacheSize().then(setImageCacheBytes); invoke('get_offline_cache_size', { customDir: auth.offlineDownloadDir || null }).then(setOfflineCacheBytes).catch(() => setOfflineCacheBytes(0)); invoke('get_hot_cache_size', { customDir: auth.hotCacheDownloadDir || null }).then(setHotCacheBytes).catch(() => setHotCacheBytes(0)); }, [auth.offlineDownloadDir, auth.hotCacheDownloadDir]); /** Live disk usage for hot cache (interval + refresh when index changes). */ useEffect(() => { const customDir = auth.hotCacheDownloadDir || null; const refresh = () => { invoke('get_hot_cache_size', { customDir }) .then(setHotCacheBytes) .catch(() => setHotCacheBytes(0)); }; refresh(); if (!auth.hotCacheEnabled) return; const interval = window.setInterval(refresh, 15_000); return () => window.clearInterval(interval); }, [auth.hotCacheEnabled, auth.hotCacheDownloadDir]); useEffect(() => { if (!auth.hotCacheEnabled) return; const handle = window.setTimeout(() => { invoke('get_hot_cache_size', { customDir: auth.hotCacheDownloadDir || null }) .then(setHotCacheBytes) .catch(() => setHotCacheBytes(0)); }, 400); return () => window.clearTimeout(handle); }, [hotCacheEntries, auth.hotCacheEnabled, auth.hotCacheDownloadDir]); const handleClearCache = useCallback(async () => { setClearing(true); await clearImageCache(); await clearAllOffline(serverId); const [imgBytes, offBytes] = await Promise.all([ getImageCacheSize(), invoke('get_offline_cache_size', { customDir: auth.offlineDownloadDir || null }).catch(() => 0), ]); setImageCacheBytes(imgBytes); setOfflineCacheBytes(offBytes); setShowClearConfirm(false); setClearing(false); }, [clearAllOffline, serverId, auth.offlineDownloadDir]); const pickOfflineDir = async () => { const selected = await openDialog({ directory: true, multiple: false, title: t('settings.offlineDirChange') }); if (selected && typeof selected === 'string') { auth.setOfflineDownloadDir(selected); } }; const pickHotCacheDir = async () => { const selected = await openDialog({ directory: true, multiple: false, title: t('settings.hotCacheDirChange') }); if (selected && typeof selected === 'string') { auth.setHotCacheDownloadDir(selected); useHotCacheStore.setState({ entries: {} }); invoke('get_hot_cache_size', { customDir: selected }).then(setHotCacheBytes).catch(() => setHotCacheBytes(0)); } }; const pickDownloadFolder = async () => { const selected = await openDialog({ directory: true, multiple: false, title: t('settings.pickFolderTitle') }); if (selected && typeof selected === 'string') { auth.setDownloadFolder(selected); } }; return ( <> {/* Offline Library (In-App) — includes cache settings */} } >
{t('settings.offlineDirDesc')}
{auth.offlineDownloadDir && ( )}
{auth.offlineDownloadDir && (
{t('settings.offlineDirHint')}
)}
{(imageCacheBytes !== null || offlineCacheBytes !== null) && (
{t('settings.cacheUsedImages')} {imageCacheBytes !== null ? formatBytes(imageCacheBytes) : '…'}
{t('settings.cacheUsedOffline')} {offlineCacheBytes !== null ? formatBytes(offlineCacheBytes) : '…'}
)}
{t('settings.cacheMaxLabel')} { const v = Number(e.target.value); if (v >= 100) auth.setMaxCacheMb(v); }} style={{ width: 80, padding: '4px 8px', fontSize: 13 }} id="cache-size-input" /> MB
{showClearConfirm ? (
{t('settings.cacheClearWarning')}
) : ( )}
{/* Buffering */} } >
{t('settings.preloadHotCacheMutualExclusive')}
{/* Preload mode */}
{t('settings.preloadMode')}
{t('settings.preloadModeDesc')}
{auth.preloadMode !== 'off' && ( <>
{(['balanced', 'early', 'custom'] as const).map(mode => ( ))}
{auth.preloadMode === 'custom' && (
auth.setPreloadCustomSeconds(parseInt(e.target.value))} style={{ flex: 1, minWidth: 80, maxWidth: 200 }} /> {t('settings.preloadCustomSeconds', { n: auth.preloadCustomSeconds })}
)} )}
{/* Hot Cache */}
{t('settings.hotCacheTitle')}
{t('settings.hotCacheDisclaimer')}
{auth.hotCacheEnabled && (
{auth.hotCacheDownloadDir && ( )}
{auth.hotCacheDownloadDir && (
{t('settings.hotCacheDirHint')}
)}
{t('settings.cacheUsedHot')} {hotCacheBytes !== null ? formatBytes(hotCacheBytes) : '…'}
{t('settings.hotCacheTrackCount')} {hotCacheTrackCount}
{t('settings.hotCacheMaxMb')}
auth.setHotCacheMaxMb(parseInt(e.target.value, 10))} style={{ flex: 1, minWidth: 80, maxWidth: 200 }} id="hot-cache-max-mb-slider" /> {snapHotCacheMb(auth.hotCacheMaxMb)} MB
{t('settings.hotCacheDebounce')}
auth.setHotCacheDebounceSec(parseInt(e.target.value, 10))} style={{ flex: 1, minWidth: 80, maxWidth: 200 }} id="hot-cache-debounce-slider" /> {Math.min(600, Math.max(0, auth.hotCacheDebounceSec)) === 0 ? t('settings.hotCacheDebounceImmediate') : t('settings.hotCacheDebounceSeconds', { n: Math.min(600, Math.max(0, auth.hotCacheDebounceSec)) })}
)}
{/* ZIP Export & Archiving */} } >
{t('settings.downloadsFolderDesc')}
{auth.downloadFolder && ( )}
); }