Add service mode set-list view with screen wake lock

This commit is contained in:
Jonathan Sykes
2026-08-16 13:51:27 +00:00
parent c5f47514ad
commit 14e1d8e3dd
3 changed files with 457 additions and 8 deletions

View File

@@ -1384,6 +1384,7 @@ const Player = {
updateNowPlayingActions();
markPlayingCard();
showMiniBar();
updateServiceMode();
// Media Session API — OS media keys + lock screen
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
@@ -1711,10 +1712,19 @@ const Player = {
// faster than the 1s watchdog interval, which matters for a jarring "audio
// paused, video kept playing" UX.
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') Player.resumeIfNeeded();
if (document.visibilityState === 'visible') {
Player.resumeIfNeeded();
if (isServiceModeOpen()) acquireWakeLock();
}
});
window.addEventListener('pageshow', () => {
Player.resumeIfNeeded();
if (isServiceModeOpen()) acquireWakeLock();
});
window.addEventListener('focus', () => {
Player.resumeIfNeeded();
if (isServiceModeOpen()) acquireWakeLock();
});
window.addEventListener('pageshow', () => Player.resumeIfNeeded());
window.addEventListener('focus', () => Player.resumeIfNeeded());
function showSpinner(on) {
els.spinner.classList.toggle('hidden', !on);
@@ -1839,6 +1849,8 @@ function wirePlayerEvents() {
function updatePlayBtn() {
els.playBtn.textContent = Player.master.paused ? '▶' : '⏸';
$('miniPlayBtn').textContent = Player.master.paused ? '▶' : '⏸';
const spBtn = $('servicePlayBtn');
if (spBtn) spBtn.textContent = Player.master.paused ? '▶' : '⏸';
}
// Reflect cache state on the now-playing Save button.
function updateNowPlayingActions() {
@@ -1878,6 +1890,7 @@ function updateProgress() {
els.durTime.textContent = fmtTime(dur);
if (dur) els.seek.value = String((cur / dur) * 1000);
updateMiniBar();
updateServiceProgress(cur, dur);
// 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").
@@ -2029,6 +2042,7 @@ function playQueue(index = 0) {
}
function renderUpNext() {
updateServiceUpNext();
const upcoming = queue.slice(queueIndex + 1);
if (!upcoming.length) { $('upnext').classList.add('hidden'); return; }
$('upnext').classList.remove('hidden');
@@ -2113,6 +2127,131 @@ function playPrev() {
}
}
// ============================================================================
// Service mode (set-list view) with screen wake lock
// ============================================================================
let serviceWakeLock = null;
function isServiceModeOpen() {
const el = $('serviceMode');
return !!(el && !el.classList.contains('hidden'));
}
async function acquireWakeLock() {
if (!isServiceModeOpen()) return;
if (!('wakeLock' in navigator) || typeof navigator.wakeLock.request !== 'function') {
serviceWakeLock = null;
updateWakeStateUI();
return;
}
try {
if (serviceWakeLock && !serviceWakeLock.released) {
try { await serviceWakeLock.release(); } catch {}
}
const lock = await navigator.wakeLock.request('screen');
serviceWakeLock = lock;
lock.addEventListener('release', () => {
if (serviceWakeLock === lock) {
serviceWakeLock = null;
updateWakeStateUI();
}
});
} catch {
serviceWakeLock = null;
}
updateWakeStateUI();
}
async function releaseWakeLock() {
if (serviceWakeLock) {
try {
if (!serviceWakeLock.released) {
await serviceWakeLock.release();
}
} catch {}
serviceWakeLock = null;
}
updateWakeStateUI();
}
function updateWakeStateUI() {
const el = $('serviceWakeState');
if (!el) return;
const isAwake = !!(serviceWakeLock && !serviceWakeLock.released);
el.textContent = isAwake ? 'Screen will stay awake' : 'Screen may sleep';
el.classList.toggle('awake', isAwake);
}
function updateServiceUpNext() {
const upNextEl = $('serviceUpNext');
if (!upNextEl) return;
const upcoming = (queue && queueIndex >= 0) ? queue.slice(queueIndex + 1) : [];
if (upcoming.length > 0) {
upNextEl.textContent = 'Up next: ' + (upcoming[0].title || 'Untitled');
} else {
upNextEl.textContent = 'Last song';
}
}
function updateServiceProgress(cur, dur) {
const curEl = $('serviceCurTime');
const durEl = $('serviceDurTime');
if (curEl) curEl.textContent = fmtTime(cur || 0);
if (durEl) durEl.textContent = fmtTime(dur || 0);
}
function updateServiceMode() {
if (!isServiceModeOpen()) return;
const meta = current ? current.meta : null;
const titleEl = $('serviceTitle');
const chanEl = $('serviceChannel');
if (titleEl) titleEl.textContent = meta ? (meta.title || '') : 'No track playing';
if (chanEl) chanEl.textContent = meta ? (meta.channel || '') : '';
// Current track note (Section 5): read entry.notes, degrade gracefully if absent
const currentEntry = (queue && queueIndex >= 0 && queueIndex < queue.length) ? queue[queueIndex] : meta;
const note = (currentEntry && currentEntry.notes) ? String(currentEntry.notes).trim() : '';
const noteEl = $('serviceNote');
if (noteEl) {
if (note) {
noteEl.textContent = note;
noteEl.classList.remove('hidden');
} else {
noteEl.textContent = '';
noteEl.classList.add('hidden');
}
}
updateServiceUpNext();
const cur = Player.master ? (Player.master.currentTime || 0) : 0;
const dur = Player.master ? (Player.master.duration || (meta ? meta.duration : 0) || 0) : 0;
updateServiceProgress(cur, dur);
const spBtn = $('servicePlayBtn');
if (spBtn && Player.master) {
spBtn.textContent = Player.master.paused ? '▶' : '⏸';
}
updateWakeStateUI();
}
function enterServiceMode(pl) {
if (pl && pl.videos && pl.videos.length) {
playFromList(pl.videos, 0, 'playlist:' + pl.id);
}
const el = $('serviceMode');
if (el) el.classList.remove('hidden');
updateServiceMode();
acquireWakeLock();
}
function exitServiceMode() {
const el = $('serviceMode');
if (el) el.classList.add('hidden');
releaseWakeLock();
}
// ============================================================================
// Channel view — list a channel's uploads with quick actions
// ============================================================================
@@ -2375,6 +2514,13 @@ function renderList() {
playFromList(pl.videos, 0, 'playlist:' + pl.id, { playFull: true });
toast('Playing in full — stops after the last video');
};
const serviceBtn = document.createElement('button');
serviceBtn.textContent = '🎤 Service mode';
serviceBtn.title = 'Set-list view with screen wake lock';
serviceBtn.onclick = () => {
if (!pl.videos || !pl.videos.length) { toast('Playlist is empty'); return; }
enterServiceMode(pl);
};
const queueAll = document.createElement('button');
queueAll.textContent = '+ Queue';
queueAll.onclick = () => { pl.videos.forEach((v) => addToQueue(v, { quiet: true })); toast('Added playlist to queue'); };
@@ -2398,7 +2544,7 @@ function renderList() {
pinBtn.title = isPinned ? 'Pinned for offline keep — click to unpin' : 'Pin to keep all videos in this playlist available offline';
pinBtn.onclick = () => togglePinPlaylist(pl);
const buttons = [playAll, playFull, queueAll, offlineBtn, pinBtn];
const buttons = [playAll, playFull, serviceBtn, queueAll, offlineBtn, pinBtn];
if (pl.videos && pl.videos.length >= 1) {
const share = document.createElement('button');
share.textContent = '🔗 Share';
@@ -4136,15 +4282,24 @@ document.querySelectorAll('.chip').forEach((c) => {
else if (e.key === 'b') { if (current) setAbB(); }
else if (e.key === '?') toggleShortcutHelp();
else if (e.key === 'Escape') {
const app = document.querySelector('.app');
if (app && app.classList.contains('sidebar-open')) closeSidebar();
else if (!$('shortcutHelp').classList.contains('hidden')) $('shortcutHelp').classList.add('hidden');
else if (selectMode) { exitSelectMode(); renderList(); }
if (isServiceModeOpen()) exitServiceMode();
else {
const app = document.querySelector('.app');
if (app && app.classList.contains('sidebar-open')) closeSidebar();
else if (!$('shortcutHelp').classList.contains('hidden')) $('shortcutHelp').classList.add('hidden');
else if (selectMode) { exitSelectMode(); renderList(); }
}
}
});
$('modal').addEventListener('click', (e) => { if (e.target.id === 'modal') closeModal(); });
// Service mode controls
$('servicePlayBtn').addEventListener('click', () => Player.toggle());
$('serviceNextBtn').addEventListener('click', playNext);
$('servicePrevBtn').addEventListener('click', playPrev);
$('serviceExitBtn').addEventListener('click', exitServiceMode);
// Mini now-playing bar
$('miniPlayBtn').addEventListener('click', (e) => { e.stopPropagation(); Player.toggle(); });
$('miniBar').addEventListener('click', () => {