Add a service mode shortcut to the now-playing bar and a video panel toggle

This commit is contained in:
Jonathan Sykes
2026-09-08 16:59:43 +08:00
parent 3e1b56610f
commit a3089dde74
3 changed files with 133 additions and 1 deletions

View File

@@ -251,6 +251,7 @@ const DEFAULT_SETTINGS = {
layout: 'classic', // 'classic'|'sanctuary-stage'|'liturgical'|'living-hymnal'|'broadcaster'|'contemplative'|'bento-hub'
autoBackupEnabled: false,
autoBackupIntervalDays: 7,
serviceVideo: false, // show the video panel inside service mode (lyric videos)
};
let data = { playlists: [], history: [], queue: [], customVideos: [], pinnedPlaylists: [], settings: { ...DEFAULT_SETTINGS }, resumePositions: {}, rememberPos: {}, playCount: {}, abMarkers: {}, lastAutoBackup: 0, profile: null };
let view = { type: 'search' }; // 'search'|'history'|'playlist'|'settings'|'queue'|'saved'|'downloads'|'channel'
@@ -2345,6 +2346,62 @@ function updateServiceMode() {
updateWakeStateUI();
}
// ---------- Service mode video panel ----------
// Lyric videos carry the words in the picture, so service mode can show the
// video instead of hiding it. The <video> element is NEVER reparented — moving
// a playing video breaks playback on iOS (and drops the stream on some
// Androids). Instead .player-stage is pinned over a placeholder slot inside the
// overlay with inline fixed coordinates, which are cleared when it closes.
function serviceVideoOn() {
return !!data.settings.serviceVideo && !data.settings.audioOnly;
}
function positionServiceVideo() {
const stage = document.querySelector('.player-stage');
if (!stage) return;
if (!(isServiceModeOpen() && serviceVideoOn())) {
stage.style.position = '';
stage.style.top = '';
stage.style.left = '';
stage.style.width = '';
stage.style.height = '';
return;
}
const slot = $('serviceVideoSlot');
if (!slot) return;
const r = slot.getBoundingClientRect();
if (!r.width || !r.height) return; // slot not laid out yet
stage.style.position = 'fixed';
stage.style.top = `${r.top}px`;
stage.style.left = `${r.left}px`;
stage.style.width = `${r.width}px`;
stage.style.height = `${r.height}px`;
}
function updateServiceVideoUI() {
const on = isServiceModeOpen() && serviceVideoOn();
document.body.classList.toggle('service-video', on);
const btn = $('serviceVideoBtn');
if (btn) {
btn.textContent = serviceVideoOn() ? '📺 Video on' : '📺 Video off';
btn.setAttribute('aria-pressed', serviceVideoOn() ? 'true' : 'false');
btn.classList.toggle('on', serviceVideoOn());
}
// The slot only has a box once the class above has been applied, so measure
// on the next frame rather than in this one.
requestAnimationFrame(positionServiceVideo);
}
function toggleServiceVideo() {
if (data.settings.audioOnly) {
toast('Audio-only mode is on — turn it off in Settings to show video');
return;
}
data.settings.serviceVideo = !data.settings.serviceVideo;
persist();
updateServiceVideoUI();
}
function enterServiceMode(pl) {
if (pl && pl.videos && pl.videos.length) {
const source = 'playlist:' + pl.id;
@@ -2365,6 +2422,7 @@ function enterServiceMode(pl) {
// Hides the mini-bar / bottom-nav / sidebar for as long as the overlay is up
// (see .service-open in styles.css).
document.body.classList.add('service-open');
updateServiceVideoUI();
updateServiceMode();
acquireWakeLock();
}
@@ -2373,6 +2431,9 @@ function exitServiceMode() {
const el = $('serviceMode');
if (el) el.classList.add('hidden');
document.body.classList.remove('service-open');
// Runs after the overlay is hidden, so this clears the pinned video styles
// and returns .player-stage to the player pane.
updateServiceVideoUI();
releaseWakeLock();
}
@@ -4522,6 +4583,11 @@ document.querySelectorAll('.chip').forEach((c) => {
$('serviceNextBtn').addEventListener('click', playNext);
$('servicePrevBtn').addEventListener('click', playPrev);
$('serviceExitBtn').addEventListener('click', exitServiceMode);
$('serviceVideoBtn').addEventListener('click', toggleServiceVideo);
// The pinned panel is positioned in viewport coordinates, so it has to be
// re-measured whenever the viewport changes shape.
window.addEventListener('resize', positionServiceVideo);
window.addEventListener('orientationchange', () => setTimeout(positionServiceVideo, 150));
// Mini now-playing bar
$('miniPlayBtn').addEventListener('click', (e) => { e.stopPropagation(); Player.toggle(); });
@@ -4534,6 +4600,12 @@ document.querySelectorAll('.chip').forEach((c) => {
if (isPortraitPWA()) scrollPlayerIntoViewPortrait();
else els.playerPane.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});
// Opens the set-list view for whatever is already playing. No playlist arg,
// so enterServiceMode leaves the live queue and position untouched.
$('miniServiceBtn').addEventListener('click', (e) => {
e.stopPropagation();
enterServiceMode();
});
$('miniCloseBtn').addEventListener('click', (e) => { e.stopPropagation(); hideMiniBar(); });
// Drag-to-reorder: prevent default on cards container