feat: implement all frontend TypeScript modules
Create player.ts (video controls, seek, volume, speed, overlay), playlist.ts (list rendering, tree SVG, drag-and-drop reorder, scrollbar), subtitles.ts (subtitle menu, track management, sidecar/embedded), ui.ts (zoom, splits, info panel, notes, toasts, recent menu), tooltips.ts (zoom-aware tooltip system with delays), store.ts (shared state and utility functions), and main.ts (boot sequence, tick loop, keyboard shortcuts). All modules compile with strict TypeScript. Vite build produces 34KB JS + 41KB CSS. 115 Rust tests pass.
This commit is contained in:
239
src/main.ts
239
src/main.ts
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* TutorialDock frontend — boot sequence, tick loop, global wiring.
|
||||
* Orchestrates all modules and holds cross-module callbacks.
|
||||
*/
|
||||
import './styles/main.css';
|
||||
import './styles/player.css';
|
||||
import './styles/playlist.css';
|
||||
@@ -5,4 +9,237 @@ import './styles/panels.css';
|
||||
import './styles/components.css';
|
||||
import './styles/animations.css';
|
||||
|
||||
console.log('TutorialDock frontend loaded');
|
||||
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<void> {
|
||||
// Init all modules
|
||||
initPlayer();
|
||||
initPlaylist();
|
||||
initSubtitles();
|
||||
initUI();
|
||||
initTooltips();
|
||||
|
||||
// Load prefs
|
||||
const pres = await api.getPrefs();
|
||||
const p: Record<string, any> = (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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user