/** * TutorialDock frontend — boot sequence, tick loop, global wiring. * Orchestrates all modules and holds cross-module callbacks. */ import '@fortawesome/fontawesome-free/css/all.min.css'; import '@fontsource/sora/500.css'; import '@fontsource/sora/600.css'; import '@fontsource/sora/700.css'; import '@fontsource/sora/800.css'; import '@fontsource/manrope/400.css'; import '@fontsource/manrope/500.css'; import '@fontsource/manrope/600.css'; import '@fontsource/manrope/700.css'; import '@fontsource/manrope/800.css'; import '@fontsource/ibm-plex-mono/400.css'; import '@fontsource/ibm-plex-mono/500.css'; import '@fontsource/ibm-plex-mono/600.css'; import './styles/main.css'; import './styles/player.css'; import './styles/playlist.css'; import './styles/panels.css'; import './styles/components.css'; import './styles/animations.css'; import { api } from './api'; import type { LibraryInfo } from './types'; import { library, currentIndex, prefs, suppressTick, lastTick, setLibrary, setCurrentIndex, setPrefs, setSuppressTick, setLastTick, clamp, cb, currentItem, } from './store'; import { initPlayer, loadVideoSrc, updatePlayPauseIcon, updateTimeReadout, updateSeekFill, updateVolFill, updateVideoOverlay, updateSpeedIcon, setVolume, setPlaybackRate, buildSpeedMenu, isPlaying, getVideoTime, getVideoDuration, getPlayer, isVolDragging, } from './player'; import { initPlaylist, renderList } from './playlist'; import { initSubtitles, refreshSubtitles, clearSubtitles } from './subtitles'; import { initUI, applyZoom, applySplit, applyDockSplit, updateInfoPanel, updateOverall, updateNowHeader, loadNoteForCurrent, refreshCurrentVideoMeta, notify, setOnTopChecked, setAutoplayChecked, } from './ui'; import { initTooltips } from './tooltips'; // ---- Wire cross-module callbacks ---- cb.loadIndex = loadIndex; cb.renderList = renderList; cb.updateInfoPanel = updateInfoPanel; cb.updateOverall = updateOverall; cb.notify = notify; cb.refreshCurrentVideoMeta = refreshCurrentVideoMeta; cb.onLibraryLoaded = onLibraryLoaded; cb.buildSpeedMenu = buildSpeedMenu; // ---- Boot ---- async function boot(): Promise { // Init all modules initPlayer(); initPlaylist(); initSubtitles(); initUI(); initTooltips(); // Load prefs const pres = await api.getPrefs(); const p: Record = (pres && pres.ok) ? (pres.prefs as any || {}) : {}; setPrefs(p); p.ui_zoom = applyZoom(p.ui_zoom || 1.0); p.split_ratio = applySplit(p.split_ratio || 0.62); p.dock_ratio = applyDockSplit(p.dock_ratio || 0.62); setOnTopChecked(!!p.always_on_top); await api.setPrefs({ ui_zoom: p.ui_zoom, split_ratio: p.split_ratio, dock_ratio: p.dock_ratio, always_on_top: !!p.always_on_top, }); // Load library const info = await api.getLibrary(); if (info && info.ok) { await onLibraryLoaded(info, true); notify('Ready.'); return; } updateOverall(); updateTimeReadout(); updatePlayPauseIcon(); updateInfoPanel(); buildSpeedMenu(1.0); notify('Open a folder to begin.'); } // ---- onLibraryLoaded ---- async function onLibraryLoaded(info: LibraryInfo, startScan: boolean): Promise { setLibrary(info); setCurrentIndex(info.current_index || 0); clearSubtitles(); const player = getPlayer(); const v = clamp(Number(info.folder_volume ?? 1.0), 0, 1); setSuppressTick(true); player.volume = v; setVolume(v); setSuppressTick(false); const r = clamp(Number(info.folder_rate ?? 1.0), 0.25, 3); setPlaybackRate(r); buildSpeedMenu(r); setAutoplayChecked(!!info.folder_autoplay); updateOverall(); renderList(); updateInfoPanel(); if (startScan) { try { await api.startDurationScan(); } catch (_) {} } await loadIndex(currentIndex, Number(info.current_time || 0.0), true, false); } // ---- loadIndex ---- async function loadIndex( idx: number, timecode: number = 0.0, pauseAfterLoad: boolean = true, autoplayOnLoad: boolean = false, ): Promise { if (!library || !library.items || library.items.length === 0) return; idx = Math.max(0, Math.min(idx, library.items.length - 1)); setCurrentIndex(idx); const it = library.items[currentIndex] || null; updateNowHeader(it); await api.setCurrent(currentIndex, Number(timecode || 0.0)); renderList(); updateInfoPanel(); await loadNoteForCurrent(); await refreshCurrentVideoMeta(); await loadVideoSrc(idx, timecode, pauseAfterLoad, autoplayOnLoad, async () => { await refreshSubtitles(); }); } // ---- Tick loop ---- async function tick(): Promise { const now = Date.now(); if (now - lastTick < 950) return; setLastTick(now); if (library && !suppressTick) { const t = getVideoTime(); const d = getVideoDuration(); const playing = isPlaying(); try { await api.tickProgress(currentIndex, t, d, playing); } catch (_) {} } if (now % 3000 < 1000) { try { const info = await api.getLibrary(); if (info && info.ok) { const oldIndex = currentIndex; const oldCount = library?.items?.length || 0; setLibrary(info); setCurrentIndex(info.current_index || currentIndex); setAutoplayChecked(!!info.folder_autoplay); const player = getPlayer(); const volSlider = document.getElementById('volSlider') as HTMLInputElement & { dragging?: boolean }; const v = clamp(Number(info.folder_volume ?? player.volume ?? 1.0), 0, 1); if (!isVolDragging() && Math.abs(v - Number(volSlider.value)) > 0.001) { volSlider.value = String(v); updateVolFill(); } const r = clamp(Number(info.folder_rate ?? player.playbackRate ?? 1.0), 0.25, 3); player.playbackRate = r; updateSpeedIcon(r); buildSpeedMenu(r); updateOverall(); updateInfoPanel(); updateNowHeader(currentItem()); if (oldIndex !== currentIndex || oldCount !== (library?.items?.length || 0)) { renderList(); } } } catch (_) {} } try { await api.saveWindowState(); } catch (_) {} } // ---- Keyboard shortcuts ---- function initKeyboard(): void { window.addEventListener('keydown', (e) => { // Don't capture when typing in textarea/input const tag = (e.target as HTMLElement).tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; switch (e.key) { case ' ': e.preventDefault(); const player = getPlayer(); if (player.paused || player.ended) player.play(); else player.pause(); updatePlayPauseIcon(); break; case 'ArrowLeft': e.preventDefault(); try { getPlayer().currentTime = Math.max(0, getPlayer().currentTime - 5); } catch (_) {} break; case 'ArrowRight': e.preventDefault(); try { getPlayer().currentTime = Math.min(getPlayer().duration || 0, getPlayer().currentTime + 5); } catch (_) {} break; case 'ArrowUp': e.preventDefault(); try { const p = getPlayer(); p.volume = clamp(p.volume + 0.05, 0, 1); setVolume(p.volume); } catch (_) {} break; case 'ArrowDown': e.preventDefault(); try { const p = getPlayer(); p.volume = clamp(p.volume - 0.05, 0, 1); setVolume(p.volume); } catch (_) {} break; } }); } // ---- Start ---- initKeyboard(); setInterval(tick, 250); boot();