Files

2033 lines
83 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
if (!globalThis.__localRedgifsScannerInstalled) {
globalThis.__localRedgifsScannerInstalled = true;
const CONTENT_SCRIPT_VERSION = "0.6.23";
function isRedgifsHost(hostname) {
const host = String(hostname || "").toLowerCase();
return host === "redgifs.com" || host.endsWith(".redgifs.com");
}
function normalizeUrl(value) {
if (!value || /^data:|^blob:/i.test(String(value))) return null;
try {
const parsed = new URL(String(value), document.baseURI);
if (parsed.protocol !== "https:" || !isRedgifsHost(parsed.hostname)) return null;
return parsed.toString();
} catch {
return null;
}
}
function idFromUrl(value) {
const normalized = normalizeUrl(value);
if (!normalized) return null;
try {
const parsed = new URL(normalized);
const pageMatch = parsed.pathname.match(/\/(?:watch|ifr)\/([a-z0-9_-]+)/i);
if (pageMatch && pageMatch[1]) return pageMatch[1];
const filename = (parsed.pathname.split("/").pop() || "")
.replace(/\.(?:mp4|webm|m3u8|m4s|gif|jpe?g|png)$/i, "")
.replace(/-(?:mobile|poster|thumbnail|thumb|sd|hd)$/i, "");
return filename && /^[a-z0-9_-]+$/i.test(filename) ? filename : null;
} catch {
return null;
}
}
function idFromPosterUrl(value) {
const normalized = normalizeUrl(value);
if (!normalized) return null;
try {
const parsed = new URL(normalized);
if (parsed.hostname.toLowerCase() !== "media.redgifs.com") return null;
const filename = parsed.pathname.split("/").pop() || "";
const match = filename.match(/^([a-z0-9_-]+)-(?:poster|thumbnail|thumb|mobile)\.(?:avif|webp|jpe?g|png)$/i);
return match && match[1] ? match[1] : null;
} catch {
return null;
}
}
function mediaPosterImage(root) {
if (!root) return null;
const images = [];
if (root.tagName === "IMG") images.push(root);
images.push.apply(images, Array.from(root.querySelectorAll
? root.querySelectorAll("img[src], img[data-src], img[data-lazy-src]")
: []));
for (const image of images) {
const candidates = [
image.currentSrc,
image.getAttribute && image.getAttribute("src"),
image.getAttribute && image.getAttribute("data-src"),
image.getAttribute && image.getAttribute("data-lazy-src")
];
if (candidates.some((candidate) => Boolean(idFromPosterUrl(candidate)))) return image;
}
return null;
}
function cleanText(value, fallback) {
const cleaned = String(value || "")
.replace(/^Poster for\s+/i, "")
.replace(/\s+(?:uploaded|posted)\s+by\s+.+$/i, "")
.replace(/\s+/g, " ")
.replace(/\b(?:like|share|sound|more|follow|views?)\b/gi, " ")
.replace(/\s+/g, " ")
.trim();
return (cleaned || fallback || "RedGifs video").slice(0, 160);
}
function videoSource(video) {
if (!video) return null;
const candidates = [
video.currentSrc,
video.src,
video.getAttribute("src"),
video.querySelector("source[src]") && video.querySelector("source[src]").getAttribute("src")
];
for (const candidate of candidates) {
const normalized = normalizeUrl(candidate);
if (normalized && !/\.m4s(?:$|\?)/i.test(normalized)) return normalized;
}
return null;
}
function posterFromRoot(root, video) {
const candidates = [];
if (video) {
candidates.push(video.poster, video.getAttribute("poster"));
}
const images = [];
if (root && root.tagName === "IMG") images.push(root);
images.push.apply(images, Array.from(root.querySelectorAll ? root.querySelectorAll("img[src], img[data-src], img[data-lazy-src]") : []));
images.sort((left, right) => Number(Boolean(idFromPosterUrl(
right.currentSrc || right.getAttribute("src") || right.getAttribute("data-src") || right.getAttribute("data-lazy-src")
))) - Number(Boolean(idFromPosterUrl(
left.currentSrc || left.getAttribute("src") || left.getAttribute("data-src") || left.getAttribute("data-lazy-src")
))));
for (const image of images.slice(0, 6)) {
candidates.push(
image.getAttribute("src"),
image.getAttribute("data-src"),
image.getAttribute("data-lazy-src")
);
}
for (const candidate of candidates) {
const normalized = normalizeUrl(candidate);
if (normalized) return normalized;
}
return null;
}
function titleFromRoot(root, id) {
const selectors = [
".descriptionText",
".description",
"[data-testid*='caption']",
"[data-testid*='description']",
"figcaption",
".userName",
".username"
];
const candidates = [];
for (const selector of selectors) {
const node = root.querySelector && root.querySelector(selector);
if (node && node.textContent) candidates.push(node.textContent);
}
candidates.push(
root.getAttribute && root.getAttribute("aria-label"),
root.getAttribute && root.getAttribute("title")
);
const image = root.querySelector && root.querySelector("img[alt]");
if (image) candidates.push(String(image.getAttribute("alt") || ""));
for (const candidate of candidates) {
const cleaned = cleanText(candidate, "");
if (cleaned && cleaned !== "RedGifs video" && cleaned.length > 2) return cleaned;
}
return id ? "RedGifs · " + id : "RedGifs video";
}
function currentProfileSlug() {
const match = String(location.pathname || "").match(/\/users\/([^/?#]+)/i);
if (!match || !match[1]) return null;
try {
return decodeURIComponent(match[1]);
} catch {
return match[1];
}
}
function normalizeProfileSlug(value) {
let slug = String(value || "").trim().replace(/^@+/, "");
try {
slug = decodeURIComponent(slug);
} catch {
// Keep the original visible username.
}
return /^[a-z0-9._-]{1,80}$/i.test(slug) ? slug : null;
}
function profileSlugFromRoot(root) {
if (!root) return null;
const profileLinks = Array.from(root.querySelectorAll
? root.querySelectorAll("a[href*='/users/']")
: []);
for (const link of profileLinks) {
const href = normalizeUrl(link.getAttribute && (link.getAttribute("href") || link.href));
if (!href) continue;
try {
const match = new URL(href).pathname.match(/\/users\/([^/?#]+)/i);
const slug = normalizeProfileSlug(match && match[1]);
if (slug) return slug;
} catch {
// Try the next profile hint.
}
}
const attributeNames = ["data-username", "data-user-name", "data-uploader", "data-owner"];
for (const name of attributeNames) {
const slug = normalizeProfileSlug(root.getAttribute && root.getAttribute(name));
if (slug) return slug;
}
const selectors = [
".userName",
".username",
"[data-testid='username']",
"[data-testid*='user-name']",
"[data-testid*='username']"
];
for (const selector of selectors) {
const node = root.querySelector && root.querySelector(selector);
const slug = normalizeProfileSlug(node && node.textContent);
if (slug) return slug;
}
return null;
}
function closestCard(node) {
if (!node || !node.closest) return node;
return node.closest("[data-feed-item-id], .GifPreview, .tileItem, article, figure")
|| node.closest("a[href*='/watch/'], a[href*='/ifr/']")
|| (node.tagName === "VIDEO" && node.parentElement ? node.parentElement : node);
}
function isExplicitMediaCard(root) {
if (!root || typeof root.matches !== "function") return false;
return root.matches([
"[data-feed-item-id]",
".GifPreview",
".tileItem",
"[data-testid*='feed-item']",
"[data-testid*='gif-preview']",
"[class*='GifPreview']",
"[class*='tileItem']"
].join(", "));
}
function ensureButtonStyles() {
if (document.getElementById("local-redgifs-download-styles")) return;
const style = document.createElement("style");
style.id = "local-redgifs-download-styles";
style.textContent = [
".local-rg-download-host{position:relative!important}",
".local-rg-scroll-host{position:relative!important}",
".local-rg-download-button{all:initial!important;box-sizing:border-box!important;position:absolute!important;right:10px!important;bottom:10px!important;left:auto!important;z-index:2147483646!important;display:flex!important;align-items:center!important;justify-content:center!important;width:42px!important;height:42px!important;border:1px solid rgba(255,255,255,.25)!important;border-radius:50%!important;background:linear-gradient(135deg,#f04468,#d72d57)!important;color:#fff!important;box-shadow:0 8px 24px rgba(0,0,0,.42)!important;font:900 23px/1 system-ui,-apple-system,'Segoe UI',sans-serif!important;cursor:pointer!important;opacity:.96!important;transform:translateY(0)!important;transition:transform .15s ease,filter .15s ease,opacity .15s ease!important;user-select:none!important;-webkit-user-select:none!important}",
".local-rg-download-button:hover{filter:brightness(1.12)!important;transform:translateY(-1px)!important;opacity:1!important}",
".local-rg-download-button:active{transform:translateY(1px)!important}",
".local-rg-download-button:disabled{cursor:wait!important;filter:saturate(.55)!important;opacity:.88!important}",
".local-rg-download-button[data-state='working']{animation:localRgButtonWorking 1.05s ease-in-out infinite!important}",
".local-rg-download-button[data-state='done'],.local-rg-download-button[data-state='downloaded']{background:#159665!important}",
".local-rg-download-button[data-state='downloaded']{border-color:rgba(185,255,225,.72)!important;box-shadow:0 8px 24px rgba(0,0,0,.42),0 0 0 2px rgba(32,184,121,.22)!important}",
".local-rg-download-button[data-state='downloaded']:hover{background:linear-gradient(135deg,#ff9c45,#e86b21)!important;border-color:rgba(255,224,184,.82)!important;box-shadow:0 10px 28px rgba(0,0,0,.44),0 0 0 3px rgba(244,130,42,.25)!important}",
".local-rg-download-button[data-state='error']{background:#9e2941!important}",
".local-rg-button-glyph{all:initial!important;box-sizing:border-box!important;position:absolute!important;inset:0!important;display:grid!important;place-items:center!important;color:inherit!important;pointer-events:none!important;transition:opacity .18s ease,transform .24s cubic-bezier(.2,.8,.2,1)!important}",
".local-rg-button-glyph svg{display:block!important;width:25px!important;height:25px!important;overflow:visible!important;color:inherit!important;fill:none!important;stroke:currentColor!important;stroke-width:2.35!important;stroke-linecap:round!important;stroke-linejoin:round!important}",
".local-rg-icon-redownload{opacity:0!important;transform:rotate(-75deg) scale(.62)!important}",
".local-rg-download-button[data-state='downloaded']:hover .local-rg-icon-check{opacity:0!important;transform:rotate(35deg) scale(.62)!important}",
".local-rg-download-button[data-state='downloaded']:hover .local-rg-icon-redownload{opacity:1!important;transform:rotate(0) scale(1)!important}",
".local-rg-download-button[data-layout='feed-external']{position:absolute!important;right:auto!important;bottom:auto!important;left:var(--local-rg-left,8px)!important;top:var(--local-rg-top,8px)!important;z-index:2147483645!important;display:none!important}",
".local-rg-download-button[data-layout='modal-external']{position:fixed!important;right:auto!important;bottom:auto!important;left:var(--local-rg-left,8px)!important;top:var(--local-rg-top,8px)!important;z-index:2147483646!important;display:none!important;width:38px!important;height:38px!important;box-shadow:0 6px 18px rgba(0,0,0,.36)!important}",
".local-rg-download-button[data-layout='modal-external'] .local-rg-button-glyph svg{width:22px!important;height:22px!important}",
".local-rg-diagnostic-anchor{outline:3px solid #31d7ff!important;outline-offset:-3px!important}",
".local-rg-diagnostic-root{outline:3px solid #ffd43b!important;outline-offset:-6px!important}",
".local-rg-diagnostic-host{outline:3px dashed #a66cff!important;outline-offset:-9px!important}",
".local-rg-diagnostic-button{outline:3px solid #ff7a33!important;outline-offset:3px!important}",
".local-rg-toast-stack{all:initial!important;position:fixed!important;top:18px!important;left:50%!important;z-index:2147483647!important;display:flex!important;width:min(440px,calc(100vw - 28px))!important;flex-direction:column!important;gap:9px!important;transform:translateX(-50%)!important;pointer-events:none!important}",
".local-rg-toast{all:initial;box-sizing:border-box!important;display:block!important;overflow:hidden!important;border:1px solid rgba(255,255,255,.14)!important;border-radius:14px!important;background:rgba(12,16,23,.96)!important;color:#f5f7fb!important;box-shadow:0 18px 55px rgba(0,0,0,.48)!important;font-family:system-ui,-apple-system,'Segoe UI',sans-serif!important;backdrop-filter:blur(12px)!important;pointer-events:none!important;will-change:transform,opacity,filter!important;transform-origin:50% 0!important}",
".local-rg-toast.is-entering{animation:localRgToastIn .46s cubic-bezier(.16,1,.3,1)!important}",
".local-rg-toast.is-leaving{animation:localRgToastOut .3s cubic-bezier(.55,0,1,.45) forwards!important}",
".local-rg-toast-main{display:flex!important;align-items:center!important;gap:11px!important;padding:12px 13px 10px!important}",
".local-rg-toast-icon{display:grid!important;width:34px!important;height:34px!important;flex:0 0 34px!important;place-items:center!important;border-radius:10px!important;background:#f04468!important;color:#fff!important;font:900 18px/1 system-ui!important}",
".local-rg-toast-copy{display:block!important;min-width:0!important;flex:1!important}",
".local-rg-toast-title,.local-rg-toast-status{display:block!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important}",
".local-rg-toast-title{color:#f4f7fb!important;font:800 12px/1.3 system-ui!important}",
".local-rg-toast-status{margin-top:4px!important;color:#95a1b5!important;font:500 11px/1.3 system-ui!important}",
".local-rg-toast-value{color:#cbd3df!important;font:800 11px/1 system-ui!important}",
".local-rg-toast-track{height:5px!important;overflow:hidden!important;background:#242d3c!important}",
".local-rg-toast-bar{height:100%!important;width:0!important;background:linear-gradient(90deg,#159665,#35d596)!important;box-shadow:0 0 10px rgba(53,213,150,.38)!important;transition:width .18s ease!important}",
".local-rg-toast[data-state='complete'] .local-rg-toast-bar{transition:width .25s linear!important}",
".local-rg-toast[data-state='complete'] .local-rg-toast-icon,.local-rg-toast[data-state='complete'] .local-rg-toast-bar{background:#20b879!important}",
".local-rg-toast[data-state='error'] .local-rg-toast-icon,.local-rg-toast[data-state='error'] .local-rg-toast-bar{background:#d53c5d!important}",
"@keyframes localRgButtonWorking{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-2px) scale(1.06)}}",
"@keyframes localRgToastIn{0%{opacity:0;filter:blur(7px);transform:translateY(-20px) scale(.94)}65%{opacity:1;filter:blur(0);transform:translateY(2px) scale(1.01)}100%{opacity:1;filter:blur(0);transform:translateY(0) scale(1)}}",
"@keyframes localRgToastOut{0%{opacity:1;filter:blur(0);transform:translateY(0) scale(1)}100%{opacity:0;filter:blur(6px);transform:translateY(-18px) scale(.96)}}",
"@media(max-width:700px){.local-rg-download-button{right:6px!important;bottom:6px!important;left:auto!important;width:38px!important;height:38px!important;font-size:21px!important}.local-rg-toast-stack{top:10px!important}}"
].join("");
(document.head || document.documentElement).appendChild(style);
}
const BUTTON_ICON_DOWNLOAD = [
'<span class="local-rg-button-glyph local-rg-icon-download" aria-hidden="true">',
'<svg viewBox="0 0 24 24"><path d="M12 3v12"/><path d="m7 11 5 5 5-5"/><path d="M5 21h14"/></svg>',
'</span>'
].join("");
const BUTTON_ICON_CHECK = [
'<span class="local-rg-button-glyph local-rg-icon-check" aria-hidden="true">',
'<svg viewBox="0 0 24 24"><path d="m5 12 4.2 4.2L19 6.5"/></svg>',
'</span>'
].join("");
const BUTTON_ICON_REDOWNLOAD = [
'<span class="local-rg-button-glyph local-rg-icon-redownload" aria-hidden="true">',
'<svg viewBox="0 0 24 24"><path d="M21 12a9 9 0 0 0-15-6.7L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 15 6.7L21 16"/><path d="M21 21v-5h-5"/></svg>',
'</span>'
].join("");
const BUTTON_ICON_ERROR = [
'<span class="local-rg-button-glyph local-rg-icon-error" aria-hidden="true">',
'<svg viewBox="0 0 24 24"><path d="M12 5v9"/><path d="M12 18h.01"/></svg>',
'</span>'
].join("");
function setButtonState(button, state) {
button.dataset.state = state;
const markup = state === "downloaded"
? BUTTON_ICON_CHECK + BUTTON_ICON_REDOWNLOAD
: state === "done"
? BUTTON_ICON_CHECK
: state === "error"
? BUTTON_ICON_ERROR
: BUTTON_ICON_DOWNLOAD;
if (button.__localRgIconMarkup !== markup) {
button.innerHTML = markup;
button.__localRgIconMarkup = markup;
}
const labels = {
downloaded: "Скачать повторно",
done: "Скачивание завершено",
error: "Ошибка загрузки",
working: "Скачивание",
idle: "Скачать видео"
};
button.setAttribute("aria-label", labels[state] || "Скачать видео");
}
const TOAST_COMPLETE_MS = 5000;
const TOAST_EXIT_MS = 300;
const toastEntries = new Map();
const jobButtons = new Map();
const pendingJobButtons = new Map();
const mediaButtons = new Map();
const downloadedRecords = new Map();
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",
event: String(event || "event").slice(0, 120),
details: details || {}
};
contentDiagnosticEvents.push(entry);
if (contentDiagnosticEvents.length > 100) contentDiagnosticEvents.splice(0, 20);
try {
const pending = chrome.runtime.sendMessage({
action: "diagnosticEvent",
level: entry.level,
area: "content",
event: entry.event,
details: entry.details
});
if (pending && typeof pending.catch === "function") pending.catch(() => {});
} catch {
// Local page diagnostics remain available.
}
}
function diagnosticSafeUrl(value) {
try {
const parsed = new URL(String(value || ""), document.baseURI);
parsed.username = "";
parsed.password = "";
parsed.search = "";
parsed.hash = "";
return parsed.toString();
} catch {
return String(value || "").slice(0, 300);
}
}
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()
}, details || {});
button.__localRgLayoutReason = next;
if (previous && previous.code === code) return;
if (!button.__localRgDecisionHistory) button.__localRgDecisionHistory = [];
const anchor = button.__localRgAnchor;
const root = button.__localRgRoot;
const host = button.__localRgScrollHost;
button.__localRgDecisionHistory.push({
decision: next,
previousCode: previous && previous.code || null,
inlineDisplay: button.style && button.style.display || null,
anchorRect: diagnosticRect(anchor),
anchorConnected: !anchor || !("isConnected" in anchor) || Boolean(anchor.isConnected),
rootConnected: !root || !("isConnected" in root) || Boolean(root.isConnected),
hostScroll: host ? {
top: Number(host.scrollTop) || 0,
left: Number(host.scrollLeft) || 0
} : null
});
if (button.__localRgDecisionHistory.length > 12) {
button.__localRgDecisionHistory.splice(0, button.__localRgDecisionHistory.length - 12);
}
}
function isGeneralFeedPage() {
return !/\/(?:users|watch|ifr)\//i.test(String(location.pathname || ""));
}
function isPageChromeRoot(root) {
if (!root || typeof root.closest !== "function") return false;
return Boolean(root.closest([
"header",
"nav",
"aside",
"[role='banner']",
"[role='navigation']",
"[role='complementary']",
"[class*='sidebar' i]",
"[class*='side-menu' i]"
].join(", ")));
}
const MODAL_SELECTOR = [
"[aria-modal='true']",
"[role='dialog']",
"[data-testid*='fullscreen' i]",
"[data-testid*='full-screen' i]",
"[class*='fullscreen' i]",
"[class*='full-screen' i]"
].join(", ");
function modalContainerFor(element) {
if (!element) return null;
if (document.fullscreenElement) {
let current = element;
while (current) {
if (current === document.fullscreenElement) return document.fullscreenElement;
current = current.parentElement;
}
}
if (typeof element.closest === "function") {
try {
const matched = element.closest(MODAL_SELECTOR);
if (matched) return matched;
} catch {
// Continue with the geometric fallback below.
}
}
if (typeof globalThis.getComputedStyle !== "function") return null;
const viewportWidth = Number(globalThis.innerWidth) || 0;
const viewportHeight = Number(globalThis.innerHeight) || 0;
let current = element;
while (current && current !== document.body && current !== document.documentElement) {
if (typeof current.getBoundingClientRect === "function") {
const rect = current.getBoundingClientRect();
const style = globalThis.getComputedStyle(current);
if (style && style.position === "fixed"
&& rect.width >= viewportWidth * 0.6
&& rect.height >= viewportHeight * 0.6) {
return current;
}
}
current = current.parentElement;
}
return null;
}
function visibleAreaFor(element) {
if (!element || ("isConnected" in element && !element.isConnected)) return 0;
if (typeof element.getBoundingClientRect !== "function") return 0;
const rect = element.getBoundingClientRect();
const viewportWidth = Number(globalThis.innerWidth) || 0;
const viewportHeight = Number(globalThis.innerHeight) || 0;
if (!rect || rect.width <= 0 || rect.height <= 0) return 0;
return Math.max(0, Math.min(viewportWidth, rect.right) - Math.max(0, rect.left))
* Math.max(0, Math.min(viewportHeight, rect.bottom) - Math.max(0, rect.top));
}
function usableLayoutRect(rect) {
return Boolean(rect && rect.width > 0 && rect.height > 0);
}
function rectOverlapRatio(first, second) {
if (!usableLayoutRect(first) || !usableLayoutRect(second)) return 0;
const width = Math.max(0, Math.min(first.right, second.right) - Math.max(first.left, second.left));
const height = Math.max(0, Math.min(first.bottom, second.bottom) - Math.max(first.top, second.top));
const smallerArea = Math.min(first.width * first.height, second.width * second.height);
return smallerArea > 0 ? (width * height) / smallerArea : 0;
}
function rememberAnchorGeometry(button, anchorRect, rootRect) {
if (!button || !usableLayoutRect(anchorRect) || !usableLayoutRect(rootRect)) return;
button.__localRgAnchorGeometry = {
left: anchorRect.left - rootRect.left,
top: anchorRect.top - rootRect.top,
width: anchorRect.width,
height: anchorRect.height
};
}
function anchorRectFromRoot(button, rootRect) {
const geometry = button && button.__localRgAnchorGeometry;
if (!geometry || !usableLayoutRect(rootRect)) return null;
const left = rootRect.left + geometry.left;
const top = rootRect.top + geometry.top;
return {
left,
right: left + geometry.width,
top,
bottom: top + geometry.height,
width: geometry.width,
height: geometry.height
};
}
function setStyleProperty(element, name, value) {
if (element && element.style && typeof element.style.setProperty === "function") {
element.style.setProperty(name, value, "important");
} else if (element && element.style) {
element.style[name] = value;
}
}
function pageTopBoundary(viewportWidth, viewportHeight) {
let boundary = 0;
if (!document.querySelectorAll) return 8;
const selectors = [
"header",
"[role='banner']",
"[class*='site-header' i]",
"[class*='top-header' i]",
"[class*='topbar' i]"
].join(", ");
for (const element of Array.from(document.querySelectorAll(selectors))) {
if (!element || typeof element.getBoundingClientRect !== "function") continue;
const rect = element.getBoundingClientRect();
if (!rect || rect.width < viewportWidth * 0.45 || rect.height <= 0 || rect.height > viewportHeight * 0.35) continue;
if (rect.top <= 8 && rect.bottom > 0) boundary = Math.max(boundary, rect.bottom);
}
return Math.max(8, Math.min(boundary + (boundary ? 8 : 0), viewportHeight));
}
function isDocumentScrollHost(element) {
return !element
|| element === document.body
|| element === document.documentElement
|| element === document.scrollingElement;
}
function scrollHostFor(element) {
if (!element || typeof globalThis.getComputedStyle !== "function") {
return document.scrollingElement || document.body || document.documentElement;
}
let current = element.parentElement;
while (current && current !== document.body && current !== document.documentElement) {
try {
const style = globalThis.getComputedStyle(current);
const overflow = String(style && (style.overflowY || style.overflow) || "");
const scrollableOverflow = /auto|scroll|overlay/i.test(overflow);
const hasScrollRange = Number(current.scrollHeight) > Number(current.clientHeight) + 1;
if ((scrollableOverflow && hasScrollRange) || Number(current.scrollTop) > 0) return current;
} catch {
// Continue towards the document scroll host.
}
current = current.parentElement;
}
return document.scrollingElement || document.body || document.documentElement;
}
function feedPositionInHost(button, left, top) {
const host = button.__localRgScrollHost;
if (isDocumentScrollHost(host)) {
return {
left: left + (Number(globalThis.scrollX) || Number(globalThis.pageXOffset) || 0),
top: top + (Number(globalThis.scrollY) || Number(globalThis.pageYOffset) || 0)
};
}
if (!host || typeof host.getBoundingClientRect !== "function") return { left, top };
const hostRect = host.getBoundingClientRect();
return {
left: left - hostRect.left + Number(host.scrollLeft || 0) - Number(host.clientLeft || 0),
top: top - hostRect.top + Number(host.scrollTop || 0) - Number(host.clientTop || 0)
};
}
function modalButtonPosition(overlay, mediaRect, size, viewportWidth, viewportHeight) {
const fallback = {
left: Math.max(8, Math.min(mediaRect.right - size - 18, viewportWidth - size - 8)),
top: Math.max(8, Math.min(mediaRect.bottom - size - 64, viewportHeight - size - 8))
};
if (!overlay || typeof overlay.querySelectorAll !== "function") return fallback;
const controls = [];
for (const element of Array.from(overlay.querySelectorAll("button, [role='button']"))) {
if (!element || element.classList && element.classList.contains("local-rg-download-button")) continue;
if (typeof element.getBoundingClientRect !== "function") continue;
const rect = element.getBoundingClientRect();
if (!rect || rect.width < 18 || rect.height < 18 || rect.width > 110 || rect.height > 110) continue;
const centerX = (rect.left + rect.right) / 2;
if (centerX < mediaRect.right - 190 || centerX > mediaRect.right + 24) continue;
if (rect.bottom <= mediaRect.top || rect.top >= mediaRect.bottom - 44) continue;
controls.push({ rect, centerX });
}
if (!controls.length) return fallback;
const clusters = [];
for (const control of controls) {
let cluster = clusters.find((item) => Math.abs(item.centerX - control.centerX) <= 26);
if (!cluster) {
cluster = { centerX: control.centerX, controls: [] };
clusters.push(cluster);
}
cluster.controls.push(control);
cluster.centerX = cluster.controls.reduce((sum, item) => sum + item.centerX, 0) / cluster.controls.length;
}
clusters.sort((left, right) => right.controls.length - left.controls.length || right.centerX - left.centerX);
const column = clusters[0];
if (!column || column.controls.length < 2) return fallback;
const lastBottom = Math.max.apply(null, column.controls.map((item) => item.rect.bottom));
const safeBottom = Math.min(mediaRect.bottom - 58, viewportHeight - 8);
let top = lastBottom + 12;
if (top + size > safeBottom) top = safeBottom - size;
if (top < mediaRect.top + 8) return fallback;
return {
left: Math.max(8, Math.min(column.centerX - size / 2, viewportWidth - size - 8)),
top: Math.max(8, Math.min(top, viewportHeight - size - 8))
};
}
function scheduleReconnectCheck() {
if (externalReconnectTimer != null) return;
externalReconnectTimer = setTimeout(() => {
externalReconnectTimer = null;
scheduleExternalLayout();
}, ANCHOR_RECONNECT_GRACE_MS + 40);
}
function preserveWhileReconnecting(button, timestamp) {
if (!button.__localRgAnchorMissingSince) {
button.__localRgAnchorMissingSince = timestamp;
scheduleReconnectCheck();
}
return timestamp - button.__localRgAnchorMissingSince < ANCHOR_RECONNECT_GRACE_MS;
}
function layoutExternalButtons() {
externalLayoutFrame = null;
const viewportWidth = Number(globalThis.innerWidth) || (document.documentElement && document.documentElement.clientWidth) || 0;
const viewportHeight = Number(globalThis.innerHeight) || (document.documentElement && document.documentElement.clientHeight) || 0;
const topBoundary = pageTopBoundary(viewportWidth, viewportHeight);
const size = viewportWidth <= 700 ? 38 : 42;
const gap = 10;
const candidates = [];
const timestamp = Date.now();
for (const button of Array.from(externalButtons)) {
const root = button.__localRgRoot;
if (!root || ("isConnected" in root && !root.isConnected)) {
if (preserveWhileReconnecting(button, timestamp)) {
setButtonLayoutReason(button, "root-reconnecting");
continue;
}
disposeButton(button, "root-disconnected");
continue;
}
const anchor = button.__localRgAnchor || root;
const overlay = modalContainerFor(anchor) || modalContainerFor(root);
const rootRect = typeof root.getBoundingClientRect === "function" ? root.getBoundingClientRect() : null;
const anchorConnected = anchor && (!("isConnected" in anchor) || anchor.isConnected);
const anchorRect = anchorConnected && typeof anchor.getBoundingClientRect === "function"
? anchor.getBoundingClientRect()
: null;
const anchorMatchesRoot = overlay
|| !usableLayoutRect(rootRect)
|| (usableLayoutRect(anchorRect) && rectOverlapRatio(anchorRect, rootRect) >= 0.35);
let rect = anchorMatchesRoot && usableLayoutRect(anchorRect) ? anchorRect : null;
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
? "visible-from-root-anchor-disconnected"
: usableLayoutRect(anchorRect)
? "visible-from-root-anchor-outside-card"
: "visible-from-root-anchor-zero-size";
}
if (!usableLayoutRect(rect)) {
if (preserveWhileReconnecting(button, timestamp)) {
setButtonLayoutReason(button, anchorConnected ? "anchor-zero-size-reconnecting" : "anchor-reconnecting");
continue;
}
setStyleProperty(button, "display", "none");
setButtonLayoutReason(button, anchorConnected ? "anchor-zero-size" : "anchor-disconnected", {
width: anchorRect && anchorRect.width,
height: anchorRect && anchorRect.height
});
continue;
}
button.__localRgAnchorMissingSince = 0;
if (rect.bottom < 0 || rect.top > viewportHeight) {
setStyleProperty(button, "display", "none");
setButtonLayoutReason(button, "anchor-outside-viewport", { top: rect.top, bottom: rect.bottom });
continue;
}
const visibleLeft = Math.max(0, rect.left);
const visibleRight = Math.min(viewportWidth, rect.right);
const visibleTop = Math.max(0, rect.top);
const visibleBottom = Math.min(viewportHeight, rect.bottom);
const visibleWidth = visibleRight - visibleLeft;
const visibleHeight = visibleBottom - visibleTop;
if (visibleWidth <= 0 || visibleHeight <= 0) {
setStyleProperty(button, "display", "none");
setButtonLayoutReason(button, "anchor-no-visible-area", { visibleWidth, visibleHeight });
continue;
}
candidates.push({ button, rect, visibleTop, visibleBottom, overlay, fallbackReason });
}
const overlayCandidates = candidates.filter((candidate) => candidate.overlay);
let layoutCandidates = candidates;
if (overlayCandidates.length) {
overlayCandidates.sort((left, right) => {
return visibleAreaFor(right.overlay)
- visibleAreaFor(left.overlay)
|| (right.visibleBottom - right.visibleTop) - (left.visibleBottom - left.visibleTop);
});
layoutCandidates = [overlayCandidates[0]];
const activeButton = layoutCandidates[0].button;
for (const button of externalButtons) {
if (button !== activeButton) {
setStyleProperty(button, "display", "none");
setButtonLayoutReason(button, "suppressed-by-fullscreen");
}
}
}
for (const candidate of layoutCandidates) {
const button = candidate.button;
const rect = candidate.rect;
if (candidate.overlay) {
const modalSize = viewportWidth <= 700 ? 36 : 38;
const position = modalButtonPosition(candidate.overlay, rect, modalSize, viewportWidth, viewportHeight);
setStyleProperty(button, "display", "flex");
setStyleProperty(button, "--local-rg-left", Math.round(position.left) + "px");
setStyleProperty(button, "--local-rg-top", Math.round(position.top) + "px");
setButtonLayoutReason(button, "visible-fullscreen", { left: position.left, top: position.top });
continue;
}
let left = rect.right + gap;
let top = Math.min(rect.bottom - size - gap, viewportHeight - size - 8);
if (left + size > viewportWidth - 8) {
left = Math.max(8, Math.min(rect.left + gap, viewportWidth - size - 8));
top = Math.min(rect.top + gap, viewportHeight - size - 8);
}
if (top < topBoundary) {
setStyleProperty(button, "display", "none");
setButtonLayoutReason(button, "hidden-under-page-header", { intendedTop: top, topBoundary });
continue;
}
setStyleProperty(button, "display", "flex");
const position = feedPositionInHost(button, left, top);
setStyleProperty(button, "--local-rg-left", Math.round(position.left) + "px");
setStyleProperty(button, "--local-rg-top", Math.round(position.top) + "px");
setButtonLayoutReason(button, candidate.fallbackReason || "visible-feed", {
left: position.left,
top: position.top,
topBoundary
});
}
}
function scheduleExternalLayout() {
if (externalLayoutFrame != null) return;
const schedule = typeof globalThis.requestAnimationFrame === "function"
? globalThis.requestAnimationFrame.bind(globalThis)
: (callback) => setTimeout(callback, 16);
let ranSynchronously = false;
const frame = schedule(() => {
ranSynchronously = true;
externalLayoutFrame = null;
layoutExternalButtons();
});
if (!ranSynchronously) externalLayoutFrame = frame;
}
function scheduleFeedRefresh() {
scheduleExternalLayout();
}
function configureButtonLayout(root, button, anchor) {
const overlay = modalContainerFor(anchor || root) || modalContainerFor(root);
const external = isGeneralFeedPage() || Boolean(overlay);
const wasExternal = externalButtons.has(button);
button.__localRgRoot = root;
button.__localRgAnchor = anchor || root;
button.__localRgOverlay = overlay;
button.__localRgAnchorMissingSince = 0;
button.dataset.layout = overlay ? "modal-external" : (external ? "feed-external" : "tile");
if (external) {
externalButtons.add(button);
if (!wasExternal) setStyleProperty(button, "display", "none");
const host = overlay
? (document.body || document.documentElement)
: scrollHostFor(anchor || root);
button.__localRgScrollHost = host;
if (!overlay && host && !isDocumentScrollHost(host)) {
host.classList.add("local-rg-scroll-host");
if (!host.__localRgScrollListener && typeof host.addEventListener === "function") {
host.__localRgScrollListener = true;
host.addEventListener("scroll", scheduleFeedRefresh, { passive: true });
}
}
if (button.parentElement !== host) host.appendChild(button);
scheduleExternalLayout();
} else {
externalButtons.delete(button);
button.__localRgScrollHost = null;
if (button.parentElement !== root) root.appendChild(button);
}
}
function normalizeMediaKey(value) {
const key = String(value || "").trim().toLowerCase();
return /^[a-z0-9_-]{2,160}$/i.test(key) ? key : null;
}
function diagnosticRect(element) {
if (!element || typeof element.getBoundingClientRect !== "function") return null;
try {
const rect = element.getBoundingClientRect();
return {
left: Math.round(rect.left * 10) / 10,
right: Math.round(rect.right * 10) / 10,
top: Math.round(rect.top * 10) / 10,
bottom: Math.round(rect.bottom * 10) / 10,
width: Math.round(rect.width * 10) / 10,
height: Math.round(rect.height * 10) / 10
};
} catch {
return null;
}
}
function diagnosticElement(element) {
if (!element) return null;
let style = null;
try {
const computed = typeof globalThis.getComputedStyle === "function"
? globalThis.getComputedStyle(element)
: null;
if (computed) {
style = {
display: computed.display || null,
visibility: computed.visibility || null,
opacity: computed.opacity || null,
position: computed.position || null,
overflow: computed.overflow || null,
overflowX: computed.overflowX || null,
overflowY: computed.overflowY || null,
zIndex: computed.zIndex || null,
transform: computed.transform || null
};
}
} catch {
// The remaining element data is still useful.
}
const get = (name) => {
try {
return element.getAttribute && element.getAttribute(name);
} catch {
return null;
}
};
return {
tag: String(element.tagName || "").toLowerCase() || null,
id: String(element.id || get("id") || "").slice(0, 160) || null,
className: String(get("class") || element.className || "").slice(0, 500) || null,
feedItemId: get("data-feed-item-id"),
role: get("role"),
ariaModal: get("aria-modal"),
isConnected: !("isConnected" in element) || Boolean(element.isConnected),
rect: diagnosticRect(element),
scroll: {
top: Number(element.scrollTop) || 0,
left: Number(element.scrollLeft) || 0,
height: Number(element.scrollHeight) || 0,
width: Number(element.scrollWidth) || 0,
clientHeight: Number(element.clientHeight) || 0,
clientWidth: Number(element.clientWidth) || 0
},
style
};
}
function diagnosticAncestors(element) {
const ancestors = [];
let current = element;
for (let depth = 0; current && depth < 14; depth += 1) {
ancestors.push(diagnosticElement(current));
current = current.parentElement;
}
return ancestors;
}
function diagnosticButton(button) {
const root = button && button.__localRgRoot;
const anchor = button && button.__localRgAnchor;
const host = button && button.__localRgScrollHost;
const overlay = button && (button.__localRgOverlay || modalContainerFor(anchor) || modalContainerFor(root));
return {
mediaId: button && button.__localRgMediaKey || null,
title: button && button.__localRedgifsItem && button.__localRedgifsItem.title || null,
layout: button && button.dataset && button.dataset.layout || null,
state: button && button.dataset && button.dataset.state || null,
inlineDisplay: button && button.style && button.style.display || null,
cssLeft: button && button.style && button.style["--local-rg-left"] || null,
cssTop: button && button.style && button.style["--local-rg-top"] || null,
lastLayoutDecision: button && button.__localRgLayoutReason || null,
decisionHistory: button && button.__localRgDecisionHistory || [],
button: diagnosticElement(button),
root: diagnosticElement(root),
anchor: diagnosticElement(anchor),
scrollHost: diagnosticElement(host),
parent: diagnosticElement(button && button.parentElement),
overlay: diagnosticElement(overlay),
anchorAncestors: diagnosticAncestors(anchor)
};
}
function capturePageDiagnostics() {
ensureButtonStyles();
const buttonSet = new Set(externalButtons);
for (const buttonsForMedia of mediaButtons.values()) {
for (const button of buttonsForMedia) buttonSet.add(button);
}
const buttons = Array.from(buttonSet);
const media = Array.from(document.querySelectorAll
? document.querySelectorAll("video, img[src], img[data-src], img[data-lazy-src]")
: []).slice(0, 300).map((element) => {
const root = closestCard(element);
const host = scrollHostFor(element);
return {
element: diagnosticElement(element),
posterId: element.tagName === "IMG"
? idFromPosterUrl(element.currentSrc || element.getAttribute("src") || element.getAttribute("data-src") || element.getAttribute("data-lazy-src"))
: idFromPosterUrl(element.poster || element.getAttribute && element.getAttribute("poster")),
root: diagnosticElement(root),
scrollHost: diagnosticElement(host),
overlay: diagnosticElement(modalContainerFor(element) || modalContainerFor(root)),
ancestors: diagnosticAncestors(element)
};
});
for (const button of buttons) {
const root = button.__localRgRoot;
const anchor = button.__localRgAnchor;
const host = button.__localRgScrollHost;
if (button.classList) button.classList.add("local-rg-diagnostic-button");
if (root && root.classList) root.classList.add("local-rg-diagnostic-root");
if (anchor && anchor.classList) anchor.classList.add("local-rg-diagnostic-anchor");
if (host && host.classList && !isDocumentScrollHost(host)) host.classList.add("local-rg-diagnostic-host");
}
setTimeout(() => {
for (const element of Array.from(document.querySelectorAll ? document.querySelectorAll(
".local-rg-diagnostic-button,.local-rg-diagnostic-root,.local-rg-diagnostic-anchor,.local-rg-diagnostic-host"
) : [])) {
if (!element.classList) continue;
element.classList.remove("local-rg-diagnostic-button");
element.classList.remove("local-rg-diagnostic-root");
element.classList.remove("local-rg-diagnostic-anchor");
element.classList.remove("local-rg-diagnostic-host");
}
}, 30000);
return {
generatedAt: new Date().toISOString(),
extensionVersion: chrome.runtime.getManifest ? chrome.runtime.getManifest().version : null,
page: {
url: diagnosticSafeUrl(location.href),
title: document.title || null,
readyState: document.readyState || null,
visibilityState: document.visibilityState || null,
online: typeof navigator !== "undefined" ? navigator.onLine : null,
viewport: { width: Number(globalThis.innerWidth) || 0, height: Number(globalThis.innerHeight) || 0 },
windowScroll: {
x: Number(globalThis.scrollX) || Number(globalThis.pageXOffset) || 0,
y: Number(globalThis.scrollY) || Number(globalThis.pageYOffset) || 0
},
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",
purpleDashed: "scroll host",
orange: "download button"
},
buttons: buttons.map(diagnosticButton),
media,
lastScanDiagnostics,
contentEvents: contentDiagnosticEvents.slice(),
resourceTiming: typeof performance !== "undefined" && performance.getEntriesByType
? performance.getEntriesByType("resource").slice(-150).map((entry) => ({
url: diagnosticSafeUrl(entry.name),
initiatorType: entry.initiatorType || null,
durationMs: Math.round(Number(entry.duration) * 10) / 10,
transferSize: Number(entry.transferSize) || 0,
encodedBodySize: Number(entry.encodedBodySize) || 0,
decodedBodySize: Number(entry.decodedBodySize) || 0
}))
: []
};
}
function findReusableExternalButton(root, item, anchor) {
const key = normalizeMediaKey(item && item.id);
const buttons = key && mediaButtons.get(key);
if (!buttons) return null;
for (const button of Array.from(buttons)) {
if (!button) {
buttons.delete(button);
continue;
}
const previousRoot = button.__localRgRoot;
if (!previousRoot) {
disposeButton(button, "reusable-without-root");
continue;
}
const sameRoot = previousRoot === root;
const previousConnected = previousRoot
&& (!("isConnected" in previousRoot) || previousRoot.isConnected);
const nextVisible = Math.max(
visibleAreaFor(root),
visibleAreaFor(anchor || root)
) > 0;
const adopt = sameRoot
|| (!previousConnected && nextVisible);
if (!adopt) continue;
if (previousRoot && previousRoot !== root && previousRoot.__localRgDownloadButton === button) {
previousRoot.__localRgDownloadButton = null;
}
root.__localRgDownloadButton = button;
return { button, adopt: true };
}
return null;
}
function setButtonReadyState(button) {
if (!button) return;
if (button.__localRgDownloadedRecord) {
button.title = "Уже скачано · нажмите, чтобы скачать ещё раз";
setButtonState(button, "downloaded", "✓");
} else {
button.title = "Скачать видео в лучшем доступном качестве";
setButtonState(button, "idle", "⇩");
}
}
function markButtonDownloaded(button, record) {
if (!button) return;
button.__localRgDownloadedRecord = record || { downloadedAt: Date.now() };
if (!button.disabled) setButtonReadyState(button);
}
function markMediaDownloaded(mediaId, record) {
const key = normalizeMediaKey(mediaId);
if (!key) return;
const value = record || { downloadedAt: Date.now() };
downloadedRecords.set(key, value);
checkedMediaIds.add(key);
const buttons = mediaButtons.get(key);
if (!buttons) return;
for (const button of buttons) {
if (button.__localRgMediaKey === key) markButtonDownloaded(button, value);
}
}
async function flushHistoryChecks() {
historyCheckTimer = null;
const ids = Array.from(pendingHistoryIds).slice(0, 500);
for (const id of ids) pendingHistoryIds.delete(id);
if (!ids.length) return;
try {
const response = await chrome.runtime.sendMessage({ action: "getDownloadedMedia", ids });
if (!response || !response.ok) throw new Error("История загрузок недоступна");
const records = response.downloaded || {};
for (const id of ids) {
checkedMediaIds.add(id);
if (records[id]) markMediaDownloaded(id, records[id]);
}
} catch (error) {
recordContentDiagnostic("warn", "download-history-check-failed", {
idCount: ids.length,
name: error && error.name,
message: error && error.message
});
for (const id of ids) pendingHistoryIds.add(id);
}
}
function queueHistoryCheck(mediaId) {
const key = normalizeMediaKey(mediaId);
if (!key || checkedMediaIds.has(key) || downloadedRecords.has(key)) return;
pendingHistoryIds.add(key);
clearTimeout(historyCheckTimer);
historyCheckTimer = setTimeout(flushHistoryChecks, 80);
}
function registerMediaButton(button, item) {
const key = normalizeMediaKey(item && item.id);
const previousKey = button.__localRgMediaKey || null;
if (previousKey && previousKey !== key) {
const previousButtons = mediaButtons.get(previousKey);
if (previousButtons) previousButtons.delete(button);
button.__localRgDownloadedRecord = null;
}
button.__localRgMediaKey = key;
if (!key) {
if (!button.disabled) setButtonReadyState(button);
return;
}
if (!mediaButtons.has(key)) mediaButtons.set(key, new Set());
mediaButtons.get(key).add(button);
if (downloadedRecords.has(key)) {
markButtonDownloaded(button, downloadedRecords.get(key));
} else {
if (previousKey !== key && !button.disabled) setButtonReadyState(button);
queueHistoryCheck(key);
}
}
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") {
try {
root.animate([
{ opacity: 0, transform: "translateY(-28px) scale(.9)", filter: "blur(9px)" },
{ offset: 0.72, opacity: 1, transform: "translateY(4px) scale(1.015)", filter: "blur(0)" },
{ opacity: 1, transform: "translateY(0) scale(1)", filter: "blur(0)" }
], {
duration: 460,
easing: "cubic-bezier(.16,1,.3,1)"
});
return;
} catch {
// CSS fallback below.
}
}
root.classList.add("is-entering");
setTimeout(() => root.classList.remove("is-entering"), 470);
}
function ensureToastStack() {
ensureButtonStyles();
let stack = document.getElementById("local-rg-toast-stack");
if (stack) return stack;
stack = document.createElement("div");
stack.id = "local-rg-toast-stack";
stack.className = "local-rg-toast-stack";
(document.body || document.documentElement).appendChild(stack);
return stack;
}
function finishToastRemoval(jobId, entry) {
if (toastEntries.get(jobId) !== entry) return;
if (entry.timer) clearTimeout(entry.timer);
if (entry.removeTimer) clearTimeout(entry.removeTimer);
if (entry.countdownInterval) clearInterval(entry.countdownInterval);
if (entry.root && entry.root.remove) entry.root.remove();
toastEntries.delete(jobId);
}
function removeToast(jobId, immediate) {
const entry = toastEntries.get(jobId);
if (!entry) return;
if (entry.timer) clearTimeout(entry.timer);
if (entry.countdownInterval) clearInterval(entry.countdownInterval);
if (immediate || !entry.root || !entry.root.classList) {
finishToastRemoval(jobId, entry);
return;
}
if (entry.leaving) return;
entry.leaving = true;
if (typeof entry.root.animate === "function") {
try {
const animation = entry.root.animate([
{ opacity: 1, transform: "translateY(0) scale(1)", filter: "blur(0)" },
{ opacity: 0, transform: "translateY(-24px) scale(.92)", filter: "blur(8px)" }
], {
duration: TOAST_EXIT_MS,
easing: "cubic-bezier(.55,0,1,.45)",
fill: "forwards"
});
if (animation && animation.finished && typeof animation.finished.then === "function") {
animation.finished.then(
() => finishToastRemoval(jobId, entry),
() => finishToastRemoval(jobId, entry)
);
}
} catch {
entry.root.classList.add("is-leaving");
}
} else {
entry.root.classList.add("is-leaving");
}
entry.removeTimer = setTimeout(() => finishToastRemoval(jobId, entry), TOAST_EXIT_MS + 80);
}
function transferToast(fromJobId, toJobId) {
if (!fromJobId || !toJobId || fromJobId === toJobId) return toastEntries.get(fromJobId) || null;
const entry = toastEntries.get(fromJobId);
if (!entry) return null;
const existing = toastEntries.get(toJobId);
if (existing && existing !== entry) finishToastRemoval(toJobId, existing);
toastEntries.delete(fromJobId);
toastEntries.set(toJobId, entry);
return entry;
}
function adoptPendingJob(job) {
const requestId = String(job && job.clientRequestId || "");
if (!requestId || !job.id) return;
const button = pendingJobButtons.get(requestId);
if (!button) return;
transferToast(requestId, job.id);
button.__localRgJobId = job.id;
jobButtons.set(job.id, button);
pendingJobButtons.delete(requestId);
}
function resetButtonForJob(jobId, button) {
if (!button || button.__localRgJobId !== jobId) return;
button.disabled = false;
button.title = "Скачать видео в лучшем доступном качестве";
button.__localRgJobId = null;
button.__localRgTerminalJobId = null;
button.__localRgResetTimer = null;
jobButtons.delete(jobId);
setButtonReadyState(button);
}
function syncButtonWithJob(job) {
const button = jobButtons.get(job.id);
if (!button || button.__localRgJobId !== job.id) return;
if (button.__localRgTerminalJobId === job.id) return;
if (job.state === "complete") {
button.disabled = true;
button.__localRgTerminalJobId = job.id;
button.title = "Скачивание завершено";
setButtonState(button, "done", "✓");
if (!button.__localRgResetTimer) {
button.__localRgResetTimer = setTimeout(
() => resetButtonForJob(job.id, button),
TOAST_COMPLETE_MS + TOAST_EXIT_MS
);
}
return;
}
if (job.state === "error") {
button.disabled = true;
button.__localRgTerminalJobId = job.id;
button.title = job.error || job.status || "Ошибка загрузки";
setButtonState(button, "error", "!");
if (!button.__localRgResetTimer) {
button.__localRgResetTimer = setTimeout(() => resetButtonForJob(job.id, button), 7500);
}
return;
}
button.disabled = true;
button.title = job.status || "Скачивание…";
setButtonState(button, "working", "⇩");
}
function showDownloadToast(job) {
if (!job || !job.id) return;
adoptPendingJob(job);
if (job.state === "complete" && job.mediaId) {
markMediaDownloaded(job.mediaId, {
downloadedAt: Date.now(),
filename: job.filename || job.title || null,
profileSlug: job.profileSlug || null
});
}
syncButtonWithJob(job);
const stack = ensureToastStack();
let entry = toastEntries.get(job.id);
if (!entry) {
const root = document.createElement("div");
root.className = "local-rg-toast";
const main = document.createElement("div");
main.className = "local-rg-toast-main";
const icon = document.createElement("span");
icon.className = "local-rg-toast-icon";
const copy = document.createElement("span");
copy.className = "local-rg-toast-copy";
const title = document.createElement("strong");
title.className = "local-rg-toast-title";
const status = document.createElement("span");
status.className = "local-rg-toast-status";
const value = document.createElement("span");
value.className = "local-rg-toast-value";
const track = document.createElement("div");
track.className = "local-rg-toast-track";
const bar = document.createElement("div");
bar.className = "local-rg-toast-bar";
copy.appendChild(title);
copy.appendChild(status);
main.appendChild(icon);
main.appendChild(copy);
main.appendChild(value);
track.appendChild(bar);
root.appendChild(main);
root.appendChild(track);
stack.appendChild(root);
animateToastEntrance(root);
entry = {
root,
icon,
title,
status,
value,
bar,
timer: null,
removeTimer: null,
countdownInterval: null,
completionStartedAt: 0,
terminalState: null,
leaving: false
};
toastEntries.set(job.id, entry);
}
if (entry.terminalState || entry.leaving) return;
const progress = Math.max(0, Math.min(100, Math.round(Number(job.progress) || 0)));
entry.root.dataset.state = job.state || "running";
entry.title.textContent = job.title || job.filename || "RedGifs video";
entry.status.textContent = job.error || job.status || "Подготовка загрузки…";
entry.value.textContent = progress + "%";
entry.bar.style.setProperty("width", progress + "%", "important");
entry.icon.textContent = job.state === "complete" ? "✓" : job.state === "error" ? "!" : "⇩";
if (job.state === "complete") {
entry.terminalState = "complete";
entry.completionStartedAt = Date.now();
const completeStatus = job.status || "Скачивание завершено";
const updateCountdown = () => {
const remaining = Math.max(0, TOAST_COMPLETE_MS - (Date.now() - entry.completionStartedAt));
const seconds = Math.ceil(remaining / 1000);
entry.status.textContent = remaining > 0
? completeStatus + " · закрытие через " + seconds + " с"
: completeStatus + " · закрытие…";
entry.value.textContent = seconds + " с";
entry.bar.style.setProperty("width", (remaining / TOAST_COMPLETE_MS * 100) + "%", "important");
};
updateCountdown();
entry.countdownInterval = setInterval(updateCountdown, 250);
entry.timer = setTimeout(() => removeToast(job.id), TOAST_COMPLETE_MS);
} else if (job.state === "error") {
entry.terminalState = "error";
entry.timer = setTimeout(() => removeToast(job.id), 7500);
}
}
function ensureDownloadButton(root, item, anchor) {
if (!root || !root.appendChild || !item) return;
ensureButtonStyles();
root.classList.add("local-rg-download-host");
let button = root.__localRgDownloadButton
|| Array.from(root.children || []).find((child) => child.classList && child.classList.contains("local-rg-download-button"));
if (!button) {
const reusable = findReusableExternalButton(root, item, anchor);
if (reusable && !reusable.adopt) return;
button = reusable && reusable.button;
}
if (button) {
button.__localRedgifsItem = item;
configureButtonLayout(root, button, anchor);
registerMediaButton(button, item);
return;
}
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 = "Скачать видео в лучшем доступном качестве";
button.setAttribute("aria-label", "Скачать видео");
setButtonState(button, "idle", "⇩");
button.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
if (button.disabled) return;
button.disabled = true;
const pendingId = "pending:"
+ (button.__localRedgifsItem.id || "video")
+ ":" + Date.now()
+ ":" + Math.random().toString(36).slice(2, 8);
pendingJobButtons.set(pendingId, button);
setButtonState(button, "working", "⇩");
showDownloadToast({
id: pendingId,
state: "running",
progress: 0,
title: button.__localRedgifsItem.title || "RedGifs video",
status: "Поиск лучшего качества…"
});
try {
button.title = "Скачать видео в лучшем доступном качестве";
const response = await chrome.runtime.sendMessage({
action: "downloadCapturedItem",
item: Object.assign({}, button.__localRedgifsItem, { clientRequestId: pendingId })
});
if (!response || !response.ok) {
throw new Error(response && response.error ? response.error : "Загрузка не запущена");
}
const jobId = response.result && response.result.jobId;
if (!jobId) throw new Error("Загрузка не вернула идентификатор задания");
pendingJobButtons.delete(pendingId);
transferToast(pendingId, jobId);
button.__localRgJobId = jobId;
jobButtons.set(jobId, button);
showDownloadToast({
id: jobId,
state: "queued",
progress: 0,
title: button.__localRedgifsItem.title || "RedGifs video",
status: "Загрузка запущена…"
});
try {
const latest = await chrome.runtime.sendMessage({ action: "getJob", jobId });
if (latest && latest.ok && latest.job) showDownloadToast(latest.job);
} catch {
// Subsequent progress messages keep the button and toast synchronized.
}
} catch (error) {
recordContentDiagnostic("error", "download-request-failed", {
mediaId: button.__localRedgifsItem && button.__localRedgifsItem.id,
name: error && error.name,
message: error && error.message
});
pendingJobButtons.delete(pendingId);
const errorId = "error:" + Date.now();
transferToast(pendingId, errorId);
showDownloadToast({
id: errorId,
state: "error",
progress: 0,
title: button.__localRedgifsItem.title || "RedGifs video",
status: error && error.message ? error.message : String(error),
error: error && error.message ? error.message : String(error)
});
button.title = error && error.message ? error.message : String(error);
setButtonState(button, "error", "!");
setTimeout(() => {
button.disabled = false;
setButtonReadyState(button);
}, 2600);
}
}, true);
configureButtonLayout(root, button, anchor);
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 seenItems = new Set();
const seenRoots = new Set();
lastScanDiagnostics = [];
const nodes = Array.from(document.querySelectorAll(MEDIA_CANDIDATE_SELECTOR));
for (const node of nodes) {
const root = closestCard(node);
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);
}
pruneDetachedButtons();
scheduleExternalLayout();
return {
pageUrl: location.href,
pageTitle: document.title || "RedGifs",
items
};
}
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 {
sendResponse({ ok: true });
} catch {
// No response channel is required.
}
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) {
recordContentDiagnostic("error", "diagnostic-capture-failed", {
name: error && error.name,
message: error && error.message
});
sendResponse({ ok: false, error: error && error.message ? error.message : String(error) });
}
return false;
}
if (message.action !== "scanRedgifsPage") return false;
try {
sendResponse({ ok: true, result: scanPage() });
} catch (error) {
recordContentDiagnostic("error", "manual-scan-failed", {
name: error && error.name,
message: error && error.message
});
sendResponse({ ok: false, error: error && error.message ? error.message : String(error) });
}
return false;
});
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(", ")));
}
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", "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);
});
}
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);
}