// Display formatters — units, durations, dates, states. export function bytes(n, dp = 1) { if (n == null || isNaN(n)) return '–'; if (n === 0) return '0 B'; const u = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; const i = Math.min(u.length - 1, Math.floor(Math.log(n) / Math.log(1024))); return `${(n / 1024 ** i).toFixed(i === 0 ? 0 : dp)} ${u[i]}`; } export function rate(n) { if (!n) return '–'; return `${bytes(n)}/s`; } export function eta(sec) { if (sec == null || sec >= 8640000 || sec < 0) return '∞'; if (sec < 1) return '0s'; const d = Math.floor(sec / 86400); const h = Math.floor((sec % 86400) / 3600); const m = Math.floor((sec % 3600) / 60); const s = Math.floor(sec % 60); if (d) return `${d}d ${h}h`; if (h) return `${h}h ${m}m`; if (m) return `${m}m ${s}s`; return `${s}s`; } export function duration(sec) { if (sec == null || sec < 0) return '–'; const d = Math.floor(sec / 86400); const h = Math.floor((sec % 86400) / 3600); const m = Math.floor((sec % 3600) / 60); if (d) return `${d}d ${h}h ${m}m`; if (h) return `${h}h ${m}m`; return `${m}m`; } export function pct(p) { return `${(p * 100).toFixed(1)}%`; } export function ratio(r) { if (r == null) return '–'; if (r >= 9999) return '∞'; return r.toFixed(2); } // Timestamps from the backend are Unix epoch SECONDS; 0/negative means "unset". export function date(sec) { if (!sec || sec < 0) return '–'; const d = new Date(sec * 1000); return d.toLocaleString(undefined, { year: '2-digit', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } export function ago(sec) { if (!sec || sec < 0) return '–'; const s = Date.now() / 1000 - sec; if (s < 60) return 'just now'; if (s < 3600) return `${Math.floor(s / 60)}m ago`; if (s < 86400) return `${Math.floor(s / 3600)}h ago`; return `${Math.floor(s / 86400)}d ago`; } const STATE_LABEL = { downloading: 'Downloading', forcedDL: '[F] Downloading', metaDL: 'Fetching metadata', stalledDL: 'Stalled (DL)', uploading: 'Seeding', forcedUP: '[F] Seeding', stalledUP: 'Seeding (idle)', pausedDL: 'Paused', pausedUP: 'Completed', checkingDL: 'Checking', checkingUP: 'Checking', moving: 'Moving', queuedDL: 'Queued', queuedUP: 'Queued', error: 'Error', missingFiles: 'Missing files', }; export function stateLabel(s) { return STATE_LABEL[s] || s; } const STATE_COLOR = { downloading: 'var(--dl)', forcedDL: 'var(--dl)', metaDL: 'var(--dl)', uploading: 'var(--up)', forcedUP: 'var(--up)', stalledDL: 'var(--txt-dim)', stalledUP: 'var(--txt-dim)', pausedDL: 'var(--pause)', pausedUP: 'var(--pause)', checkingDL: 'var(--check)', checkingUP: 'var(--check)', moving: 'var(--check)', queuedDL: 'var(--queue)', queuedUP: 'var(--queue)', error: 'var(--err)', missingFiles: 'var(--err)', }; export function stateColor(s) { return STATE_COLOR[s] || 'var(--txt-dim)'; } const PRIO_LABEL = { 0: 'Do not download', 1: 'Normal', 6: 'High', 7: 'Maximum' }; export function filePrio(p) { return PRIO_LABEL[p] ?? 'Normal'; } export function esc(s) { return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }