import { type Accessor, createEffect, createMemo, createSignal, For, onCleanup, type ResourceActions, type Setter, Show, } from "solid-js"; import "./StorageManager.css"; import "./Dialog.css"; import "toastify-js/src/toastify.css"; import type { FlatFileListing } from "../App"; import { clearAllDownloads, clearLocalData, storageStatsLive } from "../offline"; import { formatBytes, toast } from "../utils"; import { ClearSVG } from "./SVG"; interface StorageManagerProps { showStorage: Accessor; setShowStorage: Setter; fetchFiles: ResourceActions; } //formatBytes(0) returns "Queued...", which fits download progress but not a usage display function formatSize(bytes: number): string { return bytes === 0 ? "0 B" : formatBytes(bytes); } //not part of the TS dom lib types yet, only implemented in Chromium type StorageEstimateWithDetails = StorageEstimate & { usageDetails?: Record }; const USAGE_DETAIL_LABELS: Record = { indexeddb: "IndexedDB", caches: "Cache storage", serviceWorkerRegistrations: "Service workers", }; //two-step button for destructive actions: the first click arms it for a few seconds, //the second actually runs onConfirm function ConfirmButton(props: { label: string; title: string; onConfirm: () => void }) { const [armed, setArmed] = createSignal(false); let timer: ReturnType | undefined; onCleanup(() => clearTimeout(timer)); return ( ); } export default function StorageManager(props: StorageManagerProps) { let dialogElement!: HTMLDialogElement; const [estimate, setEstimate] = createSignal(); const [estimateSupported, setEstimateSupported] = createSignal(true); const [persisted, setPersisted] = createSignal(); const [persistedSupported, setPersistedSupported] = createSignal(true); const [requestingPersist, setRequestingPersist] = createSignal(false); const [resetError, setResetError] = createSignal(); const [resetting, setResetting] = createSignal(false); async function refreshStorageInfo() { if (navigator.storage?.estimate) { setEstimateSupported(true); setEstimate(await navigator.storage.estimate().catch(() => undefined)); } else { setEstimateSupported(false); } if (navigator.storage?.persisted) { setPersistedSupported(true); setPersisted(await navigator.storage.persisted().catch(() => undefined)); } else { setPersistedSupported(false); } } createEffect(() => { if (props.showStorage()) { dialogElement.showModal(); refreshStorageInfo(); setResetError(undefined); } }); function handleClose() { dialogElement.close(); props.setShowStorage(false); } const usagePercent = createMemo(() => { const current = estimate(); if (!current?.quota || !current.usage) return 0; return Math.min(100, (current.usage / current.quota) * 100); }); //usageDetails is Chromium-only; elsewhere this stays empty and the breakdown is skipped const usageDetails = createMemo(() => Object.entries(estimate()?.usageDetails ?? {})); const quotaLabel = createMemo(() => { const quota = estimate()?.quota; return quota === undefined ? "unknown" : formatSize(quota); }); async function requestPersistence() { setRequestingPersist(true); try { setPersisted(await navigator.storage.persist()); } catch (e) { toast("Requesting persistent storage failed", "error", e); } setRequestingPersist(false); } async function clearSynced() { try { await clearAllDownloads(props.fetchFiles); toast("Cleared all synced files", "success"); } catch (e) { toast("Clearing synced files failed", "error", e); } refreshStorageInfo(); } async function fullReset() { setResetting(true); setResetError(undefined); const failure = await clearLocalData().catch((e) => e?.toString() ?? "Unknown error"); if (failure) { setResetError(failure); setResetting(false); return; } location.hash = ""; location.reload(); } return ( e.target.nodeName === "DIALOG" && handleClose()} >
Storage

Storage estimate API not available in this browser
} >
Usage: {formatSize(estimate()?.usage ?? 0)} Quota: {quotaLabel()}
{usagePercent().toFixed(1)}%
0}> {([key, bytes]) => (
{USAGE_DETAIL_LABELS[key] ?? key} {formatSize(bytes)}
)}
Persistent storage: {persisted() === undefined ? "unknown" : persisted() ? "yes" : "no"}
Synced files: {storageStatsLive().fileCount} {formatSize(storageStatsLive().fileBytes)}
Covers: {storageStatsLive().coverCount} Playlists: {storageStatsLive().playlistCount}
{resetError()}
); }