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:
Jonathan Sykes
2026-10-03 07:50:15 +08:00
parent ea9e11d054
commit 5d94acf3f0
4 changed files with 62 additions and 2 deletions

View File

@@ -3273,7 +3273,7 @@ function fitLyricLines(list, { maxShare = 0.17, maxPx = 160, minPx = 18, uniform
const Notes = (() => {
const LC = window.LyricsCore;
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;
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);
}
// 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
// first when more than 3 s into it, like a CD player.
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(); },
stageLyrics: { render: renderStage, set: setStageLyrics, on: stageOn },
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 || '' }; },
};
})();
@@ -11176,6 +11213,7 @@ document.querySelectorAll('.chip').forEach((c) => {
// Mini now-playing bar
$('miniPlayBtn').addEventListener('click', (e) => { e.stopPropagation(); Player.toggle(); });
$('updateIconBtn').addEventListener('click', updateFromIcon);
$('chBadge').addEventListener('click', (e) => { e.stopPropagation(); Notes.openChapterPicker(); });
$('miniBar').addEventListener('click', () => {
setHomeOverPlayer(false);
hideMiniBar();