"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 = [
'',
'',
''
].join("");
const BUTTON_ICON_CHECK = [
'',
'',
''
].join("");
const BUTTON_ICON_REDOWNLOAD = [
'',
'',
''
].join("");
const BUTTON_ICON_ERROR = [
'',
'',
''
].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);
}