Chapter picker: tap the chapter badge to jump to any chapter
The CH badge by the scrubber is now a button opening a list of every chapter (number, title, start time); the playing one is highlighted and scrolled into view, a tap seeks there (and resumes if paused). Works with shared chapters and YouTube's.
This commit is contained in:
@@ -3273,7 +3273,7 @@ function fitLyricLines(list, { maxShare = 0.17, maxPx = 160, minPx = 18, uniform
|
|||||||
const Notes = (() => {
|
const Notes = (() => {
|
||||||
const LC = window.LyricsCore;
|
const LC = window.LyricsCore;
|
||||||
const panel = $('notesPanel');
|
const panel = $('notesPanel');
|
||||||
const noop = { load() {}, tick() {}, addBookmark() {}, handleSyncKey() { return false; }, serviceRefresh() {}, chapterAt() { return null; }, jumpChapter() {} };
|
const noop = { load() {}, tick() {}, addBookmark() {}, handleSyncKey() { return false; }, serviceRefresh() {}, chapterAt() { return null; }, jumpChapter() {}, openChapterPicker() {} };
|
||||||
if (!panel || !LC) return noop;
|
if (!panel || !LC) return noop;
|
||||||
|
|
||||||
const CACHE_KEY = 'ytpNotesCache';
|
const CACHE_KEY = 'ytpNotesCache';
|
||||||
@@ -3892,6 +3892,42 @@ const Notes = (() => {
|
|||||||
await saveShared('chapters', { items: [...chapters().map((c) => ({ t: c.t, title: c.title })), v] }, baseRev);
|
await saveShared('chapters', { items: [...chapters().map((c) => ({ t: c.t, title: c.title })), v] }, baseRev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chapter picker: every chapter with its number and start time; the one
|
||||||
|
// playing is marked and scrolled into view. Tapping a row jumps there.
|
||||||
|
function openChapterPicker() {
|
||||||
|
const cs = chapters().slice().sort((a, b) => a.t - b.t);
|
||||||
|
if (!cs.length) { toast('No chapters for this video'); return; }
|
||||||
|
const cur = (Player.master && Player.master.currentTime) || 0;
|
||||||
|
let active = -1;
|
||||||
|
cs.forEach((c, i) => { if (c.t <= cur + 0.5) active = i; });
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.className = 'ch-picker';
|
||||||
|
if (autoChapters()) {
|
||||||
|
const note = document.createElement('p');
|
||||||
|
note.className = 'ch-picker-src';
|
||||||
|
note.textContent = 'Chapters from YouTube';
|
||||||
|
body.appendChild(note);
|
||||||
|
}
|
||||||
|
const list = document.createElement('div');
|
||||||
|
list.className = 'ch-picker-list';
|
||||||
|
list.setAttribute('role', 'listbox');
|
||||||
|
cs.forEach((c, i) => {
|
||||||
|
const b = document.createElement('button');
|
||||||
|
b.type = 'button';
|
||||||
|
b.className = 'ch-pick' + (i === active ? ' on' : '');
|
||||||
|
b.setAttribute('role', 'option');
|
||||||
|
b.setAttribute('aria-selected', String(i === active));
|
||||||
|
b.innerHTML = `<span class="ch-pick-n">${String(i + 1).padStart(2, '0')}</span><span class="ch-pick-title"></span><span class="ch-pick-t">${LC.fmtClock(c.t)}</span>`;
|
||||||
|
b.querySelector('.ch-pick-title').textContent = c.title || `Chapter ${i + 1}`;
|
||||||
|
b.onclick = () => { Player.seek(c.t); if (Player.master && Player.master.paused) Player.toggle(); closeModal(); };
|
||||||
|
list.appendChild(b);
|
||||||
|
});
|
||||||
|
body.appendChild(list);
|
||||||
|
showModal(`Chapters · ${cs.length}`, body, [{ label: 'Close', onClick: closeModal }]);
|
||||||
|
const on = list.querySelector('.ch-pick.on');
|
||||||
|
if (on) requestAnimationFrame(() => { on.scrollIntoView({ block: 'center' }); on.focus({ preventScroll: true }); });
|
||||||
|
}
|
||||||
|
|
||||||
// [ / ] — previous / next chapter. "Previous" restarts the current chapter
|
// [ / ] — previous / next chapter. "Previous" restarts the current chapter
|
||||||
// first when more than 3 s into it, like a CD player.
|
// first when more than 3 s into it, like a CD player.
|
||||||
function jumpChapter(dir) {
|
function jumpChapter(dir) {
|
||||||
@@ -4443,6 +4479,7 @@ const Notes = (() => {
|
|||||||
load, tick, addBookmark, addNote, handleSyncKey, serviceRefresh, refreshFlags: () => { sv.flagsKey = ''; renderServiceButtons(); setPanelFlagMode(sv.panelFlag && flagsEnabled()); paintFlags(); loadFlags(); },
|
load, tick, addBookmark, addNote, handleSyncKey, serviceRefresh, refreshFlags: () => { sv.flagsKey = ''; renderServiceButtons(); setPanelFlagMode(sv.panelFlag && flagsEnabled()); paintFlags(); loadFlags(); },
|
||||||
stageLyrics: { render: renderStage, set: setStageLyrics, on: stageOn },
|
stageLyrics: { render: renderStage, set: setStageLyrics, on: stageOn },
|
||||||
jumpChapter,
|
jumpChapter,
|
||||||
|
openChapterPicker,
|
||||||
chapterAt: (t) => { const c = chapters().slice().sort((a, b) => a.t - b.t); let k = -1; c.forEach((x, i) => { if (x.t <= t) k = i; }); return k < 0 ? null : { n: k + 1, of: c.length, title: c[k].title || '' }; },
|
chapterAt: (t) => { const c = chapters().slice().sort((a, b) => a.t - b.t); let k = -1; c.forEach((x, i) => { if (x.t <= t) k = i; }); return k < 0 ? null : { n: k + 1, of: c.length, title: c[k].title || '' }; },
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
@@ -11176,6 +11213,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(); });
|
||||||
$('updateIconBtn').addEventListener('click', updateFromIcon);
|
$('updateIconBtn').addEventListener('click', updateFromIcon);
|
||||||
|
$('chBadge').addEventListener('click', (e) => { e.stopPropagation(); Notes.openChapterPicker(); });
|
||||||
$('miniBar').addEventListener('click', () => {
|
$('miniBar').addEventListener('click', () => {
|
||||||
setHomeOverPlayer(false);
|
setHomeOverPlayer(false);
|
||||||
hideMiniBar();
|
hideMiniBar();
|
||||||
|
|||||||
@@ -193,7 +193,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span id="durTime" class="time">0:00</span>
|
<span id="durTime" class="time">0:00</span>
|
||||||
<span id="chBadge" class="ch-badge" hidden></span>
|
<button id="chBadge" type="button" class="ch-badge" hidden title="Chapters — tap to jump" aria-haspopup="dialog"></button>
|
||||||
<button id="fsMini" class="fs-mini" type="button" title="Fullscreen" aria-label="Fullscreen"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg></button>
|
<button id="fsMini" class="fs-mini" type="button" title="Fullscreen" aria-label="Fullscreen"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg></button>
|
||||||
</div>
|
</div>
|
||||||
<button id="waveLoud" class="wave-loud hidden" type="button">⚡ Jump to the loudest part</button>
|
<button id="waveLoud" class="wave-loud hidden" type="button">⚡ Jump to the loudest part</button>
|
||||||
|
|||||||
@@ -4314,3 +4314,20 @@ html.ios-nozoom, html.ios-nozoom body { touch-action: manipulation; }
|
|||||||
/* Classic-layout SVG nav icons: `hidden` is not reliably honoured on SVG, so
|
/* Classic-layout SVG nav icons: `hidden` is not reliably honoured on SVG, so
|
||||||
hide them explicitly everywhere; theme-classic.css shows them in Classic. */
|
hide them explicitly everywhere; theme-classic.css shows them in Classic. */
|
||||||
.classic-nav-icon { display: none; }
|
.classic-nav-icon { display: none; }
|
||||||
|
|
||||||
|
/* Chapter picker (tap the chapter badge) */
|
||||||
|
.ch-picker-src { margin: 0 0 8px; font-size: 12.5px; color: var(--text-dim); }
|
||||||
|
.ch-picker-list { display: flex; flex-direction: column; gap: 4px; max-height: min(60vh, 460px); overflow-y: auto; margin: 0 -4px; padding: 2px 4px; }
|
||||||
|
.ch-pick {
|
||||||
|
display: grid; grid-template-columns: 2.2em 1fr auto; align-items: center; gap: 10px;
|
||||||
|
width: 100%; min-height: 44px; padding: 8px 12px; border-radius: 12px; text-align: left; cursor: pointer;
|
||||||
|
border: 1px solid transparent; background: var(--bg-2); color: var(--text); font: 600 14px var(--ui);
|
||||||
|
transition: border-color .15s, background .15s;
|
||||||
|
}
|
||||||
|
.ch-pick:hover { border-color: var(--accent); }
|
||||||
|
.ch-pick:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||||
|
.ch-pick-n { font: 600 12px var(--mono, ui-monospace, monospace); color: var(--text-dim); }
|
||||||
|
.ch-pick-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.ch-pick-t { font: 500 12.5px var(--mono, ui-monospace, monospace); color: var(--text-dim); font-variant-numeric: tabular-nums; }
|
||||||
|
.ch-pick.on { background: color-mix(in srgb, var(--accent) 16%, var(--bg-2)); border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
|
||||||
|
.ch-pick.on .ch-pick-n, .ch-pick.on .ch-pick-t { color: var(--accent-bright); }
|
||||||
|
|||||||
@@ -380,3 +380,8 @@ html[data-layout="glass-stage"] .svc-tile { min-height: 58px; padding: 8px 4px 7
|
|||||||
html[data-layout="glass-stage"] .svc-tile .svc-ico { width: 20px; height: 20px; }
|
html[data-layout="glass-stage"] .svc-tile .svc-ico { width: 20px; height: 20px; }
|
||||||
html[data-layout="glass-stage"] .svc-font { height: 36px; }
|
html[data-layout="glass-stage"] .svc-font { height: 36px; }
|
||||||
html[data-layout="glass-stage"] .svc-font button { width: 28px; height: 30px; }
|
html[data-layout="glass-stage"] .svc-font button { width: 28px; height: 30px; }
|
||||||
|
|
||||||
|
/* Chapter badge opens the chapter picker */
|
||||||
|
html[data-layout="glass-stage"] #controls .ch-badge { cursor: pointer; font-family: var(--mono); transition: background .15s, border-color .15s; }
|
||||||
|
html[data-layout="glass-stage"] #controls .ch-badge:hover { background: rgba(255, 255, 255, 0.12); border-color: var(--accent); }
|
||||||
|
html[data-layout="glass-stage"] #controls .ch-badge:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user