Fast lyric fitting for long videos; Search tab returns to the home screen while playing
- fitLyricLines measures text on a canvas with the line's own font and writes every --fit after measuring (it used an in-list DOM probe and wrote inside the loop, re-laying-out the whole list per line). 1200 lines: 600 ms -> 28 ms; sizes match the old method within 1 px. Service mode on long sermons with hundreds of transcribed lines opened slowly enough on phones to look broken. - Bottom-nav Search while something plays shows the home screen (playlist board / hero) over the player pane; the media keeps playing unseen and the mini bar brings the player back (its close button too, so there is always a way back). Starting another video restores the player.
This commit is contained in:
@@ -1974,6 +1974,7 @@ const Player = {
|
|||||||
// instead of hijacking playback (longest window: save-before-playing).
|
// instead of hijacking playback (longest window: save-before-playing).
|
||||||
const seq = (this._loadSeq = (this._loadSeq || 0) + 1);
|
const seq = (this._loadSeq = (this._loadSeq || 0) + 1);
|
||||||
showSpinner(true);
|
showSpinner(true);
|
||||||
|
setHomeOverPlayer(false);
|
||||||
els.placeholder.classList.add('hidden');
|
els.placeholder.classList.add('hidden');
|
||||||
// Revoke any previous OPFS blob URL to free memory
|
// Revoke any previous OPFS blob URL to free memory
|
||||||
if (_currentBlobUrl) {
|
if (_currentBlobUrl) {
|
||||||
@@ -2981,14 +2982,32 @@ function fitLyricLines(list, { maxShare = 0.17, maxPx = 160, minPx = 18, uniform
|
|||||||
const width = list.clientWidth * 0.92 * 0.97; // 4% side padding + slack
|
const width = list.clientWidth * 0.92 * 0.97; // 4% side padding + slack
|
||||||
const cap = Math.max(30, Math.min(list.clientHeight * maxShare, maxPx));
|
const cap = Math.max(30, Math.min(list.clientHeight * maxShare, maxPx));
|
||||||
const clamp = (px) => Math.max(minPx, Math.min(cap, px));
|
const clamp = (px) => Math.max(minPx, Math.min(cap, px));
|
||||||
const probe = document.createElement('div');
|
// Text widths come from a canvas (no page layout at all) using the line's
|
||||||
probe.className = 'sl-line';
|
// own font; the in-list DOM probe stays as the fallback.
|
||||||
probe.style.cssText = 'position:absolute;left:0;top:0;visibility:hidden;white-space:nowrap;width:auto;padding:0;font-size:100px;transition:none';
|
const cs = getComputedStyle(rows[0]);
|
||||||
list.appendChild(probe);
|
const ctx2d = (fitLyricLines._c || (fitLyricLines._c = document.createElement('canvas'))).getContext('2d');
|
||||||
const widthAt = (px) => { probe.style.fontSize = px + 'px'; return probe.getBoundingClientRect().width; };
|
let probe = null;
|
||||||
|
let widthAt;
|
||||||
|
if (ctx2d && cs.fontFamily) {
|
||||||
|
const lsEm = (parseFloat(cs.letterSpacing) || 0) / (parseFloat(cs.fontSize) || 1); // letter-spacing in em
|
||||||
|
widthAt = (px) => {
|
||||||
|
ctx2d.font = `${cs.fontStyle} ${cs.fontWeight} ${px}px ${cs.fontFamily}`;
|
||||||
|
const t = widthAt.text;
|
||||||
|
return ctx2d.measureText(t).width + lsEm * px * Math.max(0, t.length - 1);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
probe = document.createElement('div');
|
||||||
|
probe.className = 'sl-line';
|
||||||
|
probe.style.cssText = 'position:absolute;left:0;top:0;visibility:hidden;white-space:nowrap;width:auto;padding:0;font-size:100px;transition:none';
|
||||||
|
list.appendChild(probe);
|
||||||
|
widthAt = (px) => { probe.style.fontSize = px + 'px'; probe.textContent = widthAt.text; return probe.getBoundingClientRect().width; };
|
||||||
|
}
|
||||||
|
// Measure every line first and write the sizes afterwards: writing --fit
|
||||||
|
// inside the loop dirtied the list's layout, so each following measurement
|
||||||
|
// re-laid-out every line (O(n²) — seconds on a phone for a long sermon).
|
||||||
const fits = [];
|
const fits = [];
|
||||||
for (const el of rows) {
|
for (const el of rows) {
|
||||||
probe.textContent = el.textContent;
|
widthAt.text = el.textContent;
|
||||||
const w100 = widthAt(100);
|
const w100 = widthAt(100);
|
||||||
let px = cap;
|
let px = cap;
|
||||||
if (w100) {
|
if (w100) {
|
||||||
@@ -2996,14 +3015,11 @@ function fitLyricLines(list, { maxShare = 0.17, maxPx = 160, minPx = 18, uniform
|
|||||||
const wEst = widthAt(est);
|
const wEst = widthAt(est);
|
||||||
px = Math.floor(clamp(wEst ? est * (width / wEst) : est));
|
px = Math.floor(clamp(wEst ? est * (width / wEst) : est));
|
||||||
}
|
}
|
||||||
el.style.setProperty('--fit', px + 'px');
|
|
||||||
fits.push(px);
|
fits.push(px);
|
||||||
}
|
}
|
||||||
probe.remove();
|
if (probe) probe.remove();
|
||||||
if (uniform) {
|
const same = uniform ? Math.min(...fits) : 0;
|
||||||
const px = Math.min(...fits);
|
rows.forEach((el, i) => el.style.setProperty('--fit', (uniform ? same : fits[i]) + 'px'));
|
||||||
rows.forEach((el) => el.style.setProperty('--fit', px + 'px'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -7032,6 +7048,26 @@ function updateProgress() {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Mini now-playing bar
|
// Mini now-playing bar
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
// Home while playing: the Search tab brings back the home screen (playlist
|
||||||
|
// board / hero) over the player pane, using the same look as "nothing playing"
|
||||||
|
// (.empty). The media keeps playing — it is only made invisible, never removed
|
||||||
|
// or paused — and the mini bar stays up to get back to it.
|
||||||
|
function setHomeOverPlayer(on) {
|
||||||
|
const pane = els.playerPane;
|
||||||
|
if (!pane) return;
|
||||||
|
on = !!on && !!(current && current.meta);
|
||||||
|
if (pane.classList.contains('home-over') === on) return;
|
||||||
|
pane.classList.toggle('home-over', on);
|
||||||
|
pane.classList.toggle('empty', on);
|
||||||
|
if (els.placeholder) els.placeholder.classList.toggle('hidden', !on);
|
||||||
|
if (on) {
|
||||||
|
renderHomePlaylists();
|
||||||
|
showMiniBar();
|
||||||
|
const body = document.querySelector('.body');
|
||||||
|
if (body) body.scrollTo({ top: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showMiniBar() {
|
function showMiniBar() {
|
||||||
if (!current || !current.meta) return;
|
if (!current || !current.meta) return;
|
||||||
$('miniTitle').textContent = current.meta.title;
|
$('miniTitle').textContent = current.meta.title;
|
||||||
@@ -10146,9 +10182,14 @@ function wireUI() {
|
|||||||
b.addEventListener('click', () => { view = { type: b.dataset.view }; render(); });
|
b.addEventListener('click', () => { view = { type: b.dataset.view }; render(); });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bottom-nav (portrait PWA) — same view switching as sidebar nav
|
// Bottom-nav (portrait PWA) — same view switching as sidebar nav. Search
|
||||||
|
// while something plays also brings back the home screen (see setHomeOverPlayer).
|
||||||
document.querySelectorAll('.bottom-nav-btn').forEach((b) => {
|
document.querySelectorAll('.bottom-nav-btn').forEach((b) => {
|
||||||
b.addEventListener('click', () => { view = { type: b.dataset.view }; render(); });
|
b.addEventListener('click', () => {
|
||||||
|
view = { type: b.dataset.view };
|
||||||
|
if (b.dataset.view === 'search') setHomeOverPlayer(true);
|
||||||
|
render();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
els.newPlaylistBtn.addEventListener('click', () => newPlaylist(null));
|
els.newPlaylistBtn.addEventListener('click', () => newPlaylist(null));
|
||||||
@@ -10476,6 +10517,7 @@ document.querySelectorAll('.chip').forEach((c) => {
|
|||||||
// Mini now-playing bar
|
// Mini now-playing bar
|
||||||
$('miniPlayBtn').addEventListener('click', (e) => { e.stopPropagation(); Player.toggle(); });
|
$('miniPlayBtn').addEventListener('click', (e) => { e.stopPropagation(); Player.toggle(); });
|
||||||
$('miniBar').addEventListener('click', () => {
|
$('miniBar').addEventListener('click', () => {
|
||||||
|
setHomeOverPlayer(false);
|
||||||
hideMiniBar();
|
hideMiniBar();
|
||||||
// Scroll the player into view if needed. In portrait PWA, scroll only the
|
// Scroll the player into view if needed. In portrait PWA, scroll only the
|
||||||
// .body pane (the designated scroll container) — scrollIntoView() also
|
// .body pane (the designated scroll container) — scrollIntoView() also
|
||||||
@@ -10490,7 +10532,8 @@ document.querySelectorAll('.chip').forEach((c) => {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
enterServiceMode();
|
enterServiceMode();
|
||||||
});
|
});
|
||||||
$('miniCloseBtn').addEventListener('click', (e) => { e.stopPropagation(); hideMiniBar(); });
|
// On the home screen the mini bar is the way back, so ✕ returns to the player.
|
||||||
|
$('miniCloseBtn').addEventListener('click', (e) => { e.stopPropagation(); setHomeOverPlayer(false); hideMiniBar(); });
|
||||||
|
|
||||||
// Drag-to-reorder: prevent default on cards container
|
// Drag-to-reorder: prevent default on cards container
|
||||||
els.cards.addEventListener('dragover', (e) => {
|
els.cards.addEventListener('dragover', (e) => {
|
||||||
|
|||||||
@@ -4280,3 +4280,10 @@ body.landscape-fs .player-stage { touch-action: none; } /* fullscreen (real or t
|
|||||||
}
|
}
|
||||||
.dl-act:hover { border-color: var(--accent); }
|
.dl-act:hover { border-color: var(--accent); }
|
||||||
.dl-act.danger:hover { border-color: #ff5a4f; color: #ff5a4f; }
|
.dl-act.danger:hover { border-color: #ff5a4f; color: #ff5a4f; }
|
||||||
|
|
||||||
|
/* Home screen over a playing video (Search tab): media stays alive, unseen */
|
||||||
|
.player-pane.home-over video,
|
||||||
|
.player-pane.home-over #stageLyrics,
|
||||||
|
.player-pane.home-over #artFallback { visibility: hidden; }
|
||||||
|
.player-pane.home-over .related-panel { display: none; }
|
||||||
|
.player-pane.home-over > :not(.player-stage) { display: none !important; }
|
||||||
|
|||||||
Reference in New Issue
Block a user