diff --git a/BACKLOG.md b/BACKLOG.md index 9bb8cea..5c58653 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -19,7 +19,7 @@ bottom as they come up. - [x] Loading skeletons for search results instead of a bare "Searching…". - [x] Keyboard-shortcut help overlay (press `?`), listing space / arrows / f / m. - [x] Download/preload progress indicator on cards (percent or a bar, not just a pulse). -- [ ] Remember and restore the last playback position per video. +- [x] Remember and restore the last playback position per video. - [ ] Toast queue (stack multiple toasts instead of replacing). ## Features diff --git a/frontend/app.js b/frontend/app.js index e3ee74d..1baa0e6 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -45,7 +45,7 @@ function toAssetUrl(path) { } // ---------- State ---------- -let data = { playlists: [], history: [], settings: { quality: 'auto', volume: 1, audioOnly: false, autoPreload: true } }; +let data = { playlists: [], history: [], settings: { quality: 'auto', volume: 1, audioOnly: false, autoPreload: true }, resumePositions: {} }; let view = { type: 'search' }; // 'search' | 'history' | 'playlist' | 'settings' let searchResults = []; let queue = []; // list of video objects for autoplay @@ -245,6 +245,19 @@ const Player = { els.npChannel.textContent = current.meta.channel || ''; updateNowPlayingActions(); markPlayingCard(); + // Restore saved playback position + const id = current.meta.id; + if (data.resumePositions[id] && data.resumePositions[id] > 1) { + const saved = data.resumePositions[id]; + const restore = () => { + Player.seek(saved); + toast('Resumed from ' + fmtTime(saved)); + Player.master.removeEventListener('canplay', restore); + Player.master.removeEventListener('loadedmetadata', restore); + }; + Player.master.addEventListener('canplay', restore, { once: true }); + Player.master.addEventListener('loadedmetadata', restore, { once: true }); + } }, // Ordered list of qualities to try if playback errors out. Progressive @@ -459,7 +472,28 @@ function wirePlayerEvents() { el.addEventListener('waiting', () => { if (masterIs(el)) { showSpinner(true); if (Player.secondary) Player.secondary.pause(); } }); el.addEventListener('playing', () => { if (masterIs(el)) { showSpinner(false); if (Player.secondary && !el.paused) { Player.secondary.currentTime = el.currentTime; Player.secondary.play().catch(() => {}); } } }); el.addEventListener('canplay', () => { if (masterIs(el)) showSpinner(false); }); - el.addEventListener('timeupdate', () => { if (masterIs(el)) updateProgress(); }); + el.addEventListener('timeupdate', () => { + if (masterIs(el)) { + updateProgress(); + // Persist playback position every 10s + if (current && current.meta) { + const t = Player.master.currentTime; + if (t > 5 && Math.floor(t) % 10 === 0) { + data.resumePositions[current.meta.id] = t; + persist(); + } + } + } + }); + el.addEventListener('pause', () => { + if (masterIs(el) && current && current.meta) { + const t = Player.master.currentTime; + if (t > 1) { + data.resumePositions[current.meta.id] = t; + persist(); + } + } + }); el.addEventListener('loadedmetadata', () => { if (masterIs(el)) updateProgress(); }); el.addEventListener('ended', () => { if (masterIs(el)) playNext(); }); el.addEventListener('error', () => { @@ -1137,6 +1171,7 @@ async function boot() { data = { playlists: loaded.playlists || [], history: loaded.history || [], + resumePositions: loaded.resumePositions || {}, settings: { quality: 'auto', volume: 1, audioOnly: false, autoPreload: true, ...(loaded.settings || {}) }, }; } diff --git a/releases/YT Player_1.0.0_x64-setup.exe b/releases/YT Player_1.0.0_x64-setup.exe index 3529ba8..8342eb3 100755 Binary files a/releases/YT Player_1.0.0_x64-setup.exe and b/releases/YT Player_1.0.0_x64-setup.exe differ diff --git a/releases/YT Player_1.0.0_x64_en-US.msi b/releases/YT Player_1.0.0_x64_en-US.msi index 5c64f05..bd549cb 100755 Binary files a/releases/YT Player_1.0.0_x64_en-US.msi and b/releases/YT Player_1.0.0_x64_en-US.msi differ diff --git a/releases/ytplayer.exe b/releases/ytplayer.exe index e8d40bb..0ce7116 100644 Binary files a/releases/ytplayer.exe and b/releases/ytplayer.exe differ