Add a service mode shortcut to the now-playing bar and a video panel toggle
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -260,9 +260,17 @@
|
||||
<section id="serviceMode" class="service-mode hidden" aria-label="Service Mode">
|
||||
<div class="service-topbar">
|
||||
<div id="serviceWakeState" class="service-wake-state">Screen may sleep</div>
|
||||
<button id="serviceExitBtn" class="service-exit-btn" type="button" aria-label="Exit Service Mode">Exit</button>
|
||||
<div class="service-topbar-actions">
|
||||
<button id="serviceVideoBtn" class="service-exit-btn service-video-btn" type="button"
|
||||
aria-pressed="false" aria-label="Show or hide the video">📺 Video off</button>
|
||||
<button id="serviceExitBtn" class="service-exit-btn" type="button" aria-label="Exit Service Mode">Exit</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="service-center">
|
||||
<!-- Placeholder the real .player-stage is positioned over while the
|
||||
video panel is on. The <video> is never moved into here — see
|
||||
positionServiceVideo() in app.js. -->
|
||||
<div id="serviceVideoSlot" class="service-video-slot" aria-hidden="true"></div>
|
||||
<div class="service-meta">
|
||||
<h1 id="serviceTitle" class="service-title"></h1>
|
||||
<div id="serviceChannel" class="service-channel"></div>
|
||||
@@ -293,6 +301,8 @@
|
||||
<div id="miniChannel" class="mini-channel"></div>
|
||||
</div>
|
||||
<span id="miniTime" class="mini-time">0:00</span>
|
||||
<button id="miniServiceBtn" class="mini-btn mini-service" type="button"
|
||||
title="Service mode" aria-label="Service mode">🎤</button>
|
||||
<button id="miniCloseBtn" class="mini-btn mini-close">✕</button>
|
||||
</div>
|
||||
<div id="miniProgress" class="mini-progress"><div id="miniProgressFill" class="mini-progress-fill"></div></div>
|
||||
|
||||
@@ -1469,6 +1469,9 @@ input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.25); }
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.mini-close:hover { color: var(--text); border-color: var(--text-dim); }
|
||||
/* Set-list view straight from the now-playing bar. */
|
||||
.mini-service { font-size: 15px; }
|
||||
.mini-service:hover { border-color: var(--accent); }
|
||||
.mini-info {
|
||||
min-width: 0; flex: 1;
|
||||
display: flex; flex-direction: column;
|
||||
@@ -2647,6 +2650,50 @@ input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.25); }
|
||||
box-shadow: 0 0 10px rgba(52, 211, 153, 0.6);
|
||||
}
|
||||
|
||||
.service-topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Toggle reads as pressed when the video panel is on. */
|
||||
.service-video-btn.on {
|
||||
border-color: var(--accent);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Placeholder the real .player-stage is pinned over. It reserves the space and
|
||||
defines the geometry; it never contains the video itself. */
|
||||
.service-video-slot { display: none; }
|
||||
|
||||
body.service-video .service-video-slot {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
aspect-ratio: 16 / 9;
|
||||
margin: 0 auto 18px;
|
||||
border-radius: var(--radius);
|
||||
background: #000;
|
||||
}
|
||||
|
||||
/* Lift the pinned stage above the overlay (320) while it is on loan. */
|
||||
body.service-open.service-video .player-stage {
|
||||
z-index: 330;
|
||||
}
|
||||
|
||||
/* With the picture carrying the song, the type steps back so both fit. */
|
||||
body.service-video .service-title {
|
||||
font-size: clamp(20px, 4.2vw, 32px);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
body.service-video .service-channel {
|
||||
font-size: clamp(14px, 2.2vw, 18px);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
body.service-video .service-upnext {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.service-exit-btn {
|
||||
background: var(--bg-3);
|
||||
border: 1px solid var(--line);
|
||||
@@ -2856,6 +2903,9 @@ body.service-open .sidebar-backdrop {
|
||||
max-width: none;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
body.service-video .service-video-slot { margin-bottom: 12px; }
|
||||
body.service-video .service-title { font-size: clamp(19px, 5.2vw, 26px); }
|
||||
body.service-video .service-note { display: none; }
|
||||
.service-time {
|
||||
font-size: clamp(16px, 4.6vw, 22px);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user