Add service mode set-list view with screen wake lock
This commit is contained in:
171
frontend/app.js
171
frontend/app.js
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user