Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions extension/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
28 changes: 27 additions & 1 deletion extension/content.selftest.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,15 @@ 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();
t.clipRuns.next();
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.
Expand Down Expand Up @@ -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 回到当前.
{
Expand Down Expand Up @@ -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 }];
Expand Down
2 changes: 2 additions & 0 deletions extension/sites.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
Loading