add per-song remember-position flag and playlist play-in-full mode
This commit is contained in:
@@ -206,13 +206,16 @@ const DEFAULT_SETTINGS = {
|
||||
autoBackupEnabled: false,
|
||||
autoBackupIntervalDays: 7,
|
||||
};
|
||||
let data = { playlists: [], history: [], queue: [], settings: { ...DEFAULT_SETTINGS }, resumePositions: {}, playCount: {}, abMarkers: {}, lastAutoBackup: 0, profile: null };
|
||||
let data = { playlists: [], history: [], queue: [], settings: { ...DEFAULT_SETTINGS }, resumePositions: {}, rememberPos: {}, playCount: {}, abMarkers: {}, lastAutoBackup: 0, profile: null };
|
||||
let view = { type: 'search' }; // 'search'|'history'|'playlist'|'settings'|'queue'|'saved'|'downloads'|'channel'
|
||||
let searchResults = [];
|
||||
let channelData = { name: '', url: '', key: '', results: [], loading: false };
|
||||
let queue = []; // list of video objects for autoplay
|
||||
let queueIndex = -1;
|
||||
let queueSource = ''; // label of what's playing ('queue','playlist:<id>',…)
|
||||
let playFullMode = false; // "play in full" session: ignore resume positions,
|
||||
// A-B markers, shuffle, loop and repeat — play the
|
||||
// list start-to-finish, then stop.
|
||||
let current = null; // { meta, qualities, audioUrl, localUrl? }
|
||||
let dragSource = -1; // index of card being dragged
|
||||
let listFilter = '';
|
||||
@@ -301,6 +304,7 @@ function profilePayload() {
|
||||
history: data.history,
|
||||
settings: data.settings,
|
||||
resumePositions: data.resumePositions,
|
||||
rememberPos: data.rememberPos,
|
||||
playCount: data.playCount,
|
||||
abMarkers: data.abMarkers,
|
||||
};
|
||||
@@ -337,6 +341,7 @@ function applyProfileData(name, payload, updatedAt) {
|
||||
if (Array.isArray(payload.playlists)) data.playlists = payload.playlists;
|
||||
if (Array.isArray(payload.history)) data.history = payload.history;
|
||||
if (payload.resumePositions && typeof payload.resumePositions === 'object') data.resumePositions = payload.resumePositions;
|
||||
if (payload.rememberPos && typeof payload.rememberPos === 'object') data.rememberPos = payload.rememberPos;
|
||||
if (payload.playCount && typeof payload.playCount === 'object') data.playCount = payload.playCount;
|
||||
if (payload.abMarkers && typeof payload.abMarkers === 'object') data.abMarkers = payload.abMarkers;
|
||||
if (payload.settings && typeof payload.settings === 'object') data.settings = { ...DEFAULT_SETTINGS, ...payload.settings };
|
||||
@@ -741,14 +746,19 @@ const Player = {
|
||||
if (this._revealOnLoad) scrollPlayerIntoViewPortrait();
|
||||
// Restore A-B markers and load related (non-blocking)
|
||||
restoreAbMarkers();
|
||||
updateRememberPosUI();
|
||||
loadRelated();
|
||||
exitSelectMode();
|
||||
// Restore saved playback position.
|
||||
// Restore saved playback position — only when this song's "remember
|
||||
// position" flag is on (per-playlist copy first, global fallback), and
|
||||
// never during a "play in full" session.
|
||||
// On auto-advance (or prev/next) we do NOT resume the previous timestamp —
|
||||
// a freshly selected track should start at the beginning, or at the A point
|
||||
// when an A-B loop is set for it.
|
||||
const id = current.meta.id;
|
||||
if (this._resumeOnLoad && data.resumePositions[id] && data.resumePositions[id] > 1) {
|
||||
if (playFullMode) {
|
||||
// Play-in-full ignores resume positions and A markers alike.
|
||||
} else if (this._resumeOnLoad && rememberPosEnabled() && data.resumePositions[id] && data.resumePositions[id] > 1) {
|
||||
const saved = data.resumePositions[id];
|
||||
const restore = () => {
|
||||
Player.seek(saved);
|
||||
@@ -1139,14 +1149,16 @@ function wirePlayerEvents() {
|
||||
const bucket = Math.floor(t / 10);
|
||||
if (t > 5 && bucket !== Player._lastPersistBucket) {
|
||||
Player._lastPersistBucket = bucket;
|
||||
data.resumePositions[current.meta.id] = t;
|
||||
persist();
|
||||
if (!playFullMode && rememberPosEnabled()) {
|
||||
data.resumePositions[current.meta.id] = t;
|
||||
persist();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
el.addEventListener('pause', () => {
|
||||
if (masterIs(el) && current && current.meta) {
|
||||
if (masterIs(el) && current && current.meta && !playFullMode && rememberPosEnabled()) {
|
||||
const t = Player.master.currentTime;
|
||||
if (t > 1) {
|
||||
data.resumePositions[current.meta.id] = t;
|
||||
@@ -1206,7 +1218,7 @@ function updateProgress() {
|
||||
// A-B loop: passing B behaves like the track ending. Loop the A-B segment only
|
||||
// when "loop current" is on; otherwise B is the effective end of the track and
|
||||
// we advance per the repeat settings (next track, or wrap when "repeat list").
|
||||
if (abA !== null && abB !== null && abB > abA && cur >= abB) {
|
||||
if (!playFullMode && abA !== null && abB !== null && abB > abA && cur >= abB) {
|
||||
if (data.settings.loopOne) {
|
||||
Player.seek(abA);
|
||||
} else if (!advanceQueue()) {
|
||||
@@ -1264,12 +1276,14 @@ function addToHistory(meta) {
|
||||
// ============================================================================
|
||||
// Queue / navigation
|
||||
// ============================================================================
|
||||
function playFromList(list, index, source = '') {
|
||||
function playFromList(list, index, source = '', { playFull = false } = {}) {
|
||||
queue = list;
|
||||
queueIndex = index;
|
||||
queueSource = source;
|
||||
playFullMode = playFull;
|
||||
unshuffledQueue = null;
|
||||
if (data.settings.shuffle) applyShuffle();
|
||||
// Play-in-full keeps the list order — a service set plays as arranged.
|
||||
if (data.settings.shuffle && !playFull) applyShuffle();
|
||||
Player.loadVideo(queue[queueIndex]);
|
||||
renderUpNext();
|
||||
}
|
||||
@@ -1386,7 +1400,7 @@ function renderUpNext() {
|
||||
function advanceQueue() {
|
||||
if (queueIndex >= 0 && queueIndex < queue.length - 1) {
|
||||
queueIndex++;
|
||||
} else if (data.settings.repeatMode === 'all' && queue.length) {
|
||||
} else if (!playFullMode && data.settings.repeatMode === 'all' && queue.length) {
|
||||
queueIndex = 0;
|
||||
} else {
|
||||
return false;
|
||||
@@ -1403,8 +1417,12 @@ function playNext() {
|
||||
}
|
||||
// Fired when a track finishes on its own — honors single-video loop first.
|
||||
function onTrackEnded() {
|
||||
if (data.settings.loopOne) { Player.seek(0); Player.play(); return; }
|
||||
if (!advanceQueue()) { updatePlayBtn(); $('upnext').classList.add('hidden'); }
|
||||
if (!playFullMode && data.settings.loopOne) { Player.seek(0); Player.play(); return; }
|
||||
if (!advanceQueue()) {
|
||||
if (playFullMode) { playFullMode = false; toast('Finished playing in full'); }
|
||||
updatePlayBtn();
|
||||
$('upnext').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
function toggleLoopOne() {
|
||||
data.settings.loopOne = !data.settings.loopOne;
|
||||
@@ -1592,6 +1610,14 @@ function renderList() {
|
||||
const playAll = document.createElement('button');
|
||||
playAll.textContent = '▶ Play all';
|
||||
playAll.onclick = () => { if (pl.videos.length) playFromList(pl.videos, 0, 'playlist:' + pl.id); };
|
||||
const playFull = document.createElement('button');
|
||||
playFull.textContent = '▶ Play in full';
|
||||
playFull.title = 'Play every video start to finish — ignores saved positions and A-B loops, stops after the last one';
|
||||
playFull.onclick = () => {
|
||||
if (!pl.videos.length) return;
|
||||
playFromList(pl.videos, 0, 'playlist:' + pl.id, { playFull: true });
|
||||
toast('Playing in full — stops after the last video');
|
||||
};
|
||||
const queueAll = document.createElement('button');
|
||||
queueAll.textContent = '+ Queue';
|
||||
queueAll.onclick = () => { pl.videos.forEach((v) => addToQueue(v, { quiet: true })); toast('Added playlist to queue'); };
|
||||
@@ -1601,7 +1627,7 @@ function renderList() {
|
||||
const del = document.createElement('button');
|
||||
del.textContent = 'Delete';
|
||||
del.onclick = () => deletePlaylist(pl);
|
||||
els.listActions.append(playAll, queueAll, rename, del);
|
||||
els.listActions.append(playAll, playFull, queueAll, rename, del);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2820,6 +2846,7 @@ document.querySelectorAll('.chip').forEach((c) => {
|
||||
$('abABtn').addEventListener('click', setAbA);
|
||||
$('abBBtn').addEventListener('click', setAbB);
|
||||
$('abClearBtn').addEventListener('click', clearAb);
|
||||
$('rememberPosBtn').addEventListener('click', toggleRememberPos);
|
||||
|
||||
// Related panel collapse toggle
|
||||
$('relatedToggleBtn').addEventListener('click', () => {
|
||||
@@ -3172,6 +3199,35 @@ function currentPlaylistEntry() {
|
||||
if (!pl || !Array.isArray(pl.videos)) return null;
|
||||
return pl.videos.find((v) => v.id === current.meta.id) || null;
|
||||
}
|
||||
// ---------- Remember-position flag ----------
|
||||
// Like A-B markers, the flag is per-song-per-playlist: stored on the playlist's
|
||||
// own copy of the video (`entry.rememberPos`) when playing from that playlist,
|
||||
// with `data.rememberPos[videoId]` as the fallback for non-playlist playback.
|
||||
// Resume positions are only saved and restored for songs with the flag on.
|
||||
function rememberPosEnabled() {
|
||||
if (!current || !current.meta) return false;
|
||||
const entry = currentPlaylistEntry();
|
||||
return entry ? !!entry.rememberPos : !!data.rememberPos[current.meta.id];
|
||||
}
|
||||
function toggleRememberPos() {
|
||||
if (!current || !current.meta) return;
|
||||
const id = current.meta.id;
|
||||
const on = !rememberPosEnabled();
|
||||
const entry = currentPlaylistEntry();
|
||||
if (entry) {
|
||||
if (on) entry.rememberPos = true; else delete entry.rememberPos;
|
||||
} else {
|
||||
if (on) data.rememberPos[id] = true; else delete data.rememberPos[id];
|
||||
}
|
||||
persist();
|
||||
updateRememberPosUI();
|
||||
toast(on ? 'Remembering playback position' : 'Not remembering position');
|
||||
}
|
||||
function updateRememberPosUI() {
|
||||
const btn = $('rememberPosBtn');
|
||||
if (btn) btn.classList.toggle('active', rememberPosEnabled());
|
||||
}
|
||||
|
||||
function updateAbUI() {
|
||||
const aSet = abA !== null, bSet = abB !== null;
|
||||
const aBtn = $('abABtn'), bBtn = $('abBBtn'), clrBtn = $('abClearBtn');
|
||||
@@ -3219,7 +3275,7 @@ function renderRelated() {
|
||||
}
|
||||
item.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.ri-channel.link')) return;
|
||||
queue = [v]; queueIndex = 0; Player.loadVideo(v); renderUpNext();
|
||||
queue = [v]; queueIndex = 0; playFullMode = false; Player.loadVideo(v); renderUpNext();
|
||||
});
|
||||
list.appendChild(item);
|
||||
});
|
||||
@@ -3358,6 +3414,7 @@ function importBackup(e) {
|
||||
const histIds = new Set(data.history.map((v) => v.id));
|
||||
for (const v of (imp.history || [])) if (!histIds.has(v.id)) { data.history.push(v); histIds.add(v.id); }
|
||||
Object.assign(data.resumePositions, imp.resumePositions || {});
|
||||
Object.assign(data.rememberPos, imp.rememberPos || {});
|
||||
for (const [id, cnt] of Object.entries(imp.playCount || {})) data.playCount[id] = Math.max(data.playCount[id] || 0, cnt);
|
||||
Object.assign(data.abMarkers, imp.abMarkers || {});
|
||||
persist();
|
||||
@@ -3526,6 +3583,7 @@ async function boot() {
|
||||
history: loaded.history || [],
|
||||
queue: loaded.queue || [],
|
||||
resumePositions: loaded.resumePositions || {},
|
||||
rememberPos: loaded.rememberPos || {},
|
||||
playCount: loaded.playCount || {},
|
||||
abMarkers: loaded.abMarkers || {},
|
||||
lastAutoBackup: loaded.lastAutoBackup || 0,
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
<button id="abABtn" class="ctrl ab-ctrl" title="Set A-B loop start [A]">A</button>
|
||||
<button id="abBBtn" class="ctrl ab-ctrl" title="Set A-B loop end [B]">B</button>
|
||||
<button id="abClearBtn" class="ctrl ab-ctrl hidden" title="Clear A-B loop">↺</button>
|
||||
<button id="rememberPosBtn" class="ctrl" title="Remember playback position for this song (per playlist)">📍</button>
|
||||
<div class="vol">
|
||||
<button id="muteBtn" class="ctrl" title="Mute">🔊</button>
|
||||
<input id="volume" type="range" min="0" max="1" step="0.01" value="1" />
|
||||
|
||||
Reference in New Issue
Block a user