Release RedGifs Video Downloader 0.6.23
This commit is contained in:
1 parent
9d6149e6bf
commit
be01fbea9e
7 files changed
+640
-107
No files matched your search
+518
-90
@@ -2,6 +2,7 @@
|
||||
|
||||
if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
globalThis.__localRedgifsScannerInstalled = true;
|
||||
const CONTENT_SCRIPT_VERSION = "0.6.23";
|
||||
|
||||
function isRedgifsHost(hostname) {
|
||||
const host = String(hostname || "").toLowerCase();
|
||||
@@ -333,14 +334,39 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
const externalButtons = new Set();
|
||||
const checkedMediaIds = new Set();
|
||||
const pendingHistoryIds = new Set();
|
||||
const MEDIA_CANDIDATE_SELECTOR = [
|
||||
"[data-feed-item-id]",
|
||||
"a[href*='/watch/']",
|
||||
"a[href*='/ifr/']",
|
||||
"video",
|
||||
"img[src*='media.redgifs.com']",
|
||||
"img[data-src*='media.redgifs.com']",
|
||||
"img[data-lazy-src*='media.redgifs.com']"
|
||||
].join(", ");
|
||||
const ANCHOR_RECONNECT_GRACE_MS = 500;
|
||||
let historyCheckTimer = null;
|
||||
let externalLayoutFrame = null;
|
||||
let externalReconnectTimer = null;
|
||||
let lastScanDiagnostics = [];
|
||||
const contentDiagnosticEvents = [];
|
||||
let diagnosticsEnabled = false;
|
||||
|
||||
function applyDiagnosticsState(enabled) {
|
||||
diagnosticsEnabled = Boolean(enabled);
|
||||
if (diagnosticsEnabled) return;
|
||||
contentDiagnosticEvents.length = 0;
|
||||
lastScanDiagnostics = [];
|
||||
const buttons = new Set(externalButtons);
|
||||
for (const buttonsForMedia of mediaButtons.values()) {
|
||||
for (const button of buttonsForMedia) buttons.add(button);
|
||||
}
|
||||
for (const button of buttons) {
|
||||
if (button) button.__localRgDecisionHistory = [];
|
||||
}
|
||||
}
|
||||
|
||||
function recordContentDiagnostic(level, event, details) {
|
||||
if (!diagnosticsEnabled) return null;
|
||||
const entry = {
|
||||
at: new Date().toISOString(),
|
||||
level: level || "info",
|
||||
@@ -379,6 +405,11 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
function setButtonLayoutReason(button, code, details) {
|
||||
if (!button) return;
|
||||
const previous = button.__localRgLayoutReason || null;
|
||||
if (!diagnosticsEnabled) {
|
||||
button.__localRgLayoutReason = { code };
|
||||
button.__localRgDecisionHistory = [];
|
||||
return;
|
||||
}
|
||||
const next = Object.assign({
|
||||
code,
|
||||
at: new Date().toISOString()
|
||||
@@ -393,9 +424,7 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
decision: next,
|
||||
previousCode: previous && previous.code || null,
|
||||
inlineDisplay: button.style && button.style.display || null,
|
||||
buttonRect: diagnosticRect(button),
|
||||
anchorRect: diagnosticRect(anchor),
|
||||
rootRect: diagnosticRect(root),
|
||||
anchorConnected: !anchor || !("isConnected" in anchor) || Boolean(anchor.isConnected),
|
||||
rootConnected: !root || !("isConnected" in root) || Boolean(root.isConnected),
|
||||
hostScroll: host ? {
|
||||
@@ -403,7 +432,9 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
left: Number(host.scrollLeft) || 0
|
||||
} : null
|
||||
});
|
||||
if (button.__localRgDecisionHistory.length > 40) button.__localRgDecisionHistory.splice(0, 10);
|
||||
if (button.__localRgDecisionHistory.length > 12) {
|
||||
button.__localRgDecisionHistory.splice(0, button.__localRgDecisionHistory.length - 12);
|
||||
}
|
||||
}
|
||||
|
||||
function isGeneralFeedPage() {
|
||||
@@ -661,8 +692,7 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
setButtonLayoutReason(button, "root-reconnecting");
|
||||
continue;
|
||||
}
|
||||
setStyleProperty(button, "display", "none");
|
||||
setButtonLayoutReason(button, "root-disconnected");
|
||||
disposeButton(button, "root-disconnected");
|
||||
continue;
|
||||
}
|
||||
const anchor = button.__localRgAnchor || root;
|
||||
@@ -679,6 +709,9 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
let fallbackReason = null;
|
||||
if (rect && usableLayoutRect(rootRect)) {
|
||||
rememberAnchorGeometry(button, rect, rootRect);
|
||||
} else if (overlay && usableLayoutRect(rootRect)) {
|
||||
rect = rootRect;
|
||||
fallbackReason = "visible-from-modal-root";
|
||||
} else {
|
||||
rect = anchorRectFromRoot(button, rootRect);
|
||||
fallbackReason = !anchorConnected
|
||||
@@ -788,7 +821,6 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
|
||||
function scheduleFeedRefresh() {
|
||||
scheduleExternalLayout();
|
||||
scheduleDecoration();
|
||||
}
|
||||
|
||||
function configureButtonLayout(root, button, anchor) {
|
||||
@@ -991,6 +1023,13 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
topBoundary: pageTopBoundary(Number(globalThis.innerWidth) || 0, Number(globalThis.innerHeight) || 0),
|
||||
fullscreenElement: diagnosticElement(document.fullscreenElement)
|
||||
},
|
||||
scanner: {
|
||||
diagnosticsEnabled,
|
||||
trackedButtons: buttons.length,
|
||||
trackedMediaIds: mediaButtons.size,
|
||||
externalButtons: externalButtons.size,
|
||||
pendingDecorationRoots: pendingDecorationRoots.size
|
||||
},
|
||||
legend: {
|
||||
cyan: "anchor (video/poster)",
|
||||
yellow: "card root",
|
||||
@@ -1018,28 +1057,26 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
const key = normalizeMediaKey(item && item.id);
|
||||
const buttons = key && mediaButtons.get(key);
|
||||
if (!buttons) return null;
|
||||
for (const button of buttons) {
|
||||
if (!button) continue;
|
||||
for (const button of Array.from(buttons)) {
|
||||
if (!button) {
|
||||
buttons.delete(button);
|
||||
continue;
|
||||
}
|
||||
const previousRoot = button.__localRgRoot;
|
||||
const previousAnchor = button.__localRgAnchor || previousRoot;
|
||||
const previousOverlay = button.__localRgOverlay || modalContainerFor(previousAnchor) || modalContainerFor(previousRoot);
|
||||
const nextOverlay = modalContainerFor(anchor || root) || modalContainerFor(root);
|
||||
if (!previousRoot) {
|
||||
disposeButton(button, "reusable-without-root");
|
||||
continue;
|
||||
}
|
||||
const sameRoot = previousRoot === root;
|
||||
const previousConnected = previousRoot
|
||||
&& (!("isConnected" in previousRoot) || previousRoot.isConnected);
|
||||
const previousVisible = Math.max(
|
||||
visibleAreaFor(previousRoot),
|
||||
visibleAreaFor(previousAnchor)
|
||||
) > 0;
|
||||
const nextVisible = Math.max(
|
||||
visibleAreaFor(root),
|
||||
visibleAreaFor(anchor || root)
|
||||
) > 0;
|
||||
const adopt = sameRoot
|
||||
|| (!previousOverlay && Boolean(nextOverlay))
|
||||
|| (!previousConnected && nextVisible)
|
||||
|| (!previousVisible && nextVisible);
|
||||
if (!adopt) return { button, adopt: false };
|
||||
|| (!previousConnected && nextVisible);
|
||||
if (!adopt) continue;
|
||||
if (previousRoot && previousRoot !== root && previousRoot.__localRgDownloadButton === button) {
|
||||
previousRoot.__localRgDownloadButton = null;
|
||||
}
|
||||
@@ -1133,6 +1170,64 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
}
|
||||
}
|
||||
|
||||
function disposeButton(button, reason) {
|
||||
if (!button) return;
|
||||
const root = button.__localRgRoot;
|
||||
const mediaKey = button.__localRgMediaKey;
|
||||
externalButtons.delete(button);
|
||||
if (mediaKey) {
|
||||
const buttons = mediaButtons.get(mediaKey);
|
||||
if (buttons) {
|
||||
buttons.delete(button);
|
||||
if (!buttons.size) mediaButtons.delete(mediaKey);
|
||||
}
|
||||
}
|
||||
if (root && root.__localRgDownloadButton === button) {
|
||||
root.__localRgDownloadButton = null;
|
||||
}
|
||||
if (!button.__localRgJobId && button.__localRgResetTimer) {
|
||||
clearTimeout(button.__localRgResetTimer);
|
||||
button.__localRgResetTimer = null;
|
||||
}
|
||||
if (button.remove) button.remove();
|
||||
button.__localRgRoot = null;
|
||||
button.__localRgAnchor = null;
|
||||
button.__localRgOverlay = null;
|
||||
button.__localRgScrollHost = null;
|
||||
button.__localRgAnchorGeometry = null;
|
||||
button.__localRgAnchorMissingSince = 0;
|
||||
button.__localRgMediaKey = null;
|
||||
button.__localRgDownloadedRecord = null;
|
||||
button.__localRedgifsItem = null;
|
||||
button.__localRgLayoutReason = {
|
||||
code: reason || "disposed",
|
||||
at: new Date().toISOString()
|
||||
};
|
||||
button.__localRgDecisionHistory = [];
|
||||
}
|
||||
|
||||
function pruneDetachedButtons() {
|
||||
for (const buttons of Array.from(mediaButtons.values())) {
|
||||
for (const button of Array.from(buttons)) {
|
||||
if (externalButtons.has(button)) continue;
|
||||
const root = button && button.__localRgRoot;
|
||||
if (!root || ("isConnected" in root && !root.isConnected)) {
|
||||
disposeButton(button, "pruned-disconnected-root");
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const button of Array.from(externalButtons)) {
|
||||
if (!button) {
|
||||
externalButtons.delete(button);
|
||||
continue;
|
||||
}
|
||||
const root = button.__localRgRoot;
|
||||
if (!root || ("isConnected" in root && !root.isConnected)) {
|
||||
scheduleExternalLayout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function animateToastEntrance(root) {
|
||||
if (!root) return;
|
||||
if (typeof root.animate === "function") {
|
||||
@@ -1387,6 +1482,7 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "local-rg-download-button";
|
||||
button.dataset.runtimeVersion = CONTENT_SCRIPT_VERSION;
|
||||
root.__localRgDownloadButton = button;
|
||||
button.__localRedgifsItem = item;
|
||||
button.title = "Скачать видео в лучшем доступном качестве";
|
||||
@@ -1468,62 +1564,111 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
registerMediaButton(button, item);
|
||||
}
|
||||
|
||||
function extractCandidate(node) {
|
||||
if (!node) return null;
|
||||
const root = closestCard(node);
|
||||
if (!root || isPageChromeRoot(root)) return null;
|
||||
const feedNode = (root.matches && root.matches("[data-feed-item-id]"))
|
||||
? root
|
||||
: root.querySelector && root.querySelector("[data-feed-item-id]");
|
||||
const feedId = feedNode && feedNode.getAttribute("data-feed-item-id");
|
||||
const link = (root.matches && root.matches("a[href*='/watch/'], a[href*='/ifr/']"))
|
||||
? root
|
||||
: root.querySelector && root.querySelector("a[href*='/watch/'], a[href*='/ifr/']");
|
||||
const href = normalizeUrl(link && (link.getAttribute("href") || link.href));
|
||||
const video = (root.tagName === "VIDEO") ? root : root.querySelector && root.querySelector("video");
|
||||
const previewImage = mediaPosterImage(root);
|
||||
const directVideoUrl = videoSource(video);
|
||||
const thumbnail = posterFromRoot(root, video);
|
||||
const posterId = idFromPosterUrl(thumbnail);
|
||||
if (!feedId && !href && posterId && !isExplicitMediaCard(root)) return null;
|
||||
const stableId = feedId || idFromUrl(href) || posterId;
|
||||
if (isGeneralFeedPage() && !stableId) return null;
|
||||
const id = stableId || idFromUrl(directVideoUrl);
|
||||
if (!id && !directVideoUrl) return null;
|
||||
const item = {
|
||||
id: id || null,
|
||||
watchUrl: href || (id ? "https://www.redgifs.com/watch/" + encodeURIComponent(id) : null),
|
||||
directVideoUrl: directVideoUrl || null,
|
||||
thumbnail,
|
||||
title: titleFromRoot(root, id),
|
||||
profileSlug: profileSlugFromRoot(root) || currentProfileSlug()
|
||||
};
|
||||
return {
|
||||
root,
|
||||
item,
|
||||
mediaAnchor: video || previewImage,
|
||||
key: id ? "id:" + id.toLowerCase() : "url:" + directVideoUrl
|
||||
};
|
||||
}
|
||||
|
||||
function isNearViewport(element, margin) {
|
||||
if (!element || typeof element.getBoundingClientRect !== "function") return false;
|
||||
const rect = element.getBoundingClientRect();
|
||||
const padding = Math.max(0, Number(margin) || 0);
|
||||
const viewportWidth = Number(globalThis.innerWidth) || 0;
|
||||
const viewportHeight = Number(globalThis.innerHeight) || 0;
|
||||
return rect.width > 0
|
||||
&& rect.height > 0
|
||||
&& rect.bottom >= -padding
|
||||
&& rect.top <= viewportHeight + padding
|
||||
&& rect.right >= -padding
|
||||
&& rect.left <= viewportWidth + padding;
|
||||
}
|
||||
|
||||
function lightweightElement(element) {
|
||||
if (!element) return null;
|
||||
return {
|
||||
tag: String(element.tagName || "").toLowerCase() || null,
|
||||
id: element.id || null,
|
||||
className: typeof element.className === "string" ? element.className.slice(0, 240) : null,
|
||||
connected: !("isConnected" in element) || Boolean(element.isConnected)
|
||||
};
|
||||
}
|
||||
|
||||
function decorateCandidate(node) {
|
||||
const extracted = extractCandidate(node);
|
||||
if (!extracted || !extracted.mediaAnchor) return false;
|
||||
ensureDownloadButton(extracted.root, extracted.item, extracted.mediaAnchor);
|
||||
return true;
|
||||
}
|
||||
|
||||
function scanPage() {
|
||||
const items = [];
|
||||
const seen = new Set();
|
||||
const seenItems = new Set();
|
||||
const seenRoots = new Set();
|
||||
lastScanDiagnostics = [];
|
||||
|
||||
function add(node) {
|
||||
if (!node) return;
|
||||
const nodes = Array.from(document.querySelectorAll(MEDIA_CANDIDATE_SELECTOR));
|
||||
for (const node of nodes) {
|
||||
const root = closestCard(node);
|
||||
if (!root || isPageChromeRoot(root)) return;
|
||||
const feedNode = (root.matches && root.matches("[data-feed-item-id]"))
|
||||
? root
|
||||
: root.querySelector && root.querySelector("[data-feed-item-id]");
|
||||
const feedId = feedNode && feedNode.getAttribute("data-feed-item-id");
|
||||
const link = (root.matches && root.matches("a[href*='/watch/'], a[href*='/ifr/']"))
|
||||
? root
|
||||
: root.querySelector && root.querySelector("a[href*='/watch/'], a[href*='/ifr/']");
|
||||
const href = normalizeUrl(link && (link.getAttribute("href") || link.href));
|
||||
const video = (root.tagName === "VIDEO") ? root : root.querySelector && root.querySelector("video");
|
||||
const previewImage = mediaPosterImage(root);
|
||||
const directVideoUrl = videoSource(video);
|
||||
const thumbnail = posterFromRoot(root, video);
|
||||
const posterId = idFromPosterUrl(thumbnail);
|
||||
if (!feedId && !href && posterId && !isExplicitMediaCard(root)) return;
|
||||
const stableId = feedId || idFromUrl(href) || posterId;
|
||||
if (isGeneralFeedPage() && !stableId) return;
|
||||
const id = stableId || idFromUrl(directVideoUrl);
|
||||
if (!id && !directVideoUrl) return;
|
||||
const key = id ? "id:" + id.toLowerCase() : "url:" + directVideoUrl;
|
||||
const duplicate = seen.has(key);
|
||||
if (!duplicate) seen.add(key);
|
||||
const item = {
|
||||
id: id || null,
|
||||
watchUrl: href || (id ? "https://www.redgifs.com/watch/" + encodeURIComponent(id) : null),
|
||||
directVideoUrl: directVideoUrl || null,
|
||||
thumbnail,
|
||||
title: titleFromRoot(root, id),
|
||||
profileSlug: profileSlugFromRoot(root) || currentProfileSlug()
|
||||
};
|
||||
const mediaAnchor = video || previewImage;
|
||||
if (mediaAnchor) ensureDownloadButton(root, item, mediaAnchor);
|
||||
lastScanDiagnostics.push({
|
||||
key,
|
||||
duplicate,
|
||||
outcome: mediaAnchor ? "decorated" : "no-video-or-redgifs-poster",
|
||||
item: { id: item.id, title: item.title, watchUrl: item.watchUrl, thumbnail: item.thumbnail },
|
||||
root: diagnosticElement(root),
|
||||
anchor: diagnosticElement(mediaAnchor)
|
||||
});
|
||||
if (!duplicate && items.length < 240) items.push(item);
|
||||
if (!root || seenRoots.has(root)) continue;
|
||||
seenRoots.add(root);
|
||||
const extracted = extractCandidate(root);
|
||||
if (!extracted) continue;
|
||||
const duplicate = seenItems.has(extracted.key);
|
||||
if (!duplicate) seenItems.add(extracted.key);
|
||||
if (extracted.mediaAnchor && isNearViewport(extracted.mediaAnchor, 900)) {
|
||||
ensureDownloadButton(extracted.root, extracted.item, extracted.mediaAnchor);
|
||||
}
|
||||
if (diagnosticsEnabled && lastScanDiagnostics.length < 80) {
|
||||
lastScanDiagnostics.push({
|
||||
key: extracted.key,
|
||||
duplicate,
|
||||
outcome: extracted.mediaAnchor ? "candidate" : "no-video-or-redgifs-poster",
|
||||
item: {
|
||||
id: extracted.item.id,
|
||||
title: extracted.item.title,
|
||||
watchUrl: extracted.item.watchUrl,
|
||||
thumbnail: extracted.item.thumbnail
|
||||
},
|
||||
root: lightweightElement(extracted.root),
|
||||
anchor: lightweightElement(extracted.mediaAnchor)
|
||||
});
|
||||
}
|
||||
if (!duplicate && items.length < 240) items.push(extracted.item);
|
||||
}
|
||||
|
||||
const nodes = Array.from(document.querySelectorAll(
|
||||
"[data-feed-item-id], a[href*='/watch/'], a[href*='/ifr/'], video, img[src], img[data-src], img[data-lazy-src]"
|
||||
));
|
||||
nodes.sort((left, right) => Number(Boolean(modalContainerFor(right))) - Number(Boolean(modalContainerFor(left))));
|
||||
for (const node of nodes) add(node);
|
||||
pruneDetachedButtons();
|
||||
scheduleExternalLayout();
|
||||
|
||||
return {
|
||||
@@ -1535,6 +1680,15 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (!message) return false;
|
||||
if (message.action === "getContentRuntimeInfo") {
|
||||
sendResponse({
|
||||
ok: true,
|
||||
version: CONTENT_SCRIPT_VERSION,
|
||||
diagnosticsEnabled,
|
||||
buttons: document.querySelectorAll(".local-rg-download-button").length
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (message.action === "downloadProgress") {
|
||||
showDownloadToast(message.job);
|
||||
try {
|
||||
@@ -1545,6 +1699,10 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
return false;
|
||||
}
|
||||
if (message.action === "captureExtensionDiagnostics" || message.action === "captureButtonDiagnostics") {
|
||||
if (!diagnosticsEnabled) {
|
||||
sendResponse({ ok: false, error: "Включите диагностику в настройках расширения." });
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
sendResponse({ ok: true, report: capturePageDiagnostics() });
|
||||
} catch (error) {
|
||||
@@ -1569,36 +1727,306 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
||||
return false;
|
||||
});
|
||||
|
||||
let decorateTimer = null;
|
||||
function scheduleDecoration() {
|
||||
if (decorateTimer != null) return;
|
||||
let ranSynchronously = false;
|
||||
const timer = setTimeout(() => {
|
||||
ranSynchronously = true;
|
||||
decorateTimer = null;
|
||||
try {
|
||||
scanPage();
|
||||
} catch (error) {
|
||||
recordContentDiagnostic("error", "automatic-scan-failed", {
|
||||
name: error && error.name,
|
||||
message: error && error.message
|
||||
});
|
||||
// A later DOM mutation will retry.
|
||||
}
|
||||
}, 160);
|
||||
if (!ranSynchronously) decorateTimer = timer;
|
||||
const observedCandidates = new Set();
|
||||
const intersectingCandidates = new WeakSet();
|
||||
const pendingDecorationRoots = new Set();
|
||||
let decorationTask = null;
|
||||
let documentSweepTimer = null;
|
||||
let documentSweepTask = null;
|
||||
let tagTransitionUntil = 0;
|
||||
|
||||
const TAG_TRANSITION_GUARD_MS = 12000;
|
||||
const MUTATION_BATCH_LIMIT = 64;
|
||||
const MUTATION_NODE_LIMIT = 96;
|
||||
|
||||
function isExtensionOwnedNode(node) {
|
||||
const element = node && node.nodeType === Node.ELEMENT_NODE
|
||||
? node
|
||||
: node && node.parentElement;
|
||||
if (!element || typeof element.closest !== "function") return false;
|
||||
return Boolean(element.closest([
|
||||
"#local-redgifs-download-styles",
|
||||
"#local-rg-toast-stack",
|
||||
".local-rg-download-button"
|
||||
].join(", ")));
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(scheduleDecoration);
|
||||
function requestDecorationFlush() {
|
||||
if (decorationTask != null) return;
|
||||
const run = (deadline) => {
|
||||
decorationTask = null;
|
||||
const startedAt = typeof performance !== "undefined" ? performance.now() : Date.now();
|
||||
let processed = 0;
|
||||
while (pendingDecorationRoots.size && processed < 16) {
|
||||
if (processed > 0 && deadline && typeof deadline.timeRemaining === "function" && deadline.timeRemaining() < 2) break;
|
||||
const root = pendingDecorationRoots.values().next().value;
|
||||
pendingDecorationRoots.delete(root);
|
||||
if (root && (!("isConnected" in root) || root.isConnected)) {
|
||||
try {
|
||||
decorateCandidate(root);
|
||||
} catch (error) {
|
||||
recordContentDiagnostic("error", "automatic-decoration-failed", {
|
||||
name: error && error.name,
|
||||
message: error && error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
processed += 1;
|
||||
const now = typeof performance !== "undefined" ? performance.now() : Date.now();
|
||||
if (now - startedAt >= 9) break;
|
||||
}
|
||||
pruneDetachedButtons();
|
||||
if (pendingDecorationRoots.size) requestDecorationFlush();
|
||||
};
|
||||
if (typeof globalThis.requestIdleCallback === "function") {
|
||||
decorationTask = globalThis.requestIdleCallback(run, { timeout: 240 });
|
||||
} else {
|
||||
decorationTask = setTimeout(() => run(null), 48);
|
||||
}
|
||||
}
|
||||
|
||||
function queueCandidateDecoration(candidate) {
|
||||
if (!candidate || isExtensionOwnedNode(candidate)) return;
|
||||
const root = closestCard(candidate);
|
||||
if (!root || isPageChromeRoot(root)) return;
|
||||
pendingDecorationRoots.add(root);
|
||||
requestDecorationFlush();
|
||||
}
|
||||
|
||||
const candidateObserver = typeof globalThis.IntersectionObserver === "function"
|
||||
? new globalThis.IntersectionObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
intersectingCandidates.add(entry.target);
|
||||
queueCandidateDecoration(entry.target);
|
||||
} else {
|
||||
intersectingCandidates.delete(entry.target);
|
||||
}
|
||||
}
|
||||
}, { root: null, rootMargin: "800px 240px", threshold: 0 })
|
||||
: null;
|
||||
|
||||
function observeCandidate(candidate) {
|
||||
if (!candidate || candidate.nodeType !== Node.ELEMENT_NODE || isExtensionOwnedNode(candidate)) return;
|
||||
if (typeof candidate.matches !== "function" || !candidate.matches(MEDIA_CANDIDATE_SELECTOR)) return;
|
||||
if (observedCandidates.has(candidate)) return;
|
||||
observedCandidates.add(candidate);
|
||||
if (candidateObserver) {
|
||||
candidateObserver.observe(candidate);
|
||||
} else {
|
||||
queueCandidateDecoration(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
function observeCandidateScope(scope) {
|
||||
if (!scope || isExtensionOwnedNode(scope)) return false;
|
||||
let found = false;
|
||||
if (scope.nodeType === Node.ELEMENT_NODE && typeof scope.matches === "function" && scope.matches(MEDIA_CANDIDATE_SELECTOR)) {
|
||||
observeCandidate(scope);
|
||||
found = true;
|
||||
}
|
||||
if (typeof scope.querySelectorAll !== "function") return found;
|
||||
for (const candidate of scope.querySelectorAll(MEDIA_CANDIDATE_SELECTOR)) {
|
||||
observeCandidate(candidate);
|
||||
found = true;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function unobserveCandidateScope(scope) {
|
||||
if (!candidateObserver || !scope || scope.nodeType !== Node.ELEMENT_NODE || isExtensionOwnedNode(scope)) return false;
|
||||
let found = false;
|
||||
if (typeof scope.matches === "function" && scope.matches(MEDIA_CANDIDATE_SELECTOR)) {
|
||||
candidateObserver.unobserve(scope);
|
||||
observedCandidates.delete(scope);
|
||||
intersectingCandidates.delete(scope);
|
||||
found = true;
|
||||
}
|
||||
if (typeof scope.querySelectorAll !== "function") return found;
|
||||
for (const candidate of scope.querySelectorAll(MEDIA_CANDIDATE_SELECTOR)) {
|
||||
candidateObserver.unobserve(candidate);
|
||||
observedCandidates.delete(candidate);
|
||||
intersectingCandidates.delete(candidate);
|
||||
found = true;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
function pruneObservedCandidates() {
|
||||
for (const candidate of Array.from(observedCandidates)) {
|
||||
const connected = !("isConnected" in candidate) || candidate.isConnected;
|
||||
const stillMatches = connected
|
||||
&& typeof candidate.matches === "function"
|
||||
&& candidate.matches(MEDIA_CANDIDATE_SELECTOR);
|
||||
if (stillMatches) continue;
|
||||
if (candidateObserver) candidateObserver.unobserve(candidate);
|
||||
observedCandidates.delete(candidate);
|
||||
intersectingCandidates.delete(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildCandidateRegistry() {
|
||||
pruneObservedCandidates();
|
||||
for (const candidate of document.querySelectorAll(MEDIA_CANDIDATE_SELECTOR)) {
|
||||
observeCandidate(candidate);
|
||||
if (isNearViewport(candidate, 900)) queueCandidateDecoration(candidate);
|
||||
}
|
||||
pruneDetachedButtons();
|
||||
scheduleExternalLayout();
|
||||
}
|
||||
|
||||
function cancelDocumentSweepTask() {
|
||||
if (documentSweepTimer != null) {
|
||||
clearTimeout(documentSweepTimer);
|
||||
documentSweepTimer = null;
|
||||
}
|
||||
if (documentSweepTask == null) return;
|
||||
if (typeof globalThis.cancelIdleCallback === "function") {
|
||||
globalThis.cancelIdleCallback(documentSweepTask);
|
||||
} else {
|
||||
clearTimeout(documentSweepTask);
|
||||
}
|
||||
documentSweepTask = null;
|
||||
}
|
||||
|
||||
function requestDocumentSweep(delay) {
|
||||
cancelDocumentSweepTask();
|
||||
documentSweepTimer = setTimeout(() => {
|
||||
documentSweepTimer = null;
|
||||
const run = () => {
|
||||
documentSweepTask = null;
|
||||
try {
|
||||
rebuildCandidateRegistry();
|
||||
} catch (error) {
|
||||
recordContentDiagnostic("error", "candidate-registry-rebuild-failed", {
|
||||
name: error && error.name,
|
||||
message: error && error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
if (typeof globalThis.requestIdleCallback === "function") {
|
||||
documentSweepTask = globalThis.requestIdleCallback(run, { timeout: 700 });
|
||||
} else {
|
||||
documentSweepTask = setTimeout(run, 0);
|
||||
}
|
||||
}, Math.max(0, Number(delay) || 0));
|
||||
}
|
||||
|
||||
function isTagNavigationControl(target) {
|
||||
if (!target || typeof target.closest !== "function") return false;
|
||||
return Boolean(target.closest([
|
||||
".tagButton",
|
||||
"button[aria-label$=' tag']",
|
||||
"[role='button'][aria-label$=' tag']"
|
||||
].join(", ")));
|
||||
}
|
||||
|
||||
function armTagTransitionGuard(event) {
|
||||
if (!event || !isTagNavigationControl(event.target)) return;
|
||||
tagTransitionUntil = Date.now() + TAG_TRANSITION_GUARD_MS;
|
||||
pendingDecorationRoots.clear();
|
||||
requestDocumentSweep(260);
|
||||
recordContentDiagnostic("info", "tag-transition-guard-armed", {
|
||||
label: event.target && event.target.closest
|
||||
? (event.target.closest("[aria-label]") || {}).ariaLabel || null
|
||||
: null,
|
||||
guardMs: TAG_TRANSITION_GUARD_MS
|
||||
});
|
||||
}
|
||||
|
||||
function isLargeMutationBatch(mutations) {
|
||||
if (!mutations || mutations.length >= MUTATION_BATCH_LIMIT) return true;
|
||||
let changedNodes = 0;
|
||||
for (const mutation of mutations) {
|
||||
changedNodes += (mutation.addedNodes && mutation.addedNodes.length) || 0;
|
||||
changedNodes += (mutation.removedNodes && mutation.removedNodes.length) || 0;
|
||||
if (changedNodes >= MUTATION_NODE_LIMIT) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function refreshKnownButtonContexts() {
|
||||
const buttons = new Set(externalButtons);
|
||||
for (const buttonsForMedia of mediaButtons.values()) {
|
||||
for (const button of buttonsForMedia) buttons.add(button);
|
||||
}
|
||||
for (const button of buttons) {
|
||||
const root = button && button.__localRgRoot;
|
||||
const anchor = button && button.__localRgAnchor;
|
||||
if (!root || ("isConnected" in root && !root.isConnected)) continue;
|
||||
configureButtonLayout(root, button, anchor || root);
|
||||
}
|
||||
pruneDetachedButtons();
|
||||
scheduleExternalLayout();
|
||||
}
|
||||
|
||||
function handlePageMutations(mutations) {
|
||||
if (Date.now() < tagTransitionUntil || isLargeMutationBatch(mutations)) {
|
||||
requestDocumentSweep(260);
|
||||
return;
|
||||
}
|
||||
let pageChanged = false;
|
||||
for (const mutation of mutations) {
|
||||
if (!mutation || isExtensionOwnedNode(mutation.target)) continue;
|
||||
if (mutation.type === "attributes") {
|
||||
const matchesCandidate = mutation.target.matches && mutation.target.matches(MEDIA_CANDIDATE_SELECTOR);
|
||||
if (!matchesCandidate) {
|
||||
if (candidateObserver && observedCandidates.has(mutation.target)) {
|
||||
candidateObserver.unobserve(mutation.target);
|
||||
observedCandidates.delete(mutation.target);
|
||||
intersectingCandidates.delete(mutation.target);
|
||||
pageChanged = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
observeCandidate(mutation.target);
|
||||
if (intersectingCandidates.has(mutation.target)) queueCandidateDecoration(mutation.target);
|
||||
pageChanged = true;
|
||||
continue;
|
||||
}
|
||||
for (const addedNode of mutation.addedNodes || []) {
|
||||
if (!addedNode || addedNode.nodeType !== Node.ELEMENT_NODE || isExtensionOwnedNode(addedNode)) continue;
|
||||
if (observeCandidateScope(addedNode)) pageChanged = true;
|
||||
}
|
||||
for (const removedNode of mutation.removedNodes || []) {
|
||||
if (!removedNode || removedNode.nodeType !== Node.ELEMENT_NODE || isExtensionOwnedNode(removedNode)) continue;
|
||||
if (unobserveCandidateScope(removedNode)) pageChanged = true;
|
||||
}
|
||||
}
|
||||
if (pageChanged) {
|
||||
pruneDetachedButtons();
|
||||
scheduleExternalLayout();
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(handlePageMutations);
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["src", "poster", "data-src", "data-lazy-src", "data-feed-item-id"]
|
||||
attributeFilter: ["src", "poster", "data-src", "data-lazy-src", "data-feed-item-id", "href"]
|
||||
});
|
||||
if (typeof globalThis.addEventListener === "function") {
|
||||
globalThis.addEventListener("click", armTagTransitionGuard, { capture: true, passive: true });
|
||||
globalThis.addEventListener("scroll", scheduleFeedRefresh, { passive: true, capture: true });
|
||||
globalThis.addEventListener("resize", scheduleExternalLayout, { passive: true });
|
||||
globalThis.addEventListener("fullscreenchange", () => {
|
||||
refreshKnownButtonContexts();
|
||||
observeCandidateScope(document.fullscreenElement || document);
|
||||
setTimeout(refreshKnownButtonContexts, 260);
|
||||
});
|
||||
}
|
||||
scheduleDecoration();
|
||||
try {
|
||||
chrome.storage.local.get("diagnosticsEnabled").then((stored) => {
|
||||
applyDiagnosticsState(Boolean(stored && stored.diagnosticsEnabled));
|
||||
}).catch(() => {});
|
||||
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||
if (areaName === "local" && changes.diagnosticsEnabled) {
|
||||
applyDiagnosticsState(Boolean(changes.diagnosticsEnabled.newValue));
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
applyDiagnosticsState(false);
|
||||
}
|
||||
observeCandidateScope(document);
|
||||
}
|
||||
Reference in new issue
Block a user