From 9d2a7af87da5b71f737243571d96387fb4c48011 Mon Sep 17 00:00:00 2001 From: PathGao <42336971+PathGao@users.noreply.github.com> Date: Tue, 27 Oct 2026 11:15:49 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=93=E6=B3=A8=E6=A8=A1=E5=BC=8F=EF=BC=9A?= =?UTF-8?q?=E4=BB=8E=E5=B8=A6=20mdg=5Freader=20=E7=9A=84=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E6=89=93=E5=BC=80=E6=97=B6=EF=BC=8C=E7=AD=89=20B=E7=AB=99=20?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E6=8E=A5=E7=AE=A1=E5=AE=8C=E5=86=8D=E6=94=BE?= =?UTF-8?q?=E8=BF=9B=E5=AD=97=E5=B9=95=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 页面还是服务器渲染的那份(#app 上有 data-server-rendered)时就插入字幕栏,B站 这次访问里点分 P 列表、下一个、自动连播都切不了分 P,退出专注模式也不恢复。挂载播放器前等这个标记消失,最多等到导航后 10 秒。顺带修了 content.selftest.js 里一个没还原 currentSite 的测试。 --- extension/content.js | 14 ++++++++++++++ extension/content.selftest.js | 28 +++++++++++++++++++++++++++- extension/sites.js | 2 ++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/extension/content.js b/extension/content.js index 7b6bd93..2f94b8a 100644 --- a/extension/content.js +++ b/extension/content.js @@ -337,6 +337,8 @@ const UNNAMED_SUBTITLE = "未命名字幕"; const READER_FOLLOW_IDLE_MS = 1000; // Between two tries to mount the player after focus mode opened without one. const PLAYER_RETRY_MS = 500; +// How long after navigation focus mode waits for the page to be hydrated (pageHydrating). +const HYDRATE_WAIT_MS = 10000; // Smaller than this is a thumbnail or a preview, not the player. const MIN_PLAYER = { width: 240, height: 120 }; // The appearance settings focus mode changes itself; a change to them does not touch the note. @@ -1621,6 +1623,10 @@ async function ensureReaderPlayerMounted({ retries = 1, delayMs = 100, forceLayo if (!isReaderMode()) { return false; } + if (pageHydrating()) { + await sleep(delayMs); + continue; + } const video = getRuntimeVideoElement(); const playerHost = findReaderPlayerHost(video); if (video && playerHost) { @@ -1706,6 +1712,14 @@ async function ensureReaderPlayerMounted({ retries = 1, delayMs = 100, forceLayo return false; } +// B站 hands its server-rendered page to the client only after load (Vue then drops data-server-rendered). Focus mode +// put into the page before that leaves the page's own part switching dead for the visit (列表、下一个、自动连播), so the +// mount waits; past HYDRATE_WAIT_MS it goes ahead, in case a page keeps the marker. +function pageHydrating() { + const marks = reader().hydrating || []; + return Boolean(marks.length && document.querySelector(sel(marks))) && performance.now() < HYDRATE_WAIT_MS; +} + function queueEnsureReaderPlayerMounted() { if (!state.readingViewOpen || state.readingPlayerMountTimer) { return; diff --git a/extension/content.selftest.js b/extension/content.selftest.js index 6b3b32e..02267f7 100644 --- a/extension/content.selftest.js +++ b/extension/content.selftest.js @@ -172,6 +172,7 @@ const settle = async () => { state.subtitleBody = []; state.subtitles = [{ id: "1", url: "u" }]; const body = gate("raw"); + const site = t.currentSite; t.currentSite = () => ({ fetchRaw: () => body.promise, parseSegments: () => [{ from: 0, to: 1, content: "旧视频" }] }); const switching = t.switchSubtitle("u"); await settle(); @@ -179,6 +180,7 @@ const settle = async () => { body.open(); await assert.rejects(switching, (e) => e.message === "视频已切换,请刷新网页后重试"); assert.deepStrictEqual(state.subtitleBody, [], "the new video keeps an empty body"); + t.currentSite = site; } // G4: the note's AI turns and triage extras read for the old video are dropped once it changed. @@ -759,6 +761,31 @@ const settle = async () => { Object.assign(t, keep); } + // V8: on a B站 page not hydrated yet (data-server-rendered still there) the mount puts nothing into the page: that + // left B站's own part switching dead. Past HYDRATE_WAIT_MS after navigation it mounts anyway. + { + const names = ["getRuntimeVideoElement", "findReaderPlayerHost", "dismissReaderMiniPlayer", "bindReadingViewVideo", "moveReadingMainInline", "normalizeReaderPlayerContainer", "clearNativeReaderFloatingStyles", "hasNativeReaderPlayerLayoutIssue", "bindReaderPlayerControlsHover", "bindReaderLayout", "layoutReaderPlayerHost", "cleanupReaderPlayerHostNode", "setReaderPlayerControlsVisible", "ensureReaderPlayerControlsRecovered", "queueEnsureReaderPlayerControlsRecovered"]; + const keep = Object.fromEntries(names.map((k) => [k, t[k]])); + const [query, perf] = [t.document.querySelector, t.performance]; + t.document.querySelector = (s) => (s === "[data-server-rendered]" ? {} : null); + for (const [name, now, mounted, moved] of [["hydrating", 3000, false, 0], ["marker kept past the wait", 10001, true, 1]]) { + for (const f of names) t[f] = () => {}; + let moves = 0; + t.performance = { ...perf, now: () => now }; + t.getRuntimeVideoElement = () => ({ removeAttribute() {}, setAttribute() {}, closest: () => null }); + t.findReaderPlayerHost = () => ({ classList: { add() {} } }); + t.hasNativeReaderPlayerLayoutIssue = () => false; + t.moveReadingMainInline = () => moves++; + t.stepReader("mark"); + t.stepReader("enter"); + assert.deepStrictEqual([await real.ensureReaderPlayerMounted({ retries: 2, delayMs: 1 }), moves], [mounted, moved], name); + t.stepReader("close"); + } + Object.assign(t, keep, { performance: perf }); + t.document.querySelector = query; + state.readingPlayerHost = null; + } + // 跟随已停 shows after a manual scroll only while playing. Following stays off while the reader is away from the // playing line, however long; it resumes once the line is back in view and the reader has let go, or at 回到当前. { @@ -1523,7 +1550,6 @@ const settle = async () => { const was = { href: t.location.href, hostname: t.location.hostname, ...Object.fromEntries(["site", "videoId", "videoDuration", "subtitles"].map((k) => [k, state[k]])) }; t.location.href = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; t.location.hostname = "www.youtube.com"; - t.currentSite = () => t.MdgSites.matchSite(t.location.href); // the real one (a test above stubs it) Object.assign(state, { site: "youtube", videoId: "dQw4w9WgXcQ", videoDuration: 0 }); const url = "https://www.youtube.com/api/timedtext?v=dQw4w9WgXcQ&lang=en"; state.subtitles = [{ id: "en", lang: "en", url }]; diff --git a/extension/sites.js b/extension/sites.js index 4f3a235..5efe8ef 100644 --- a/extension/sites.js +++ b/extension/sites.js @@ -553,6 +553,8 @@ playerLayout: [".bpx-player-container", ".bpx-docker", ".bpx-player-video-area", ".bpx-player-primary-area", ".bpx-player-inner", ".scroll-sticky", "#bilibili-player", "#playerWrap", ".player-wrap"], // /video/ and /list/, 番剧 (#bilibili-player-wrap), 活动页 (.festival-video-player). playerWrap: ["#playerWrap", ".player-wrap", "#bilibili-player-wrap", ".festival-video-player"], + // The server-rendered page until the client takes it over (content.js pageHydrating). + hydrating: ["[data-server-rendered]"], miniPlayer: [".bpx-player-mini-warp", ".bpx-player-mini-close"], miniClose: [".bpx-player-mini-close"], endingPanel: [".bpx-player-ending-panel", ".bpx-player-ending-related"],