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'
|
layout: 'classic', // 'classic'|'sanctuary-stage'|'liturgical'|'living-hymnal'|'broadcaster'|'contemplative'|'bento-hub'
|
||||||
autoBackupEnabled: false,
|
autoBackupEnabled: false,
|
||||||
autoBackupIntervalDays: 7,
|
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 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'
|
let view = { type: 'search' }; // 'search'|'history'|'playlist'|'settings'|'queue'|'saved'|'downloads'|'channel'
|
||||||
@@ -2345,6 +2346,62 @@ function updateServiceMode() {
|
|||||||
updateWakeStateUI();
|
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) {
|
function enterServiceMode(pl) {
|
||||||
if (pl && pl.videos && pl.videos.length) {
|
if (pl && pl.videos && pl.videos.length) {
|
||||||
const source = 'playlist:' + pl.id;
|
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
|
// Hides the mini-bar / bottom-nav / sidebar for as long as the overlay is up
|
||||||
// (see .service-open in styles.css).
|
// (see .service-open in styles.css).
|
||||||
document.body.classList.add('service-open');
|
document.body.classList.add('service-open');
|
||||||
|
updateServiceVideoUI();
|
||||||
updateServiceMode();
|
updateServiceMode();
|
||||||
acquireWakeLock();
|
acquireWakeLock();
|
||||||
}
|
}
|
||||||
@@ -2373,6 +2431,9 @@ function exitServiceMode() {
|
|||||||
const el = $('serviceMode');
|
const el = $('serviceMode');
|
||||||
if (el) el.classList.add('hidden');
|
if (el) el.classList.add('hidden');
|
||||||
document.body.classList.remove('service-open');
|
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();
|
releaseWakeLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4522,6 +4583,11 @@ document.querySelectorAll('.chip').forEach((c) => {
|
|||||||
$('serviceNextBtn').addEventListener('click', playNext);
|
$('serviceNextBtn').addEventListener('click', playNext);
|
||||||
$('servicePrevBtn').addEventListener('click', playPrev);
|
$('servicePrevBtn').addEventListener('click', playPrev);
|
||||||
$('serviceExitBtn').addEventListener('click', exitServiceMode);
|
$('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
|
// Mini now-playing bar
|
||||||
$('miniPlayBtn').addEventListener('click', (e) => { e.stopPropagation(); Player.toggle(); });
|
$('miniPlayBtn').addEventListener('click', (e) => { e.stopPropagation(); Player.toggle(); });
|
||||||
@@ -4534,6 +4600,12 @@ document.querySelectorAll('.chip').forEach((c) => {
|
|||||||
if (isPortraitPWA()) scrollPlayerIntoViewPortrait();
|
if (isPortraitPWA()) scrollPlayerIntoViewPortrait();
|
||||||
else els.playerPane.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
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(); });
|
$('miniCloseBtn').addEventListener('click', (e) => { e.stopPropagation(); hideMiniBar(); });
|
||||||
|
|
||||||
// Drag-to-reorder: prevent default on cards container
|
// Drag-to-reorder: prevent default on cards container
|
||||||
|
|||||||
@@ -260,9 +260,17 @@
|
|||||||
<section id="serviceMode" class="service-mode hidden" aria-label="Service Mode">
|
<section id="serviceMode" class="service-mode hidden" aria-label="Service Mode">
|
||||||
<div class="service-topbar">
|
<div class="service-topbar">
|
||||||
<div id="serviceWakeState" class="service-wake-state">Screen may sleep</div>
|
<div id="serviceWakeState" class="service-wake-state">Screen may sleep</div>
|
||||||
|
<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>
|
<button id="serviceExitBtn" class="service-exit-btn" type="button" aria-label="Exit Service Mode">Exit</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="service-center">
|
<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">
|
<div class="service-meta">
|
||||||
<h1 id="serviceTitle" class="service-title"></h1>
|
<h1 id="serviceTitle" class="service-title"></h1>
|
||||||
<div id="serviceChannel" class="service-channel"></div>
|
<div id="serviceChannel" class="service-channel"></div>
|
||||||
@@ -293,6 +301,8 @@
|
|||||||
<div id="miniChannel" class="mini-channel"></div>
|
<div id="miniChannel" class="mini-channel"></div>
|
||||||
</div>
|
</div>
|
||||||
<span id="miniTime" class="mini-time">0:00</span>
|
<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>
|
<button id="miniCloseBtn" class="mini-btn mini-close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="miniProgress" class="mini-progress"><div id="miniProgressFill" class="mini-progress-fill"></div></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);
|
color: var(--text-dim);
|
||||||
}
|
}
|
||||||
.mini-close:hover { color: var(--text); border-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 {
|
.mini-info {
|
||||||
min-width: 0; flex: 1;
|
min-width: 0; flex: 1;
|
||||||
display: flex; flex-direction: column;
|
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);
|
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 {
|
.service-exit-btn {
|
||||||
background: var(--bg-3);
|
background: var(--bg-3);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
@@ -2856,6 +2903,9 @@ body.service-open .sidebar-backdrop {
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
padding: 12px 14px;
|
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 {
|
.service-time {
|
||||||
font-size: clamp(16px, 4.6vw, 22px);
|
font-size: clamp(16px, 4.6vw, 22px);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user