/** * Shared mutable state and pure utility functions. * All modules import from here - no circular dependencies. */ import type { LibraryInfo, VideoItem } from './types'; // ---- Shared mutable state ---- export let library: LibraryInfo | null = null; export let currentIndex = 0; export let prefs: Record | null = null; export let suppressTick = false; export let lastTick = 0; export let seeking = false; export function setLibrary(lib: LibraryInfo | null) { library = lib; } export function setCurrentIndex(idx: number) { currentIndex = idx; } export function setPrefs(p: Record | null) { prefs = p; } export function setSuppressTick(v: boolean) { suppressTick = v; } export function setLastTick(v: number) { lastTick = v; } export function setSeeking(v: boolean) { seeking = v; } // ---- Cross-module callbacks (set by main.ts) ---- export const cb = { loadIndex: null as ((idx: number, tc?: number, pause?: boolean, autoplay?: boolean) => Promise) | null, renderList: null as (() => void) | null, updateInfoPanel: null as (() => void) | null, updateOverall: null as (() => void) | null, notify: null as ((msg: string) => void) | null, refreshCurrentVideoMeta: null as (() => Promise) | null, onLibraryLoaded: null as ((info: LibraryInfo, startScan: boolean) => Promise) | null, buildSpeedMenu: null as ((rate: number) => void) | null, }; // ---- Pure utility functions ---- export function clamp(n: number, a: number, b: number): number { return Math.max(a, Math.min(b, n)); } export function fmtTime(sec: number): string { sec = Math.max(0, Math.floor(sec || 0)); const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); const s = sec % 60; if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; return `${m}:${String(s).padStart(2, '0')}`; } export function fmtBytes(n: number): string { n = Number(n || 0); if (!isFinite(n) || n <= 0) return '-'; const u = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0; while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; } return `${n.toFixed(i === 0 ? 0 : 1)} ${u[i]}`; } export function fmtDate(ts: number): string { if (!ts) return '-'; try { return new Date(ts * 1000).toLocaleString(); } catch { return '-'; } } export function fmtBitrate(bps: number): string | null { const n = Number(bps || 0); if (!isFinite(n) || n <= 0) return null; const kb = n / 1000.0; if (kb < 1000) return `${kb.toFixed(0)} kbps`; return `${(kb / 1000).toFixed(2)} Mbps`; } export function currentItem(): VideoItem | null { if (!library || !library.items) return null; return library.items[currentIndex] || null; } export function computeResumeTime(item: VideoItem | null): number { if (!item) return 0.0; if (item.finished) return 0.0; const pos = Number(item.pos || 0.0); const dur = Number(item.duration || 0.0); if (dur > 0) return clamp(pos, 0.0, Math.max(0.0, dur - 0.25)); return Math.max(0.0, pos); }