Release RedGifs Video Downloader 0.6.23
This commit is contained in:
1 parent
9d6149e6bf
commit
3f1d8b9845
7 files changed
+640
-107
No files matched your search
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
### Из GitHub Release
|
### Из GitHub Release
|
||||||
|
|
||||||
1. Скачайте архив `RedGifs-Video-Downloader-v0.6.21.zip` из последнего Release.
|
1. Скачайте архив `RedGifs-Video-Downloader-v0.6.23.zip` из последнего Release.
|
||||||
2. Распакуйте архив в постоянную папку. Не удаляйте её после установки.
|
2. Распакуйте архив в постоянную папку. Не удаляйте её после установки.
|
||||||
3. Откройте `chrome://extensions` в Chrome, Chromium, Brave или другом совместимом браузере.
|
3. Откройте `chrome://extensions` в Chrome, Chromium, Brave или другом совместимом браузере.
|
||||||
4. Включите **Режим разработчика**.
|
4. Включите **Режим разработчика**.
|
||||||
@@ -95,7 +95,7 @@ SAMPLE-AES и DRM не поддерживаются и не обходятся.
|
|||||||
|
|
||||||
## Диагностика
|
## Диагностика
|
||||||
|
|
||||||
Кнопка **Скачать диагностический лог** находится в popup. Журнал может содержать сведения о DOM-структуре страницы, расположении карточек и кнопок, этапах работы расширения, HTTP-статусах и сетевых ошибках. Он не содержит тело видео и намеренно очищается от известных секретов.
|
Диагностика по умолчанию выключена. Для поиска проблемы откройте настройки popup, включите переключатель **Включить диагностику**, воспроизведите проблему и нажмите **Скачать диагностический лог**. После отключения накопленные диагностические события и история позиционирования очищаются. Журнал не содержит тело видео и намеренно очищается от известных секретов.
|
||||||
|
|
||||||
Перед публикацией диагностического файла всё равно рекомендуется просмотреть его вручную.
|
Перед публикацией диагностического файла всё равно рекомендуется просмотреть его вручную.
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ SAMPLE-AES и DRM не поддерживаются и не обходятся.
|
|||||||
|
|
||||||
## Версия
|
## Версия
|
||||||
|
|
||||||
Текущая версия: **0.6.21**.
|
Текущая версия: **0.6.23**.
|
||||||
|
|
||||||
История релиза: [RELEASE_NOTES.md](RELEASE_NOTES.md).
|
История релиза: [RELEASE_NOTES.md](RELEASE_NOTES.md).
|
||||||
|
|
||||||
|
|||||||
+20
-4
@@ -1,7 +1,23 @@
|
|||||||
# RedGifs Video Downloader 0.6.21
|
# RedGifs Video Downloader 0.6.23
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Исправления 0.6.23
|
||||||
|
|
||||||
|
- Выбор тега в профиле больше не запускает отдельный глубокий обход для каждого фрагмента перестраиваемой страницы.
|
||||||
|
- Серии крупных DOM-изменений объединяются в один отложенный пересмотр после стабилизации страницы.
|
||||||
|
- Отключённые и удалённые карточки освобождаются из реестра наблюдения одним проходом.
|
||||||
|
- В кнопки и диагностический интерфейс добавлен номер фактически работающего content script, чтобы отличать свежую версию от оставшейся в старой вкладке.
|
||||||
|
|
||||||
|
## Исправления 0.6.22
|
||||||
|
|
||||||
|
- Устранён полный перескан страницы каждые 160 мс при прокрутке и изменениях DOM.
|
||||||
|
- Карточки обрабатываются небольшими порциями только рядом с видимой областью.
|
||||||
|
- Кнопки полноэкранного просмотра больше не забираются у исходной карточки профиля.
|
||||||
|
- Удалённые карточки, кнопки и цели наблюдения освобождаются после фильтрации по тегам.
|
||||||
|
- Диагностика получила отдельный переключатель и по умолчанию выключена.
|
||||||
|
- При выключении диагностики очищаются накопленные события и история позиционирования.
|
||||||
|
|
||||||
Первый релиз независимого локального загрузчика для Chromium.
|
Первый релиз независимого локального загрузчика для Chromium.
|
||||||
|
|
||||||
## Главное
|
## Главное
|
||||||
@@ -22,7 +38,7 @@
|
|||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
1. Скачайте `RedGifs-Video-Downloader-v0.6.21.zip`.
|
1. Скачайте `RedGifs-Video-Downloader-v0.6.23.zip`.
|
||||||
2. Распакуйте архив.
|
2. Распакуйте архив.
|
||||||
3. Откройте `chrome://extensions`.
|
3. Откройте `chrome://extensions`.
|
||||||
4. Включите режим разработчика.
|
4. Включите режим разработчика.
|
||||||
@@ -35,6 +51,6 @@
|
|||||||
## Контрольная сумма
|
## Контрольная сумма
|
||||||
|
|
||||||
```text
|
```text
|
||||||
RedGifs-Video-Downloader-v0.6.21.zip
|
RedGifs-Video-Downloader-v0.6.23.zip
|
||||||
SHA-256: a81ea275639fcbf56f1224c32c02a1db45e7d618f701068b277881aaea7efba7
|
SHA-256: см. описание GitHub Release
|
||||||
```
|
```
|
||||||
+42
-1
@@ -5,6 +5,7 @@ const TOKEN_TTL_MS = 50 * 60 * 1000;
|
|||||||
const DOWNLOAD_HISTORY_KEY = "downloadHistoryV1";
|
const DOWNLOAD_HISTORY_KEY = "downloadHistoryV1";
|
||||||
const DOWNLOAD_HISTORY_LIMIT = 5000;
|
const DOWNLOAD_HISTORY_LIMIT = 5000;
|
||||||
const DIAGNOSTIC_LOG_KEY = "diagnosticLogV1";
|
const DIAGNOSTIC_LOG_KEY = "diagnosticLogV1";
|
||||||
|
const DIAGNOSTICS_SETTING_KEY = "diagnosticsEnabled";
|
||||||
const DIAGNOSTIC_LOG_LIMIT = 500;
|
const DIAGNOSTIC_LOG_LIMIT = 500;
|
||||||
const DIAGNOSTIC_REPORT_MAX_CHARS = 6 * 1024 * 1024;
|
const DIAGNOSTIC_REPORT_MAX_CHARS = 6 * 1024 * 1024;
|
||||||
const tokenCache = { value: null, expiresAt: 0 };
|
const tokenCache = { value: null, expiresAt: 0 };
|
||||||
@@ -15,6 +16,33 @@ let downloadHistoryCache = null;
|
|||||||
let downloadHistoryWrite = Promise.resolve();
|
let downloadHistoryWrite = Promise.resolve();
|
||||||
let diagnosticLogCache = null;
|
let diagnosticLogCache = null;
|
||||||
let diagnosticLogWrite = Promise.resolve();
|
let diagnosticLogWrite = Promise.resolve();
|
||||||
|
let diagnosticsEnabled = false;
|
||||||
|
|
||||||
|
function applyDiagnosticsState(enabled) {
|
||||||
|
diagnosticsEnabled = Boolean(enabled);
|
||||||
|
if (diagnosticsEnabled) return;
|
||||||
|
diagnosticLogWrite = diagnosticLogWrite.catch(() => {}).then(async () => {
|
||||||
|
diagnosticLogCache = [];
|
||||||
|
try {
|
||||||
|
await chrome.storage.session.remove(DIAGNOSTIC_LOG_KEY);
|
||||||
|
} catch {
|
||||||
|
// Diagnostics are already disabled in memory.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
chrome.storage.local.get(DIAGNOSTICS_SETTING_KEY).then((stored) => {
|
||||||
|
applyDiagnosticsState(Boolean(stored && stored[DIAGNOSTICS_SETTING_KEY]));
|
||||||
|
}).catch(() => {});
|
||||||
|
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||||
|
if (areaName === "local" && changes[DIAGNOSTICS_SETTING_KEY]) {
|
||||||
|
applyDiagnosticsState(Boolean(changes[DIAGNOSTICS_SETTING_KEY].newValue));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
applyDiagnosticsState(false);
|
||||||
|
}
|
||||||
|
|
||||||
function diagnosticUrl(value) {
|
function diagnosticUrl(value) {
|
||||||
try {
|
try {
|
||||||
@@ -57,6 +85,7 @@ function diagnosticValue(value, key, depth) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadDiagnosticLog() {
|
async function loadDiagnosticLog() {
|
||||||
|
if (!diagnosticsEnabled) return [];
|
||||||
if (diagnosticLogCache) return diagnosticLogCache;
|
if (diagnosticLogCache) return diagnosticLogCache;
|
||||||
try {
|
try {
|
||||||
const stored = await chrome.storage.session.get(DIAGNOSTIC_LOG_KEY);
|
const stored = await chrome.storage.session.get(DIAGNOSTIC_LOG_KEY);
|
||||||
@@ -70,6 +99,7 @@ async function loadDiagnosticLog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recordDiagnostic(level, area, event, details) {
|
function recordDiagnostic(level, area, event, details) {
|
||||||
|
if (!diagnosticsEnabled) return null;
|
||||||
const entry = {
|
const entry = {
|
||||||
at: new Date().toISOString(),
|
at: new Date().toISOString(),
|
||||||
level: ["debug", "info", "warn", "error"].includes(level) ? level : "info",
|
level: ["debug", "info", "warn", "error"].includes(level) ? level : "info",
|
||||||
@@ -91,12 +121,21 @@ function recordDiagnostic(level, area, event, details) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function diagnosticRuntimeBundle() {
|
async function diagnosticRuntimeBundle() {
|
||||||
|
if (!diagnosticsEnabled) {
|
||||||
|
return {
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
extensionVersion: chrome.runtime.getManifest ? chrome.runtime.getManifest().version : null,
|
||||||
|
diagnosticsEnabled: false,
|
||||||
|
events: []
|
||||||
|
};
|
||||||
|
}
|
||||||
await diagnosticLogWrite.catch(() => {});
|
await diagnosticLogWrite.catch(() => {});
|
||||||
const log = await loadDiagnosticLog();
|
const log = await loadDiagnosticLog();
|
||||||
const settings = await getDownloadSettings();
|
const settings = await getDownloadSettings();
|
||||||
return {
|
return {
|
||||||
generatedAt: new Date().toISOString(),
|
generatedAt: new Date().toISOString(),
|
||||||
extensionVersion: chrome.runtime.getManifest ? chrome.runtime.getManifest().version : null,
|
extensionVersion: chrome.runtime.getManifest ? chrome.runtime.getManifest().version : null,
|
||||||
|
diagnosticsEnabled: true,
|
||||||
platform: typeof navigator !== "undefined" ? {
|
platform: typeof navigator !== "undefined" ? {
|
||||||
userAgent: navigator.userAgent || null,
|
userAgent: navigator.userAgent || null,
|
||||||
language: navigator.language || null,
|
language: navigator.language || null,
|
||||||
@@ -1082,10 +1121,12 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
tabId: sender && sender.tab && Number.isInteger(sender.tab.id) ? sender.tab.id : null,
|
tabId: sender && sender.tab && Number.isInteger(sender.tab.id) ? sender.tab.id : null,
|
||||||
source: sender && sender.tab ? "content" : "extension-page"
|
source: sender && sender.tab ? "content" : "extension-page"
|
||||||
}));
|
}));
|
||||||
return { ok: true };
|
return { ok: true, ignored: !diagnosticsEnabled };
|
||||||
case "getDiagnosticLog":
|
case "getDiagnosticLog":
|
||||||
|
if (!diagnosticsEnabled) return { ok: false, error: "Включите диагностику в настройках расширения." };
|
||||||
return { ok: true, diagnostics: await diagnosticRuntimeBundle() };
|
return { ok: true, diagnostics: await diagnosticRuntimeBundle() };
|
||||||
case "downloadDiagnosticReport":
|
case "downloadDiagnosticReport":
|
||||||
|
if (!diagnosticsEnabled) return { ok: false, error: "Включите диагностику в настройках расширения." };
|
||||||
return { ok: true, result: await downloadDiagnosticReport(message.report) };
|
return { ok: true, result: await downloadDiagnosticReport(message.report) };
|
||||||
case "hlsProgress":
|
case "hlsProgress":
|
||||||
return { ok: true, job: await updateJob(message) };
|
return { ok: true, job: await updateJob(message) };
|
||||||
|
|||||||
+518
-90
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
if (!globalThis.__localRedgifsScannerInstalled) {
|
if (!globalThis.__localRedgifsScannerInstalled) {
|
||||||
globalThis.__localRedgifsScannerInstalled = true;
|
globalThis.__localRedgifsScannerInstalled = true;
|
||||||
|
const CONTENT_SCRIPT_VERSION = "0.6.23";
|
||||||
|
|
||||||
function isRedgifsHost(hostname) {
|
function isRedgifsHost(hostname) {
|
||||||
const host = String(hostname || "").toLowerCase();
|
const host = String(hostname || "").toLowerCase();
|
||||||
@@ -333,14 +334,39 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
const externalButtons = new Set();
|
const externalButtons = new Set();
|
||||||
const checkedMediaIds = new Set();
|
const checkedMediaIds = new Set();
|
||||||
const pendingHistoryIds = 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;
|
const ANCHOR_RECONNECT_GRACE_MS = 500;
|
||||||
let historyCheckTimer = null;
|
let historyCheckTimer = null;
|
||||||
let externalLayoutFrame = null;
|
let externalLayoutFrame = null;
|
||||||
let externalReconnectTimer = null;
|
let externalReconnectTimer = null;
|
||||||
let lastScanDiagnostics = [];
|
let lastScanDiagnostics = [];
|
||||||
const contentDiagnosticEvents = [];
|
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) {
|
function recordContentDiagnostic(level, event, details) {
|
||||||
|
if (!diagnosticsEnabled) return null;
|
||||||
const entry = {
|
const entry = {
|
||||||
at: new Date().toISOString(),
|
at: new Date().toISOString(),
|
||||||
level: level || "info",
|
level: level || "info",
|
||||||
@@ -379,6 +405,11 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
function setButtonLayoutReason(button, code, details) {
|
function setButtonLayoutReason(button, code, details) {
|
||||||
if (!button) return;
|
if (!button) return;
|
||||||
const previous = button.__localRgLayoutReason || null;
|
const previous = button.__localRgLayoutReason || null;
|
||||||
|
if (!diagnosticsEnabled) {
|
||||||
|
button.__localRgLayoutReason = { code };
|
||||||
|
button.__localRgDecisionHistory = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
const next = Object.assign({
|
const next = Object.assign({
|
||||||
code,
|
code,
|
||||||
at: new Date().toISOString()
|
at: new Date().toISOString()
|
||||||
@@ -393,9 +424,7 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
decision: next,
|
decision: next,
|
||||||
previousCode: previous && previous.code || null,
|
previousCode: previous && previous.code || null,
|
||||||
inlineDisplay: button.style && button.style.display || null,
|
inlineDisplay: button.style && button.style.display || null,
|
||||||
buttonRect: diagnosticRect(button),
|
|
||||||
anchorRect: diagnosticRect(anchor),
|
anchorRect: diagnosticRect(anchor),
|
||||||
rootRect: diagnosticRect(root),
|
|
||||||
anchorConnected: !anchor || !("isConnected" in anchor) || Boolean(anchor.isConnected),
|
anchorConnected: !anchor || !("isConnected" in anchor) || Boolean(anchor.isConnected),
|
||||||
rootConnected: !root || !("isConnected" in root) || Boolean(root.isConnected),
|
rootConnected: !root || !("isConnected" in root) || Boolean(root.isConnected),
|
||||||
hostScroll: host ? {
|
hostScroll: host ? {
|
||||||
@@ -403,7 +432,9 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
left: Number(host.scrollLeft) || 0
|
left: Number(host.scrollLeft) || 0
|
||||||
} : null
|
} : null
|
||||||
});
|
});
|
||||||
if (button.__localRgDecisionHistory.length > 40) button.__localRgDecisionHistory.splice(0, 10);
|
if (button.__localRgDecisionHistory.length > 12) {
|
||||||
|
button.__localRgDecisionHistory.splice(0, button.__localRgDecisionHistory.length - 12);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGeneralFeedPage() {
|
function isGeneralFeedPage() {
|
||||||
@@ -661,8 +692,7 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
setButtonLayoutReason(button, "root-reconnecting");
|
setButtonLayoutReason(button, "root-reconnecting");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
setStyleProperty(button, "display", "none");
|
disposeButton(button, "root-disconnected");
|
||||||
setButtonLayoutReason(button, "root-disconnected");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const anchor = button.__localRgAnchor || root;
|
const anchor = button.__localRgAnchor || root;
|
||||||
@@ -679,6 +709,9 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
let fallbackReason = null;
|
let fallbackReason = null;
|
||||||
if (rect && usableLayoutRect(rootRect)) {
|
if (rect && usableLayoutRect(rootRect)) {
|
||||||
rememberAnchorGeometry(button, rect, rootRect);
|
rememberAnchorGeometry(button, rect, rootRect);
|
||||||
|
} else if (overlay && usableLayoutRect(rootRect)) {
|
||||||
|
rect = rootRect;
|
||||||
|
fallbackReason = "visible-from-modal-root";
|
||||||
} else {
|
} else {
|
||||||
rect = anchorRectFromRoot(button, rootRect);
|
rect = anchorRectFromRoot(button, rootRect);
|
||||||
fallbackReason = !anchorConnected
|
fallbackReason = !anchorConnected
|
||||||
@@ -788,7 +821,6 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
|
|
||||||
function scheduleFeedRefresh() {
|
function scheduleFeedRefresh() {
|
||||||
scheduleExternalLayout();
|
scheduleExternalLayout();
|
||||||
scheduleDecoration();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function configureButtonLayout(root, button, anchor) {
|
function configureButtonLayout(root, button, anchor) {
|
||||||
@@ -991,6 +1023,13 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
topBoundary: pageTopBoundary(Number(globalThis.innerWidth) || 0, Number(globalThis.innerHeight) || 0),
|
topBoundary: pageTopBoundary(Number(globalThis.innerWidth) || 0, Number(globalThis.innerHeight) || 0),
|
||||||
fullscreenElement: diagnosticElement(document.fullscreenElement)
|
fullscreenElement: diagnosticElement(document.fullscreenElement)
|
||||||
},
|
},
|
||||||
|
scanner: {
|
||||||
|
diagnosticsEnabled,
|
||||||
|
trackedButtons: buttons.length,
|
||||||
|
trackedMediaIds: mediaButtons.size,
|
||||||
|
externalButtons: externalButtons.size,
|
||||||
|
pendingDecorationRoots: pendingDecorationRoots.size
|
||||||
|
},
|
||||||
legend: {
|
legend: {
|
||||||
cyan: "anchor (video/poster)",
|
cyan: "anchor (video/poster)",
|
||||||
yellow: "card root",
|
yellow: "card root",
|
||||||
@@ -1018,28 +1057,26 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
const key = normalizeMediaKey(item && item.id);
|
const key = normalizeMediaKey(item && item.id);
|
||||||
const buttons = key && mediaButtons.get(key);
|
const buttons = key && mediaButtons.get(key);
|
||||||
if (!buttons) return null;
|
if (!buttons) return null;
|
||||||
for (const button of buttons) {
|
for (const button of Array.from(buttons)) {
|
||||||
if (!button) continue;
|
if (!button) {
|
||||||
|
buttons.delete(button);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const previousRoot = button.__localRgRoot;
|
const previousRoot = button.__localRgRoot;
|
||||||
const previousAnchor = button.__localRgAnchor || previousRoot;
|
if (!previousRoot) {
|
||||||
const previousOverlay = button.__localRgOverlay || modalContainerFor(previousAnchor) || modalContainerFor(previousRoot);
|
disposeButton(button, "reusable-without-root");
|
||||||
const nextOverlay = modalContainerFor(anchor || root) || modalContainerFor(root);
|
continue;
|
||||||
|
}
|
||||||
const sameRoot = previousRoot === root;
|
const sameRoot = previousRoot === root;
|
||||||
const previousConnected = previousRoot
|
const previousConnected = previousRoot
|
||||||
&& (!("isConnected" in previousRoot) || previousRoot.isConnected);
|
&& (!("isConnected" in previousRoot) || previousRoot.isConnected);
|
||||||
const previousVisible = Math.max(
|
|
||||||
visibleAreaFor(previousRoot),
|
|
||||||
visibleAreaFor(previousAnchor)
|
|
||||||
) > 0;
|
|
||||||
const nextVisible = Math.max(
|
const nextVisible = Math.max(
|
||||||
visibleAreaFor(root),
|
visibleAreaFor(root),
|
||||||
visibleAreaFor(anchor || root)
|
visibleAreaFor(anchor || root)
|
||||||
) > 0;
|
) > 0;
|
||||||
const adopt = sameRoot
|
const adopt = sameRoot
|
||||||
|| (!previousOverlay && Boolean(nextOverlay))
|
|| (!previousConnected && nextVisible);
|
||||||
|| (!previousConnected && nextVisible)
|
if (!adopt) continue;
|
||||||
|| (!previousVisible && nextVisible);
|
|
||||||
if (!adopt) return { button, adopt: false };
|
|
||||||
if (previousRoot && previousRoot !== root && previousRoot.__localRgDownloadButton === button) {
|
if (previousRoot && previousRoot !== root && previousRoot.__localRgDownloadButton === button) {
|
||||||
previousRoot.__localRgDownloadButton = null;
|
previousRoot.__localRgDownloadButton = null;
|
||||||
}
|
}
|
||||||
@@ -1133,6 +1170,64 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function disposeButton(button, reason) {
|
||||||
|
if (!button) return;
|
||||||
|
const root = button.__localRgRoot;
|
||||||
|
const mediaKey = button.__localRgMediaKey;
|
||||||
|
externalButtons.delete(button);
|
||||||
|
if (mediaKey) {
|
||||||
|
const buttons = mediaButtons.get(mediaKey);
|
||||||
|
if (buttons) {
|
||||||
|
buttons.delete(button);
|
||||||
|
if (!buttons.size) mediaButtons.delete(mediaKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (root && root.__localRgDownloadButton === button) {
|
||||||
|
root.__localRgDownloadButton = null;
|
||||||
|
}
|
||||||
|
if (!button.__localRgJobId && button.__localRgResetTimer) {
|
||||||
|
clearTimeout(button.__localRgResetTimer);
|
||||||
|
button.__localRgResetTimer = null;
|
||||||
|
}
|
||||||
|
if (button.remove) button.remove();
|
||||||
|
button.__localRgRoot = null;
|
||||||
|
button.__localRgAnchor = null;
|
||||||
|
button.__localRgOverlay = null;
|
||||||
|
button.__localRgScrollHost = null;
|
||||||
|
button.__localRgAnchorGeometry = null;
|
||||||
|
button.__localRgAnchorMissingSince = 0;
|
||||||
|
button.__localRgMediaKey = null;
|
||||||
|
button.__localRgDownloadedRecord = null;
|
||||||
|
button.__localRedgifsItem = null;
|
||||||
|
button.__localRgLayoutReason = {
|
||||||
|
code: reason || "disposed",
|
||||||
|
at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
button.__localRgDecisionHistory = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneDetachedButtons() {
|
||||||
|
for (const buttons of Array.from(mediaButtons.values())) {
|
||||||
|
for (const button of Array.from(buttons)) {
|
||||||
|
if (externalButtons.has(button)) continue;
|
||||||
|
const root = button && button.__localRgRoot;
|
||||||
|
if (!root || ("isConnected" in root && !root.isConnected)) {
|
||||||
|
disposeButton(button, "pruned-disconnected-root");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const button of Array.from(externalButtons)) {
|
||||||
|
if (!button) {
|
||||||
|
externalButtons.delete(button);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const root = button.__localRgRoot;
|
||||||
|
if (!root || ("isConnected" in root && !root.isConnected)) {
|
||||||
|
scheduleExternalLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function animateToastEntrance(root) {
|
function animateToastEntrance(root) {
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
if (typeof root.animate === "function") {
|
if (typeof root.animate === "function") {
|
||||||
@@ -1387,6 +1482,7 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
button = document.createElement("button");
|
button = document.createElement("button");
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.className = "local-rg-download-button";
|
button.className = "local-rg-download-button";
|
||||||
|
button.dataset.runtimeVersion = CONTENT_SCRIPT_VERSION;
|
||||||
root.__localRgDownloadButton = button;
|
root.__localRgDownloadButton = button;
|
||||||
button.__localRedgifsItem = item;
|
button.__localRedgifsItem = item;
|
||||||
button.title = "Скачать видео в лучшем доступном качестве";
|
button.title = "Скачать видео в лучшем доступном качестве";
|
||||||
@@ -1468,62 +1564,111 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
registerMediaButton(button, item);
|
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() {
|
function scanPage() {
|
||||||
const items = [];
|
const items = [];
|
||||||
const seen = new Set();
|
const seenItems = new Set();
|
||||||
|
const seenRoots = new Set();
|
||||||
lastScanDiagnostics = [];
|
lastScanDiagnostics = [];
|
||||||
|
|
||||||
function add(node) {
|
const nodes = Array.from(document.querySelectorAll(MEDIA_CANDIDATE_SELECTOR));
|
||||||
if (!node) return;
|
for (const node of nodes) {
|
||||||
const root = closestCard(node);
|
const root = closestCard(node);
|
||||||
if (!root || isPageChromeRoot(root)) return;
|
if (!root || seenRoots.has(root)) continue;
|
||||||
const feedNode = (root.matches && root.matches("[data-feed-item-id]"))
|
seenRoots.add(root);
|
||||||
? root
|
const extracted = extractCandidate(root);
|
||||||
: root.querySelector && root.querySelector("[data-feed-item-id]");
|
if (!extracted) continue;
|
||||||
const feedId = feedNode && feedNode.getAttribute("data-feed-item-id");
|
const duplicate = seenItems.has(extracted.key);
|
||||||
const link = (root.matches && root.matches("a[href*='/watch/'], a[href*='/ifr/']"))
|
if (!duplicate) seenItems.add(extracted.key);
|
||||||
? root
|
if (extracted.mediaAnchor && isNearViewport(extracted.mediaAnchor, 900)) {
|
||||||
: root.querySelector && root.querySelector("a[href*='/watch/'], a[href*='/ifr/']");
|
ensureDownloadButton(extracted.root, extracted.item, extracted.mediaAnchor);
|
||||||
const href = normalizeUrl(link && (link.getAttribute("href") || link.href));
|
}
|
||||||
const video = (root.tagName === "VIDEO") ? root : root.querySelector && root.querySelector("video");
|
if (diagnosticsEnabled && lastScanDiagnostics.length < 80) {
|
||||||
const previewImage = mediaPosterImage(root);
|
lastScanDiagnostics.push({
|
||||||
const directVideoUrl = videoSource(video);
|
key: extracted.key,
|
||||||
const thumbnail = posterFromRoot(root, video);
|
duplicate,
|
||||||
const posterId = idFromPosterUrl(thumbnail);
|
outcome: extracted.mediaAnchor ? "candidate" : "no-video-or-redgifs-poster",
|
||||||
if (!feedId && !href && posterId && !isExplicitMediaCard(root)) return;
|
item: {
|
||||||
const stableId = feedId || idFromUrl(href) || posterId;
|
id: extracted.item.id,
|
||||||
if (isGeneralFeedPage() && !stableId) return;
|
title: extracted.item.title,
|
||||||
const id = stableId || idFromUrl(directVideoUrl);
|
watchUrl: extracted.item.watchUrl,
|
||||||
if (!id && !directVideoUrl) return;
|
thumbnail: extracted.item.thumbnail
|
||||||
const key = id ? "id:" + id.toLowerCase() : "url:" + directVideoUrl;
|
},
|
||||||
const duplicate = seen.has(key);
|
root: lightweightElement(extracted.root),
|
||||||
if (!duplicate) seen.add(key);
|
anchor: lightweightElement(extracted.mediaAnchor)
|
||||||
const item = {
|
});
|
||||||
id: id || null,
|
}
|
||||||
watchUrl: href || (id ? "https://www.redgifs.com/watch/" + encodeURIComponent(id) : null),
|
if (!duplicate && items.length < 240) items.push(extracted.item);
|
||||||
directVideoUrl: directVideoUrl || null,
|
|
||||||
thumbnail,
|
|
||||||
title: titleFromRoot(root, id),
|
|
||||||
profileSlug: profileSlugFromRoot(root) || currentProfileSlug()
|
|
||||||
};
|
|
||||||
const mediaAnchor = video || previewImage;
|
|
||||||
if (mediaAnchor) ensureDownloadButton(root, item, mediaAnchor);
|
|
||||||
lastScanDiagnostics.push({
|
|
||||||
key,
|
|
||||||
duplicate,
|
|
||||||
outcome: mediaAnchor ? "decorated" : "no-video-or-redgifs-poster",
|
|
||||||
item: { id: item.id, title: item.title, watchUrl: item.watchUrl, thumbnail: item.thumbnail },
|
|
||||||
root: diagnosticElement(root),
|
|
||||||
anchor: diagnosticElement(mediaAnchor)
|
|
||||||
});
|
|
||||||
if (!duplicate && items.length < 240) items.push(item);
|
|
||||||
}
|
}
|
||||||
|
pruneDetachedButtons();
|
||||||
const nodes = Array.from(document.querySelectorAll(
|
|
||||||
"[data-feed-item-id], a[href*='/watch/'], a[href*='/ifr/'], video, img[src], img[data-src], img[data-lazy-src]"
|
|
||||||
));
|
|
||||||
nodes.sort((left, right) => Number(Boolean(modalContainerFor(right))) - Number(Boolean(modalContainerFor(left))));
|
|
||||||
for (const node of nodes) add(node);
|
|
||||||
scheduleExternalLayout();
|
scheduleExternalLayout();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1535,6 +1680,15 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (!message) return false;
|
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") {
|
if (message.action === "downloadProgress") {
|
||||||
showDownloadToast(message.job);
|
showDownloadToast(message.job);
|
||||||
try {
|
try {
|
||||||
@@ -1545,6 +1699,10 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (message.action === "captureExtensionDiagnostics" || message.action === "captureButtonDiagnostics") {
|
if (message.action === "captureExtensionDiagnostics" || message.action === "captureButtonDiagnostics") {
|
||||||
|
if (!diagnosticsEnabled) {
|
||||||
|
sendResponse({ ok: false, error: "Включите диагностику в настройках расширения." });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
sendResponse({ ok: true, report: capturePageDiagnostics() });
|
sendResponse({ ok: true, report: capturePageDiagnostics() });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1569,36 +1727,306 @@ if (!globalThis.__localRedgifsScannerInstalled) {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
let decorateTimer = null;
|
const observedCandidates = new Set();
|
||||||
function scheduleDecoration() {
|
const intersectingCandidates = new WeakSet();
|
||||||
if (decorateTimer != null) return;
|
const pendingDecorationRoots = new Set();
|
||||||
let ranSynchronously = false;
|
let decorationTask = null;
|
||||||
const timer = setTimeout(() => {
|
let documentSweepTimer = null;
|
||||||
ranSynchronously = true;
|
let documentSweepTask = null;
|
||||||
decorateTimer = null;
|
let tagTransitionUntil = 0;
|
||||||
try {
|
|
||||||
scanPage();
|
const TAG_TRANSITION_GUARD_MS = 12000;
|
||||||
} catch (error) {
|
const MUTATION_BATCH_LIMIT = 64;
|
||||||
recordContentDiagnostic("error", "automatic-scan-failed", {
|
const MUTATION_NODE_LIMIT = 96;
|
||||||
name: error && error.name,
|
|
||||||
message: error && error.message
|
function isExtensionOwnedNode(node) {
|
||||||
});
|
const element = node && node.nodeType === Node.ELEMENT_NODE
|
||||||
// A later DOM mutation will retry.
|
? node
|
||||||
}
|
: node && node.parentElement;
|
||||||
}, 160);
|
if (!element || typeof element.closest !== "function") return false;
|
||||||
if (!ranSynchronously) decorateTimer = timer;
|
return Boolean(element.closest([
|
||||||
|
"#local-redgifs-download-styles",
|
||||||
|
"#local-rg-toast-stack",
|
||||||
|
".local-rg-download-button"
|
||||||
|
].join(", ")));
|
||||||
}
|
}
|
||||||
|
|
||||||
const observer = new MutationObserver(scheduleDecoration);
|
function requestDecorationFlush() {
|
||||||
|
if (decorationTask != null) return;
|
||||||
|
const run = (deadline) => {
|
||||||
|
decorationTask = null;
|
||||||
|
const startedAt = typeof performance !== "undefined" ? performance.now() : Date.now();
|
||||||
|
let processed = 0;
|
||||||
|
while (pendingDecorationRoots.size && processed < 16) {
|
||||||
|
if (processed > 0 && deadline && typeof deadline.timeRemaining === "function" && deadline.timeRemaining() < 2) break;
|
||||||
|
const root = pendingDecorationRoots.values().next().value;
|
||||||
|
pendingDecorationRoots.delete(root);
|
||||||
|
if (root && (!("isConnected" in root) || root.isConnected)) {
|
||||||
|
try {
|
||||||
|
decorateCandidate(root);
|
||||||
|
} catch (error) {
|
||||||
|
recordContentDiagnostic("error", "automatic-decoration-failed", {
|
||||||
|
name: error && error.name,
|
||||||
|
message: error && error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processed += 1;
|
||||||
|
const now = typeof performance !== "undefined" ? performance.now() : Date.now();
|
||||||
|
if (now - startedAt >= 9) break;
|
||||||
|
}
|
||||||
|
pruneDetachedButtons();
|
||||||
|
if (pendingDecorationRoots.size) requestDecorationFlush();
|
||||||
|
};
|
||||||
|
if (typeof globalThis.requestIdleCallback === "function") {
|
||||||
|
decorationTask = globalThis.requestIdleCallback(run, { timeout: 240 });
|
||||||
|
} else {
|
||||||
|
decorationTask = setTimeout(() => run(null), 48);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function queueCandidateDecoration(candidate) {
|
||||||
|
if (!candidate || isExtensionOwnedNode(candidate)) return;
|
||||||
|
const root = closestCard(candidate);
|
||||||
|
if (!root || isPageChromeRoot(root)) return;
|
||||||
|
pendingDecorationRoots.add(root);
|
||||||
|
requestDecorationFlush();
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidateObserver = typeof globalThis.IntersectionObserver === "function"
|
||||||
|
? new globalThis.IntersectionObserver((entries) => {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
intersectingCandidates.add(entry.target);
|
||||||
|
queueCandidateDecoration(entry.target);
|
||||||
|
} else {
|
||||||
|
intersectingCandidates.delete(entry.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { root: null, rootMargin: "800px 240px", threshold: 0 })
|
||||||
|
: null;
|
||||||
|
|
||||||
|
function observeCandidate(candidate) {
|
||||||
|
if (!candidate || candidate.nodeType !== Node.ELEMENT_NODE || isExtensionOwnedNode(candidate)) return;
|
||||||
|
if (typeof candidate.matches !== "function" || !candidate.matches(MEDIA_CANDIDATE_SELECTOR)) return;
|
||||||
|
if (observedCandidates.has(candidate)) return;
|
||||||
|
observedCandidates.add(candidate);
|
||||||
|
if (candidateObserver) {
|
||||||
|
candidateObserver.observe(candidate);
|
||||||
|
} else {
|
||||||
|
queueCandidateDecoration(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function observeCandidateScope(scope) {
|
||||||
|
if (!scope || isExtensionOwnedNode(scope)) return false;
|
||||||
|
let found = false;
|
||||||
|
if (scope.nodeType === Node.ELEMENT_NODE && typeof scope.matches === "function" && scope.matches(MEDIA_CANDIDATE_SELECTOR)) {
|
||||||
|
observeCandidate(scope);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
if (typeof scope.querySelectorAll !== "function") return found;
|
||||||
|
for (const candidate of scope.querySelectorAll(MEDIA_CANDIDATE_SELECTOR)) {
|
||||||
|
observeCandidate(candidate);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unobserveCandidateScope(scope) {
|
||||||
|
if (!candidateObserver || !scope || scope.nodeType !== Node.ELEMENT_NODE || isExtensionOwnedNode(scope)) return false;
|
||||||
|
let found = false;
|
||||||
|
if (typeof scope.matches === "function" && scope.matches(MEDIA_CANDIDATE_SELECTOR)) {
|
||||||
|
candidateObserver.unobserve(scope);
|
||||||
|
observedCandidates.delete(scope);
|
||||||
|
intersectingCandidates.delete(scope);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
if (typeof scope.querySelectorAll !== "function") return found;
|
||||||
|
for (const candidate of scope.querySelectorAll(MEDIA_CANDIDATE_SELECTOR)) {
|
||||||
|
candidateObserver.unobserve(candidate);
|
||||||
|
observedCandidates.delete(candidate);
|
||||||
|
intersectingCandidates.delete(candidate);
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneObservedCandidates() {
|
||||||
|
for (const candidate of Array.from(observedCandidates)) {
|
||||||
|
const connected = !("isConnected" in candidate) || candidate.isConnected;
|
||||||
|
const stillMatches = connected
|
||||||
|
&& typeof candidate.matches === "function"
|
||||||
|
&& candidate.matches(MEDIA_CANDIDATE_SELECTOR);
|
||||||
|
if (stillMatches) continue;
|
||||||
|
if (candidateObserver) candidateObserver.unobserve(candidate);
|
||||||
|
observedCandidates.delete(candidate);
|
||||||
|
intersectingCandidates.delete(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebuildCandidateRegistry() {
|
||||||
|
pruneObservedCandidates();
|
||||||
|
for (const candidate of document.querySelectorAll(MEDIA_CANDIDATE_SELECTOR)) {
|
||||||
|
observeCandidate(candidate);
|
||||||
|
if (isNearViewport(candidate, 900)) queueCandidateDecoration(candidate);
|
||||||
|
}
|
||||||
|
pruneDetachedButtons();
|
||||||
|
scheduleExternalLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelDocumentSweepTask() {
|
||||||
|
if (documentSweepTimer != null) {
|
||||||
|
clearTimeout(documentSweepTimer);
|
||||||
|
documentSweepTimer = null;
|
||||||
|
}
|
||||||
|
if (documentSweepTask == null) return;
|
||||||
|
if (typeof globalThis.cancelIdleCallback === "function") {
|
||||||
|
globalThis.cancelIdleCallback(documentSweepTask);
|
||||||
|
} else {
|
||||||
|
clearTimeout(documentSweepTask);
|
||||||
|
}
|
||||||
|
documentSweepTask = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestDocumentSweep(delay) {
|
||||||
|
cancelDocumentSweepTask();
|
||||||
|
documentSweepTimer = setTimeout(() => {
|
||||||
|
documentSweepTimer = null;
|
||||||
|
const run = () => {
|
||||||
|
documentSweepTask = null;
|
||||||
|
try {
|
||||||
|
rebuildCandidateRegistry();
|
||||||
|
} catch (error) {
|
||||||
|
recordContentDiagnostic("error", "candidate-registry-rebuild-failed", {
|
||||||
|
name: error && error.name,
|
||||||
|
message: error && error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (typeof globalThis.requestIdleCallback === "function") {
|
||||||
|
documentSweepTask = globalThis.requestIdleCallback(run, { timeout: 700 });
|
||||||
|
} else {
|
||||||
|
documentSweepTask = setTimeout(run, 0);
|
||||||
|
}
|
||||||
|
}, Math.max(0, Number(delay) || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTagNavigationControl(target) {
|
||||||
|
if (!target || typeof target.closest !== "function") return false;
|
||||||
|
return Boolean(target.closest([
|
||||||
|
".tagButton",
|
||||||
|
"button[aria-label$=' tag']",
|
||||||
|
"[role='button'][aria-label$=' tag']"
|
||||||
|
].join(", ")));
|
||||||
|
}
|
||||||
|
|
||||||
|
function armTagTransitionGuard(event) {
|
||||||
|
if (!event || !isTagNavigationControl(event.target)) return;
|
||||||
|
tagTransitionUntil = Date.now() + TAG_TRANSITION_GUARD_MS;
|
||||||
|
pendingDecorationRoots.clear();
|
||||||
|
requestDocumentSweep(260);
|
||||||
|
recordContentDiagnostic("info", "tag-transition-guard-armed", {
|
||||||
|
label: event.target && event.target.closest
|
||||||
|
? (event.target.closest("[aria-label]") || {}).ariaLabel || null
|
||||||
|
: null,
|
||||||
|
guardMs: TAG_TRANSITION_GUARD_MS
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLargeMutationBatch(mutations) {
|
||||||
|
if (!mutations || mutations.length >= MUTATION_BATCH_LIMIT) return true;
|
||||||
|
let changedNodes = 0;
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
changedNodes += (mutation.addedNodes && mutation.addedNodes.length) || 0;
|
||||||
|
changedNodes += (mutation.removedNodes && mutation.removedNodes.length) || 0;
|
||||||
|
if (changedNodes >= MUTATION_NODE_LIMIT) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshKnownButtonContexts() {
|
||||||
|
const buttons = new Set(externalButtons);
|
||||||
|
for (const buttonsForMedia of mediaButtons.values()) {
|
||||||
|
for (const button of buttonsForMedia) buttons.add(button);
|
||||||
|
}
|
||||||
|
for (const button of buttons) {
|
||||||
|
const root = button && button.__localRgRoot;
|
||||||
|
const anchor = button && button.__localRgAnchor;
|
||||||
|
if (!root || ("isConnected" in root && !root.isConnected)) continue;
|
||||||
|
configureButtonLayout(root, button, anchor || root);
|
||||||
|
}
|
||||||
|
pruneDetachedButtons();
|
||||||
|
scheduleExternalLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageMutations(mutations) {
|
||||||
|
if (Date.now() < tagTransitionUntil || isLargeMutationBatch(mutations)) {
|
||||||
|
requestDocumentSweep(260);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let pageChanged = false;
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
if (!mutation || isExtensionOwnedNode(mutation.target)) continue;
|
||||||
|
if (mutation.type === "attributes") {
|
||||||
|
const matchesCandidate = mutation.target.matches && mutation.target.matches(MEDIA_CANDIDATE_SELECTOR);
|
||||||
|
if (!matchesCandidate) {
|
||||||
|
if (candidateObserver && observedCandidates.has(mutation.target)) {
|
||||||
|
candidateObserver.unobserve(mutation.target);
|
||||||
|
observedCandidates.delete(mutation.target);
|
||||||
|
intersectingCandidates.delete(mutation.target);
|
||||||
|
pageChanged = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
observeCandidate(mutation.target);
|
||||||
|
if (intersectingCandidates.has(mutation.target)) queueCandidateDecoration(mutation.target);
|
||||||
|
pageChanged = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const addedNode of mutation.addedNodes || []) {
|
||||||
|
if (!addedNode || addedNode.nodeType !== Node.ELEMENT_NODE || isExtensionOwnedNode(addedNode)) continue;
|
||||||
|
if (observeCandidateScope(addedNode)) pageChanged = true;
|
||||||
|
}
|
||||||
|
for (const removedNode of mutation.removedNodes || []) {
|
||||||
|
if (!removedNode || removedNode.nodeType !== Node.ELEMENT_NODE || isExtensionOwnedNode(removedNode)) continue;
|
||||||
|
if (unobserveCandidateScope(removedNode)) pageChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pageChanged) {
|
||||||
|
pruneDetachedButtons();
|
||||||
|
scheduleExternalLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(handlePageMutations);
|
||||||
observer.observe(document.documentElement, {
|
observer.observe(document.documentElement, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true,
|
subtree: true,
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeFilter: ["src", "poster", "data-src", "data-lazy-src", "data-feed-item-id"]
|
attributeFilter: ["src", "poster", "data-src", "data-lazy-src", "data-feed-item-id", "href"]
|
||||||
});
|
});
|
||||||
if (typeof globalThis.addEventListener === "function") {
|
if (typeof globalThis.addEventListener === "function") {
|
||||||
|
globalThis.addEventListener("click", armTagTransitionGuard, { capture: true, passive: true });
|
||||||
globalThis.addEventListener("scroll", scheduleFeedRefresh, { passive: true, capture: true });
|
globalThis.addEventListener("scroll", scheduleFeedRefresh, { passive: true, capture: true });
|
||||||
globalThis.addEventListener("resize", scheduleExternalLayout, { passive: true });
|
globalThis.addEventListener("resize", scheduleExternalLayout, { passive: true });
|
||||||
|
globalThis.addEventListener("fullscreenchange", () => {
|
||||||
|
refreshKnownButtonContexts();
|
||||||
|
observeCandidateScope(document.fullscreenElement || document);
|
||||||
|
setTimeout(refreshKnownButtonContexts, 260);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
scheduleDecoration();
|
try {
|
||||||
|
chrome.storage.local.get("diagnosticsEnabled").then((stored) => {
|
||||||
|
applyDiagnosticsState(Boolean(stored && stored.diagnosticsEnabled));
|
||||||
|
}).catch(() => {});
|
||||||
|
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||||
|
if (areaName === "local" && changes.diagnosticsEnabled) {
|
||||||
|
applyDiagnosticsState(Boolean(changes.diagnosticsEnabled.newValue));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
applyDiagnosticsState(false);
|
||||||
|
}
|
||||||
|
observeCandidateScope(document);
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "RedGifs Video Downloader",
|
"name": "RedGifs Video Downloader",
|
||||||
"version": "0.6.21",
|
"version": "0.6.23",
|
||||||
"description": "Downloads RedGifs media directly and processes HLS locally. No account or telemetry.",
|
"description": "Downloads RedGifs media directly and processes HLS locally. No account or telemetry.",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"activeTab",
|
"activeTab",
|
||||||
|
|||||||
@@ -50,6 +50,14 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label class="settings-check-row">
|
||||||
|
<input id="diagnosticsEnabled" type="checkbox">
|
||||||
|
<span>
|
||||||
|
<strong>Включить диагностику</strong>
|
||||||
|
<small>Временно собирать данные для диагностического лога.</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="settings-preview">
|
<div class="settings-preview">
|
||||||
<span>Пример пути</span>
|
<span>Пример пути</span>
|
||||||
<strong id="settingsPathPreview">Загрузки/RedGifs/profile/video.mp4</strong>
|
<strong id="settingsPathPreview">Загрузки/RedGifs/profile/video.mp4</strong>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const settingsPanel = document.getElementById("settingsPanel");
|
|||||||
const baseFolderInput = document.getElementById("baseFolder");
|
const baseFolderInput = document.getElementById("baseFolder");
|
||||||
const groupByProfileInput = document.getElementById("groupByProfile");
|
const groupByProfileInput = document.getElementById("groupByProfile");
|
||||||
const saveAsInput = document.getElementById("saveAs");
|
const saveAsInput = document.getElementById("saveAs");
|
||||||
|
const diagnosticsEnabledInput = document.getElementById("diagnosticsEnabled");
|
||||||
const settingsPathPreview = document.getElementById("settingsPathPreview");
|
const settingsPathPreview = document.getElementById("settingsPathPreview");
|
||||||
const settingsSaveButton = document.getElementById("settingsSaveButton");
|
const settingsSaveButton = document.getElementById("settingsSaveButton");
|
||||||
const settingsResetButton = document.getElementById("settingsResetButton");
|
const settingsResetButton = document.getElementById("settingsResetButton");
|
||||||
@@ -42,6 +43,7 @@ const DEFAULT_DOWNLOAD_SETTINGS = Object.freeze({
|
|||||||
saveAs: false
|
saveAs: false
|
||||||
});
|
});
|
||||||
let downloadSettings = Object.assign({}, DEFAULT_DOWNLOAD_SETTINGS);
|
let downloadSettings = Object.assign({}, DEFAULT_DOWNLOAD_SETTINGS);
|
||||||
|
let diagnosticsEnabled = false;
|
||||||
let settingsStatusTimer = null;
|
let settingsStatusTimer = null;
|
||||||
let popupBlurTimer = null;
|
let popupBlurTimer = null;
|
||||||
const panelAnimations = new WeakMap();
|
const panelAnimations = new WeakMap();
|
||||||
@@ -125,6 +127,7 @@ function setPanelExpanded(element, expanded, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function reportPopupDiagnostic(level, event, details) {
|
function reportPopupDiagnostic(level, event, details) {
|
||||||
|
if (!diagnosticsEnabled) return;
|
||||||
try {
|
try {
|
||||||
const pending = chrome.runtime.sendMessage({
|
const pending = chrome.runtime.sendMessage({
|
||||||
action: "diagnosticEvent",
|
action: "diagnosticEvent",
|
||||||
@@ -215,9 +218,23 @@ function writeSettingsForm(settings) {
|
|||||||
baseFolderInput.value = settings.baseFolder || "";
|
baseFolderInput.value = settings.baseFolder || "";
|
||||||
groupByProfileInput.checked = settings.groupByProfile !== false;
|
groupByProfileInput.checked = settings.groupByProfile !== false;
|
||||||
saveAsInput.checked = Boolean(settings.saveAs);
|
saveAsInput.checked = Boolean(settings.saveAs);
|
||||||
|
diagnosticsEnabledInput.checked = diagnosticsEnabled;
|
||||||
renderSettingsPathPreview();
|
renderSettingsPathPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderDiagnosticControls() {
|
||||||
|
diagnosticButton.disabled = !diagnosticsEnabled;
|
||||||
|
diagnosticButton.title = diagnosticsEnabled
|
||||||
|
? "Скачать диагностический лог"
|
||||||
|
: "Сначала включите диагностику в настройках";
|
||||||
|
if (!diagnosticsEnabled) {
|
||||||
|
diagnosticStatus.className = "diagnostic-status";
|
||||||
|
diagnosticStatus.textContent = "Диагностика выключена.";
|
||||||
|
} else if (diagnosticStatus.textContent === "Диагностика выключена.") {
|
||||||
|
diagnosticStatus.textContent = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showSettingsStatus(text, kind) {
|
function showSettingsStatus(text, kind) {
|
||||||
if (settingsStatusTimer) clearTimeout(settingsStatusTimer);
|
if (settingsStatusTimer) clearTimeout(settingsStatusTimer);
|
||||||
settingsStatusTimer = null;
|
settingsStatusTimer = null;
|
||||||
@@ -233,11 +250,17 @@ function showSettingsStatus(text, kind) {
|
|||||||
|
|
||||||
async function saveDownloadSettings() {
|
async function saveDownloadSettings() {
|
||||||
const settings = readSettingsForm();
|
const settings = readSettingsForm();
|
||||||
|
const nextDiagnosticsEnabled = Boolean(diagnosticsEnabledInput.checked);
|
||||||
settingsSaveButton.disabled = true;
|
settingsSaveButton.disabled = true;
|
||||||
try {
|
try {
|
||||||
await chrome.storage.local.set({ downloadSettings: settings });
|
await chrome.storage.local.set({
|
||||||
|
downloadSettings: settings,
|
||||||
|
diagnosticsEnabled: nextDiagnosticsEnabled
|
||||||
|
});
|
||||||
downloadSettings = Object.assign({}, DEFAULT_DOWNLOAD_SETTINGS, settings);
|
downloadSettings = Object.assign({}, DEFAULT_DOWNLOAD_SETTINGS, settings);
|
||||||
|
diagnosticsEnabled = nextDiagnosticsEnabled;
|
||||||
writeSettingsForm(downloadSettings);
|
writeSettingsForm(downloadSettings);
|
||||||
|
renderDiagnosticControls();
|
||||||
renderSaveDestination();
|
renderSaveDestination();
|
||||||
showSettingsStatus("Настройки сохранены.");
|
showSettingsStatus("Настройки сохранены.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -487,13 +510,20 @@ groupByProfileInput.addEventListener("change", renderSettingsPathPreview);
|
|||||||
saveAsInput.addEventListener("change", renderSettingsPathPreview);
|
saveAsInput.addEventListener("change", renderSettingsPathPreview);
|
||||||
settingsSaveButton.addEventListener("click", saveDownloadSettings);
|
settingsSaveButton.addEventListener("click", saveDownloadSettings);
|
||||||
settingsResetButton.addEventListener("click", () => {
|
settingsResetButton.addEventListener("click", () => {
|
||||||
|
diagnosticsEnabled = false;
|
||||||
writeSettingsForm(DEFAULT_DOWNLOAD_SETTINGS);
|
writeSettingsForm(DEFAULT_DOWNLOAD_SETTINGS);
|
||||||
|
diagnosticsEnabledInput.checked = false;
|
||||||
|
renderDiagnosticControls();
|
||||||
showSettingsStatus("Значения сброшены — нажмите «Сохранить».");
|
showSettingsStatus("Значения сброшены — нажмите «Сохранить».");
|
||||||
});
|
});
|
||||||
diagnosticButton.addEventListener("click", async () => {
|
diagnosticButton.addEventListener("click", async () => {
|
||||||
|
if (!diagnosticsEnabled) {
|
||||||
|
renderDiagnosticControls();
|
||||||
|
return;
|
||||||
|
}
|
||||||
diagnosticButton.disabled = true;
|
diagnosticButton.disabled = true;
|
||||||
diagnosticStatus.className = "diagnostic-status";
|
diagnosticStatus.className = "diagnostic-status";
|
||||||
diagnosticStatus.textContent = "Собираю DOM, события и сетевые ошибки…";
|
diagnosticStatus.textContent = "Собираю диагностический лог…";
|
||||||
try {
|
try {
|
||||||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
const tab = tabs && tabs[0];
|
const tab = tabs && tabs[0];
|
||||||
@@ -553,7 +583,7 @@ diagnosticButton.addEventListener("click", async () => {
|
|||||||
diagnosticStatus.className = "diagnostic-status error";
|
diagnosticStatus.className = "diagnostic-status error";
|
||||||
diagnosticStatus.textContent = error && error.message ? error.message : String(error);
|
diagnosticStatus.textContent = error && error.message ? error.message : String(error);
|
||||||
} finally {
|
} finally {
|
||||||
diagnosticButton.disabled = false;
|
diagnosticButton.disabled = !diagnosticsEnabled;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
alternativeToggle.addEventListener("click", async () => {
|
alternativeToggle.addEventListener("click", async () => {
|
||||||
@@ -573,10 +603,13 @@ alternativeToggle.addEventListener("click", async () => {
|
|||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
writeSettingsForm(downloadSettings);
|
writeSettingsForm(downloadSettings);
|
||||||
|
renderDiagnosticControls();
|
||||||
try {
|
try {
|
||||||
const stored = await chrome.storage.local.get("downloadSettings");
|
const stored = await chrome.storage.local.get(["downloadSettings", "diagnosticsEnabled"]);
|
||||||
downloadSettings = Object.assign({}, DEFAULT_DOWNLOAD_SETTINGS, stored.downloadSettings || {});
|
downloadSettings = Object.assign({}, DEFAULT_DOWNLOAD_SETTINGS, stored.downloadSettings || {});
|
||||||
|
diagnosticsEnabled = Boolean(stored.diagnosticsEnabled);
|
||||||
writeSettingsForm(downloadSettings);
|
writeSettingsForm(downloadSettings);
|
||||||
|
renderDiagnosticControls();
|
||||||
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
const tab = tabs && tabs[0];
|
const tab = tabs && tabs[0];
|
||||||
if (tab && /^https:\/\/(?:[^/]+\.)?redgifs\.com\//i.test(tab.url || "")) {
|
if (tab && /^https:\/\/(?:[^/]+\.)?redgifs\.com\//i.test(tab.url || "")) {
|
||||||
@@ -593,8 +626,15 @@ alternativeToggle.addEventListener("click", async () => {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
chrome.storage.onChanged.addListener((changes, areaName) => {
|
chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||||
if (areaName !== "local" || !changes.downloadSettings) return;
|
if (areaName !== "local") return;
|
||||||
downloadSettings = Object.assign({}, DEFAULT_DOWNLOAD_SETTINGS, changes.downloadSettings.newValue || {});
|
if (changes.downloadSettings) {
|
||||||
writeSettingsForm(downloadSettings);
|
downloadSettings = Object.assign({}, DEFAULT_DOWNLOAD_SETTINGS, changes.downloadSettings.newValue || {});
|
||||||
renderSaveDestination();
|
writeSettingsForm(downloadSettings);
|
||||||
|
renderSaveDestination();
|
||||||
|
}
|
||||||
|
if (changes.diagnosticsEnabled) {
|
||||||
|
diagnosticsEnabled = Boolean(changes.diagnosticsEnabled.newValue);
|
||||||
|
diagnosticsEnabledInput.checked = diagnosticsEnabled;
|
||||||
|
renderDiagnosticControls();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user