Add the Glass Stage layout theme with overlay controls, 10 s skips, quick chips and stacked tiles

This commit is contained in:
Claude
2026-10-01 21:40:20 +00:00
parent 36fc5ff9e4
commit 19973c75c0
6 changed files with 309 additions and 14 deletions

View File

@@ -364,7 +364,7 @@ const DEFAULT_SETTINGS = {
density: 'comfortable', // 'comfortable' | 'compact'
perfMode: false, // disable heavy visual effects for speed
reduceMotion: false, // disable animations
layout: 'classic', // 'classic'|'sanctuary-stage'|'liturgical'|'living-hymnal'|'broadcaster'|'contemplative'|'bento-hub'
layout: 'classic', // 'classic'|'glass-stage'|'sanctuary-stage'|'liturgical'|'living-hymnal'|'broadcaster'|'contemplative'|'bento-hub'
autoBackupEnabled: false,
autoBackupIntervalDays: 7,
serviceVideo: false, // show the video panel inside service mode (lyric videos)
@@ -1103,6 +1103,8 @@ function syncMuteIcon() {
const waves = lvl === 0 ? '<path d="M16 9l5 6M21 9l-5 6"/>' : lvl === 1 ? '<path d="M15.5 9.5a4 4 0 0 1 0 5"/>' : '<path d="M15.5 9.5a4 4 0 0 1 0 5M18.5 6.5a8 8 0 0 1 0 11"/>';
b.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9.5v5h3.5L12 18.5v-13L7.5 9.5z" class="spk"/>${waves}</svg>`;
b.classList.toggle('muted', lvl === 0);
const pct = $('volPct');
if (pct) pct.textContent = Math.round(v * 100) + '%';
b.title = lvl === 0 ? 'Unmute' : 'Mute';
b.setAttribute('aria-label', b.title);
}
@@ -2411,6 +2413,10 @@ function wirePlayerEvents() {
function updatePlayBtn() {
Remote.changed();
els.playBtn.innerHTML = Player.master.paused ? ICON_PLAY : ICON_PAUSE;
const sp = $('stgPlay');
if (sp) sp.innerHTML = Player.master.paused ? ICON_PLAY : ICON_PAUSE;
const stage = document.querySelector('.player-stage');
if (stage) stage.classList.toggle('is-paused', !!Player.master.paused);
$('miniPlayBtn').textContent = Player.master.paused ? '▶' : '⏸';
const spBtn = $('servicePlayBtn');
if (spBtn) spBtn.textContent = Player.master.paused ? '▶' : '⏸';
@@ -2656,6 +2662,11 @@ function openMoreActions() {
}
// Reflect cache state on the now-playing Save button.
// Tiles are [icon][label]; only the label text changes with state.
function setTile(btn, label) {
const tl = btn.querySelector('.tl');
if (tl) tl.textContent = label; else btn.textContent = label;
}
function updateNowPlayingActions() {
if (!current || !current.meta) return;
const id = current.meta.id;
@@ -2674,20 +2685,20 @@ function updateNowPlayingActions() {
const btn = els.saveBtn;
if (!btn) return;
if (removing.has(id)) {
btn.textContent = '⏳ Removing…';
setTile(btn, 'Removing…');
btn.classList.remove('done');
btn.disabled = true;
} else if (downloading.has(id)) {
btn.textContent = '⏳ Saving…';
setTile(btn, 'Saving…');
btn.classList.remove('done');
btn.disabled = true;
} else if (cachedIds.has(id)) {
btn.textContent = '✓ Saved';
setTile(btn, 'Saved');
btn.classList.add('done');
btn.disabled = false;
btn.title = 'Saved for offline — click to remove from cache';
} else {
btn.textContent = '⬇ Save';
setTile(btn, 'Save');
btn.classList.remove('done');
btn.disabled = false;
btn.title = 'Save this video for offline playback';
@@ -3889,6 +3900,48 @@ const Notes = (() => {
};
})();
// ---- Glass Stage layout: stage overlay, 10 s skips, quick-setting chips ----
(function glassStage() {
const skip = (d) => { try { Player.seek(Math.max(0, Player.master.currentTime + d)); } catch { /* not ready */ } };
const on = (id, fn) => { const el = document.getElementById(id); if (el) el.addEventListener('click', fn); };
on('back10Btn', () => skip(-10)); on('fwd10Btn', () => skip(10));
on('stgBack', () => skip(-10)); on('stgFwd', () => skip(10));
on('stgPlay', () => $('playBtn').click());
const setAudio = (v) => { const t = $('audioOnlyToggle'); if (t && t.checked !== v) { t.checked = v; t.dispatchEvent(new Event('change', { bubbles: true })); } };
on('stgVideo', () => setAudio(false)); on('stgAudio', () => setAudio(true));
const stage = document.querySelector('.player-stage');
let hideT = null;
if (stage) stage.addEventListener('click', (e) => {
if (e.target.closest('.stg-overlay button')) return;
stage.classList.add('stg-show');
clearTimeout(hideT);
hideT = setTimeout(() => stage.classList.remove('stg-show'), 3000);
});
function chips() {
if (document.documentElement.dataset.layout !== 'glass-stage') return;
const box = $('deckChips');
if (!box) return;
const after = (id) => { const t = String(($(id) || {}).title || ''); const m = t.match(/:\s*([^—(]+?)\s*(?:—|\(|$)/); return m ? m[1].trim() : ''; };
const list = [['Speed ' + (($('speedSelect') || {}).value || '1') + '×', true]];
const eq = after('eqBtn'); if (eq) list.push(['EQ: ' + eq, false]);
const tr = after('transitionBtn'); if (tr) list.push([tr === 'Normal' ? 'Gapless: off' : tr, false]);
const pip = $('pipBtn'); if (pip && !pip.classList.contains('hidden')) list.push(['PiP ready', false]);
const key = list.map((x) => x[0]).join('|');
if (box.dataset.key !== key) {
box.dataset.key = key;
box.innerHTML = list.map(([t, a]) => `<button type="button" class="dchip${a ? ' on' : ''}"></button>`).join('');
[...box.children].forEach((b, i) => { b.textContent = list[i][0]; b.onclick = openMoreControls; });
}
const q = $('qualitySelect'), sq = $('stgQuality');
if (sq && q && q.options.length) sq.textContent = q.options[q.selectedIndex] ? q.options[q.selectedIndex].text : 'Auto';
const ao = !!data.settings.audioOnly;
const v = $('stgVideo'), a = $('stgAudio');
if (v) v.classList.toggle('on', !ao); if (a) a.classList.toggle('on', ao);
}
setInterval(chips, 1500);
setTimeout(chips, 400);
})();
// ============================================================================
// Phone remote (server/remote.js relays both ends over /ws/remote)
// ============================================================================
@@ -6931,7 +6984,7 @@ function updateLoopRepeatButtons() {
const mb = $('modeBtn');
if (mb) {
const i = playModeIndex();
mb.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${MODE_ICONS[i]}</svg>`;
mb.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${MODE_ICONS[i]}</svg><span class="mode-name">${PLAY_MODES[i].name}</span>`;
mb.classList.toggle('active', i > 0);
mb.dataset.badge = i === 3 ? '∞' : '';
mb.title = `${PLAY_MODES[i].name} — ${PLAY_MODES[i].sub}. Tap to change.`;
@@ -7939,7 +7992,7 @@ async function renderSettings() {
</label>
<label class="set-row">
<span>Layout theme</span>
${sel('setLayout', data.settings.layout || 'classic', [['classic', 'Classic (default)'], ['sanctuary-stage', 'Sanctuary Stage'], ['liturgical', 'Liturgical Chronicle'], ['living-hymnal', 'Living Hymnal'], ['broadcaster', 'Sunday Broadcaster'], ['contemplative', 'Contemplative'], ['bento-hub', 'Bento Hub']])}
${sel('setLayout', data.settings.layout || 'classic', [['classic', 'Classic (default)'], ['glass-stage', 'Glass Stage'], ['sanctuary-stage', 'Sanctuary Stage'], ['liturgical', 'Liturgical Chronicle'], ['living-hymnal', 'Living Hymnal'], ['broadcaster', 'Sunday Broadcaster'], ['contemplative', 'Contemplative'], ['bento-hub', 'Bento Hub']])}
</label>
<label class="set-row">
<span>Font size</span>