mirror of
https://github.com/SoPat712/Speeder.git
synced 2026-10-08 19:05:02 -04:00
Fix controller visibility issues and improve Shadow DOM video detection
This commit is contained in:
3 files changed
+148
-15
No files matched your search
@@ -11,11 +11,6 @@
|
|||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Use minimal z-index for non-YouTube sites to avoid overlapping modals */
|
|
||||||
.vsc-controller.vsc-non-youtube {
|
|
||||||
z-index: 1 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vsc-nosource {
|
.vsc-nosource {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ var YT_NATIVE_MIN = 0.25;
|
|||||||
var YT_NATIVE_MAX = 2.0;
|
var YT_NATIVE_MAX = 2.0;
|
||||||
var YT_NATIVE_STEP = 0.05;
|
var YT_NATIVE_STEP = 0.05;
|
||||||
var vscObservedRoots = new WeakSet();
|
var vscObservedRoots = new WeakSet();
|
||||||
|
var vscConnectedScannedRoots = new WeakSet();
|
||||||
var requestIdle =
|
var requestIdle =
|
||||||
typeof window.requestIdleCallback === "function"
|
typeof window.requestIdleCallback === "function"
|
||||||
? window.requestIdleCallback.bind(window)
|
? window.requestIdleCallback.bind(window)
|
||||||
@@ -1039,6 +1040,22 @@ function scanNodeForMedia(node, parent, added) {
|
|||||||
if (node.shadowRoot) {
|
if (node.shadowRoot) {
|
||||||
observeRoot(node.shadowRoot);
|
observeRoot(node.shadowRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deep-scan descendant elements for shadow roots we haven't observed yet.
|
||||||
|
// This catches custom elements (like archive.org's <play-av>) whose shadow
|
||||||
|
// roots were created before our attachShadow patch was installed.
|
||||||
|
if (added && typeof node.querySelectorAll === "function") {
|
||||||
|
try {
|
||||||
|
var allElements = node.querySelectorAll("*");
|
||||||
|
for (var j = 0; j < allElements.length; j++) {
|
||||||
|
if (allElements[j].shadowRoot && !vscObservedRoots.has(allElements[j].shadowRoot)) {
|
||||||
|
observeRoot(allElements[j].shadowRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// querySelectorAll may throw on detached or unusual nodes
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScanNodeForRoot(root) {
|
function getScanNodeForRoot(root) {
|
||||||
@@ -1052,6 +1069,8 @@ function getScanNodeForRoot(root) {
|
|||||||
function rootMayContainMedia(root) {
|
function rootMayContainMedia(root) {
|
||||||
if (!root) return false;
|
if (!root) return false;
|
||||||
if (root.nodeType === Node.DOCUMENT_NODE) return true;
|
if (root.nodeType === Node.DOCUMENT_NODE) return true;
|
||||||
|
// Always scan shadow roots so we can find nested shadow roots or media.
|
||||||
|
if (root.host || (typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot)) return true;
|
||||||
if (typeof root.querySelector !== "function") return true;
|
if (typeof root.querySelector !== "function") return true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1061,6 +1080,7 @@ function rootMayContainMedia(root) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function scanRootForMedia(root) {
|
function scanRootForMedia(root) {
|
||||||
var scanRoot = getScanNodeForRoot(root);
|
var scanRoot = getScanNodeForRoot(root);
|
||||||
if (!scanRoot) return;
|
if (!scanRoot) return;
|
||||||
@@ -1071,13 +1091,30 @@ function scanRootForMedia(root) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function observeRoot(root) {
|
function observeRoot(root) {
|
||||||
if (!root || vscObservedRoots.has(root)) return;
|
if (!root) return;
|
||||||
vscObservedRoots.add(root);
|
|
||||||
setupListener(root);
|
var isConnected = false;
|
||||||
attachMutationObserver(root);
|
try {
|
||||||
attachMediaDetectionListeners(root);
|
isConnected = root.nodeType === Node.DOCUMENT_NODE ||
|
||||||
if (rootMayContainMedia(root)) {
|
root.isConnected ||
|
||||||
scanRootForMedia(root);
|
(root.host && (root.host.isConnected || (root.host.ownerDocument && root.host.ownerDocument.contains(root.host)))) ||
|
||||||
|
(root.ownerDocument && root.ownerDocument.contains(root));
|
||||||
|
} catch (e) {
|
||||||
|
isConnected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vscObservedRoots.has(root)) {
|
||||||
|
vscObservedRoots.add(root);
|
||||||
|
setupListener(root);
|
||||||
|
attachMutationObserver(root);
|
||||||
|
attachMediaDetectionListeners(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConnected && !vscConnectedScannedRoots.has(root)) {
|
||||||
|
vscConnectedScannedRoots.add(root);
|
||||||
|
if (rootMayContainMedia(root)) {
|
||||||
|
scanRootForMedia(root);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1122,6 +1159,12 @@ function log(message, level) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Patch attachShadow immediately — before any async operations — so we
|
||||||
|
// catch shadow roots created while chrome.storage.sync.get is pending.
|
||||||
|
// Sites like archive.org create Lit/LitElement shadow DOMs during page load;
|
||||||
|
// waiting for the storage callback would miss them entirely.
|
||||||
|
patchAttachShadow();
|
||||||
|
|
||||||
chrome.storage.sync.get(tc.settings, function(storage) {
|
chrome.storage.sync.get(tc.settings, function(storage) {
|
||||||
var storedBindings = Array.isArray(storage.keyBindings)
|
var storedBindings = Array.isArray(storage.keyBindings)
|
||||||
? storage.keyBindings
|
? storage.keyBindings
|
||||||
@@ -1227,7 +1270,7 @@ chrome.storage.sync.get(tc.settings, function(storage) {
|
|||||||
chrome.storage.sync.set({ keyBindings: tc.settings.keyBindings });
|
chrome.storage.sync.set({ keyBindings: tc.settings.keyBindings });
|
||||||
}
|
}
|
||||||
captureSiteRuleBase();
|
captureSiteRuleBase();
|
||||||
patchAttachShadow();
|
// patchAttachShadow() is now called at top-level before this callback
|
||||||
// Add a listener for messages from the popup.
|
// Add a listener for messages from the popup.
|
||||||
// We use a global flag to ensure the listener is only attached once.
|
// We use a global flag to ensure the listener is only attached once.
|
||||||
if (!window.vscMessageListener) {
|
if (!window.vscMessageListener) {
|
||||||
@@ -1832,8 +1875,10 @@ function defineVideoController() {
|
|||||||
if (!this.video.src && !this.video.currentSrc)
|
if (!this.video.src && !this.video.currentSrc)
|
||||||
wrapper.classList.add("vsc-nosource");
|
wrapper.classList.add("vsc-nosource");
|
||||||
if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden");
|
if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden");
|
||||||
// Use lower z-index for non-YouTube sites to avoid overlapping modals
|
// z-index is handled by the base .vsc-controller CSS rule (2147483646).
|
||||||
if (!isOnYouTube()) wrapper.classList.add("vsc-non-youtube");
|
// The controller lives inside the video container, so high z-index only
|
||||||
|
// makes it topmost within the local stacking context — it won't overlay
|
||||||
|
// page-level modals or dialogs.
|
||||||
var shadow = wrapper.attachShadow({ mode: "open" });
|
var shadow = wrapper.attachShadow({ mode: "open" });
|
||||||
var shadowStylesheet = doc.createElement("link");
|
var shadowStylesheet = doc.createElement("link");
|
||||||
shadowStylesheet.rel = "stylesheet";
|
shadowStylesheet.rel = "stylesheet";
|
||||||
@@ -2481,6 +2526,24 @@ function initializeNow(doc, forceReinit = false) {
|
|||||||
attachNavigationListeners();
|
attachNavigationListeners();
|
||||||
observeRoot(doc);
|
observeRoot(doc);
|
||||||
|
|
||||||
|
// Delayed rescan to catch custom elements whose shadow roots were created
|
||||||
|
// before our attachShadow patch was installed (e.g. archive.org's <play-av>
|
||||||
|
// Lit component, or any site that lazily creates shadow DOM video players).
|
||||||
|
if (!doc.vscDelayedShadowScanDone) {
|
||||||
|
doc.vscDelayedShadowScanDone = true;
|
||||||
|
setTimeout(function() {
|
||||||
|
if (!doc.body) return;
|
||||||
|
try {
|
||||||
|
var els = doc.body.querySelectorAll("*");
|
||||||
|
for (var i = 0; i < els.length; i++) {
|
||||||
|
if (els[i].shadowRoot && !vscObservedRoots.has(els[i].shadowRoot)) {
|
||||||
|
observeRoot(els[i].shadowRoot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
if (forceReinit) {
|
if (forceReinit) {
|
||||||
log("Force re-initialization requested", 4);
|
log("Force re-initialization requested", 4);
|
||||||
refreshAllControllerGeometry();
|
refreshAllControllerGeometry();
|
||||||
|
|||||||
@@ -160,4 +160,79 @@ describe("inject runtime", () => {
|
|||||||
window.resetSettingsFromSiteRuleBase();
|
window.resetSettingsFromSiteRuleBase();
|
||||||
expect(window.tc.settings.subtitleNudgeEnabledByDefault).toBe(true);
|
expect(window.tc.settings.subtitleNudgeEnabledByDefault).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("detects media inside dynamically added shadow DOMs", async () => {
|
||||||
|
await bootInject();
|
||||||
|
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
expect(window.vscAttachShadowPatched).toBe(true);
|
||||||
|
|
||||||
|
const host = document.createElement("custom-player");
|
||||||
|
const shadow = host.attachShadow({ mode: "open" });
|
||||||
|
const video = document.createElement("video");
|
||||||
|
video.src = "https://example.org/dynamic.mp4";
|
||||||
|
shadow.appendChild(video);
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
// Flush MutationObserver microtasks so that the observer callback runs
|
||||||
|
// and schedules requestIdleCallback's setTimeout.
|
||||||
|
await flushAsyncWork();
|
||||||
|
// Run the scheduled timers (requestIdleCallback)
|
||||||
|
vi.runAllTimers();
|
||||||
|
// Flush any remaining microtasks/promises
|
||||||
|
await flushAsyncWork();
|
||||||
|
|
||||||
|
expect(video.vsc).toBeDefined();
|
||||||
|
expect(video.vsc.div).toBeDefined();
|
||||||
|
expect(video.vsc.div.classList.contains("vsc-non-youtube")).toBe(false);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects media in pre-existing shadow DOMs via delayed rescan", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
loadHtmlString("<!doctype html><html><body></body></html>");
|
||||||
|
|
||||||
|
const host = document.createElement("custom-player");
|
||||||
|
const shadow = host.attachShadow({ mode: "open" });
|
||||||
|
const video = document.createElement("video");
|
||||||
|
video.src = "https://example.org/pre-existing.mp4";
|
||||||
|
shadow.appendChild(video);
|
||||||
|
document.body.appendChild(host);
|
||||||
|
|
||||||
|
globalThis.chrome = createChromeMock({ sync: {}, local: {} });
|
||||||
|
window.chrome = globalThis.chrome;
|
||||||
|
globalThis.chrome.runtime.onMessage = {
|
||||||
|
addListener: vi.fn()
|
||||||
|
};
|
||||||
|
const originalSyncGet = globalThis.chrome.storage.sync.get;
|
||||||
|
const originalLocalGet = globalThis.chrome.storage.local.get;
|
||||||
|
globalThis.chrome.storage.sync.get = vi.fn((keys, callback) => {
|
||||||
|
Promise.resolve().then(() => originalSyncGet(keys, callback));
|
||||||
|
});
|
||||||
|
globalThis.chrome.storage.local.get = vi.fn((keys, callback) => {
|
||||||
|
Promise.resolve().then(() => originalLocalGet(keys, callback));
|
||||||
|
});
|
||||||
|
|
||||||
|
loadScript("shared/controller-utils.js");
|
||||||
|
loadScript("shared/key-bindings.js");
|
||||||
|
loadScript("shared/site-rules.js");
|
||||||
|
loadScript("ui-icons.js");
|
||||||
|
loadScript("inject.js");
|
||||||
|
|
||||||
|
// Fast-forward 3000ms for delayed rescan to trigger
|
||||||
|
vi.advanceTimersByTime(3000);
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i += 1) {
|
||||||
|
await flushAsyncWork();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(video.vsc).toBeDefined();
|
||||||
|
expect(video.vsc.div).toBeDefined();
|
||||||
|
expect(video.vsc.div.classList.contains("vsc-non-youtube")).toBe(false);
|
||||||
|
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Reference in new issue
Block a user