TutorialVault: complete Tauri v2 port with runtime fixes

Rename from TutorialDock to TutorialVault. Remove legacy Python app
and scripts. Fix video playback, subtitles, metadata display, window
state persistence, and auto-download of ffmpeg/ffprobe on first run.
Bundle fonts via npm instead of runtime download.
This commit is contained in:
Your Name
2026-02-19 12:44:57 +02:00
parent a459efae45
commit 9c8d7d94cd
25 changed files with 11665 additions and 7364 deletions

View File

@@ -31,7 +31,7 @@ export const api = {
invoke<OkResponse>('set_current', { index, timecode }),
tickProgress: (index: number, currentTime: number, duration: number | null, playing: boolean) =>
invoke<OkResponse>('tick_progress', { index, current_time: currentTime, duration, playing }),
invoke<OkResponse>('tick_progress', { index, currentTime, duration, playing }),
setFolderVolume: (volume: number) =>
invoke<OkResponse>('set_folder_volume', { volume }),
@@ -76,13 +76,13 @@ export const api = {
invoke<EmbeddedSubsResponse>('get_embedded_subtitles'),
extractEmbeddedSubtitle: (trackIndex: number) =>
invoke<SubtitleResponse>('extract_embedded_subtitle', { track_index: trackIndex }),
invoke<SubtitleResponse>('extract_embedded_subtitle', { trackIndex }),
getAvailableSubtitles: () =>
invoke<AvailableSubsResponse>('get_available_subtitles'),
loadSidecarSubtitle: (filePath: string) =>
invoke<SubtitleResponse>('load_sidecar_subtitle', { file_path: filePath }),
invoke<SubtitleResponse>('load_sidecar_subtitle', { filePath }),
chooseSubtitleFile: () =>
invoke<SubtitleResponse>('choose_subtitle_file'),

View File

@@ -3,9 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>TutorialDock</title>
<link rel="stylesheet" href="tutdock://localhost/fonts.css">
<link rel="stylesheet" href="tutdock://localhost/fa.css">
<title>TutorialVault</title>
</head>
<body>
<div id="zoomRoot">
@@ -14,7 +12,7 @@
<div class="brand">
<div class="appIcon" aria-hidden="true"><div class="appIconGlow"></div><i class="fa-solid fa-graduation-cap"></i></div>
<div class="brandText">
<div class="appName">TutorialDock</div>
<div class="appName">TutorialVault</div>
<div class="tagline">Watch local tutorials, resume instantly, and actually finish them.</div>
</div>
</div>
@@ -80,7 +78,7 @@
</div>
<div class="videoWrap">
<video id="player" preload="metadata"></video>
<video id="player" preload="metadata" crossorigin="anonymous"></video>
<div class="videoOverlay" id="videoOverlay">
<div class="overlayIcon" id="overlayIcon">
<i class="fa-solid fa-play" id="overlayIconI"></i>

View File

@@ -2,6 +2,19 @@
* 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';

View File

@@ -296,7 +296,7 @@ export async function loadVideoSrc(
const keepRate = clamp(Number(library.folder_rate ?? player.playbackRate ?? 1.0), 0.25, 3);
setSuppressTick(true);
player.src = `tutdock://localhost/video/${idx}`;
player.src = `http://tutdock.localhost/video/${idx}`;
player.load();
player.onloadedmetadata = async () => {

View File

@@ -46,7 +46,7 @@ export interface LibraryInfo {
finished_count?: number;
remaining_count?: number;
remaining_seconds_known?: number | null;
top_folders?: [string, number][];
top_folders?: { name: string; total: number; finished: number }[];
next_up?: NextUp | null;
}

View File

@@ -328,9 +328,9 @@ export function updateOverall(): void {
if (library.overall_progress === null || library.overall_progress === undefined) {
overallBar.style.width = '0%'; overallPct.textContent = '-'; return;
}
const p = clamp(library.overall_progress, 0, 1);
overallBar.style.width = `${(p * 100).toFixed(1)}%`;
overallPct.textContent = `${(p * 100).toFixed(1)}%`;
const p = clamp(library.overall_progress, 0, 100);
overallBar.style.width = `${p.toFixed(1)}%`;
overallPct.textContent = `${p.toFixed(1)}%`;
}
export function updateInfoPanel(): void {
@@ -350,7 +350,7 @@ export function updateInfoPanel(): void {
infoRemaining.textContent = `${library.remaining_count ?? 0}`;
infoEta.textContent = (library.remaining_seconds_known != null) ? fmtTime(library.remaining_seconds_known) : '-';
infoKnown.textContent = `${library.durations_known || 0}/${library.count || 0}`;
infoTop.textContent = (library.top_folders || []).map(([n, c]: [string, number]) => `${n}:${c}`).join(' \u2022 ') || '-';
infoTop.textContent = (library.top_folders || []).map((f: any) => `${f.name}: ${f.finished}/${f.total}`).join(' \u2022 ') || '-';
infoVolume.textContent = `${Math.round(clamp(Number(library.folder_volume ?? 1), 0, 1) * 100)}%`;
infoSpeed.textContent = `${Number(library.folder_rate ?? 1).toFixed(2)}x`;
} else {