// ==UserScript== // @name Return YouTube Dislike // @version 3.0.0.18 // @description Returns ability to see dislikes // @namespace return-youtube-dislike // @author Converter Script // @match *://youtube.com/* // @match *://www.youtube.com/* // @match *://m.youtube.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_openInTab // @icon data:image/png;base64,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 // @run-at document-idle // ==/UserScript== console.log("Script start:",performance.now());const e=!0,t=e=>e,o="passthrough";let s,c={createHTML:t,createScript:t,createScriptURL:t},i=!1;const r=()=>{try{void 0!==window.isSecureContext&&window.isSecureContext&&window.trustedTypes&&window.trustedTypes.createPolicy&&(i=!0,trustedTypes.defaultPolicy?(l("TT Default Policy exists"),c=window.trustedTypes.createPolicy("default",c),s=trustedTypes.defaultPolicy,l(`Created custom passthrough policy, in case the default policy is too restrictive: Use Policy '${o}' in var 'TTP':`,c)):s=c=window.trustedTypes.createPolicy("default",c),l("Trusted-Type Policies: TTP:",c,"TTP_default:",s))}catch(e){l(e)}},l=(...e)=>{console.log(...e)};r(); (function() { // #region Logging const SCRIPT_NAME = "Return YouTube Dislike"; const _log = (...args) => {}; const _warn = (...args) => console.warn(`[${typeof SCRIPT_NAME === 'string' ? SCRIPT_NAME : '[USERSCRIPT_CONVERTED]'}]`, ...args); const _error = (...args) => { let e = args[0]; console.error(`[${typeof SCRIPT_NAME === 'string' ? SCRIPT_NAME : '[USERSCRIPT_CONVERTED]'}]`, ...args); } // #endregion // #region Unified Polyfill // #region Messaging implementation function createEventBus( scopeId, type = "page", // "page" or "iframe" { allowedOrigin = "*", children = [], parentWindow = null } = {} ) { if (!scopeId) throw new Error("createEventBus requires a scopeId"); const handlers = {}; function handleIncoming(ev) { if (allowedOrigin !== "*" && ev.origin !== allowedOrigin) return; const msg = ev.data; if (!msg || msg.__eventBus !== true || msg.scopeId !== scopeId) return; const { event, payload } = msg; // PAGE: if it's an INIT from an iframe, adopt it if (type === "page" && event === "__INIT__") { const win = ev.source; if (win && !children.includes(win)) { children.push(win); } return; } (handlers[event] || []).forEach((fn) => fn(payload, { origin: ev.origin, source: ev.source }) ); } window.addEventListener("message", handleIncoming); function emitTo(win, event, payload) { const envelope = { __eventBus: true, scopeId, event, payload, }; win.postMessage(envelope, allowedOrigin); } // IFRAME: announce to page on startup if (type === "iframe") { setTimeout(() => { const pw = parentWindow || window.parent; if (pw && pw.postMessage) { emitTo(pw, "__INIT__", null); } }, 0); } return { on(event, fn) { handlers[event] = handlers[event] || []; handlers[event].push(fn); }, off(event, fn) { if (!handlers[event]) return; handlers[event] = handlers[event].filter((h) => h !== fn); }, /** * Emits an event. * @param {string} event - The event name. * @param {any} payload - The event payload. * @param {object} [options] - Emission options. * @param {Window} [options.to] - A specific window to target. If provided, message is ONLY sent to the target. */ emit(event, payload, { to } = {}) { // If a specific target window is provided, send only to it and DO NOT dispatch locally. // This prevents a port from receiving its own messages. if (to) { if (to && typeof to.postMessage === "function") { emitTo(to, event, payload); } return; // Exit after targeted send. } // For broadcast messages (no 'to' target), dispatch locally first. (handlers[event] || []).forEach((fn) => fn(payload, { origin: location.origin, source: window }) ); // Then propagate the broadcast to other windows. if (type === "page") { children.forEach((win) => emitTo(win, event, payload)); } else { const pw = parentWindow || window.parent; if (pw && pw.postMessage) { emitTo(pw, event, payload); } } }, }; } function createRuntime(type = "background", bus) { let nextId = 1; const pending = {}; const msgListeners = []; let nextPortId = 1; const ports = {}; const onConnectListeners = []; function parseArgs(args) { let target, message, options, callback; const arr = [...args]; if (arr.length === 0) { throw new Error("sendMessage requires at least one argument"); } if (arr.length === 1) { return { message: arr[0] }; } // last object could be options if ( arr.length && typeof arr[arr.length - 1] === "object" && !Array.isArray(arr[arr.length - 1]) ) { options = arr.pop(); } // last function is callback if (arr.length && typeof arr[arr.length - 1] === "function") { callback = arr.pop(); } if ( arr.length === 2 && (typeof arr[0] === "string" || typeof arr[0] === "number") ) { [target, message] = arr; } else { [message] = arr; } return { target, message, options, callback }; } if (type === "background") { bus.on("__REQUEST__", ({ id, message }, { source }) => { let responded = false, isAsync = false; function sendResponse(resp) { if (responded) return; responded = true; // Target the response directly back to the window that sent the request. bus.emit("__RESPONSE__", { id, response: resp }, { to: source }); } const results = msgListeners .map((fn) => { try { // msg, sender, sendResponse const ret = fn(message, { id, tab: { id: source } }, sendResponse); if (ret === true || (ret && typeof ret.then === "function")) { isAsync = true; return ret; } return ret; } catch (e) { _error(e); } }) .filter((r) => r !== undefined); const promises = results.filter((r) => r && typeof r.then === "function"); if (!isAsync && promises.length === 0) { const out = results.length === 1 ? results[0] : results; sendResponse(out); } else if (promises.length) { Promise.all(promises).then((vals) => { if (!responded) { const out = vals.length === 1 ? vals[0] : vals; sendResponse(out); } }); } }); } if (type !== "background") { bus.on("__RESPONSE__", ({ id, response }) => { const entry = pending[id]; if (!entry) return; entry.resolve(response); if (entry.callback) entry.callback(response); delete pending[id]; }); } function sendMessage(...args) { // Background should be able to send message to itself // if (type === "background") { // throw new Error("Background cannot sendMessage to itself"); // } const { target, message, callback } = parseArgs(args); const id = nextId++; const promise = new Promise((resolve) => { pending[id] = { resolve, callback }; bus.emit("__REQUEST__", { id, message }); }); return promise; } bus.on("__PORT_CONNECT__", ({ portId, name }, { source }) => { if (type !== "background") return; const backgroundPort = makePort("background", portId, name, source); ports[portId] = backgroundPort; onConnectListeners.forEach((fn) => fn(backgroundPort)); // send back a CONNECT_ACK so the client can // start listening on its end: bus.emit("__PORT_CONNECT_ACK__", { portId, name }, { to: source }); }); // Clients handle the ACK and finalize their Port object by learning the remote window. bus.on("__PORT_CONNECT_ACK__", ({ portId, name }, { source }) => { if (type === "background") return; // ignore const p = ports[portId]; if (!p) return; // Call the port's internal finalize method to complete the handshake if (p._finalize) { p._finalize(source); } }); // Any port message travels via "__PORT_MESSAGE__" bus.on("__PORT_MESSAGE__", (envelope, { source }) => { const { portId } = envelope; const p = ports[portId]; if (!p) return; p._receive(envelope, source); }); // Any port disconnect: bus.on("__PORT_DISCONNECT__", ({ portId }) => { const p = ports[portId]; if (!p) return; p._disconnect(); delete ports[portId]; }); // Refactored makePort to correctly manage internal state and the connection handshake. function makePort(side, portId, name, remoteWindow) { let onMessageHandlers = []; let onDisconnectHandlers = []; let buffer = []; // Unique instance ID for this port instance const instanceId = Math.random().toString(36).slice(2) + Date.now(); // These state variables are part of the closure and are updated by _finalize let _ready = side === "background"; function _drainBuffer() { buffer.forEach((m) => _post(m)); buffer = []; } function _post(msg) { // Always use the 'to' parameter for port messages, making them directional. // Include senderInstanceId bus.emit( "__PORT_MESSAGE__", { portId, msg, senderInstanceId: instanceId }, { to: remoteWindow } ); } function postMessage(msg) { if (!_ready) { buffer.push(msg); } else { _post(msg); } } function _receive(envelope, source) { // envelope: { msg, senderInstanceId } if (envelope.senderInstanceId === instanceId) return; // Don't dispatch to self onMessageHandlers.forEach((fn) => fn(envelope.msg, { id: portId, tab: { id: source } }) ); } function disconnect() { // Also use the 'to' parameter for disconnect messages bus.emit("__PORT_DISCONNECT__", { portId }, { to: remoteWindow }); _disconnect(); delete ports[portId]; } function _disconnect() { onDisconnectHandlers.forEach((fn) => fn()); onMessageHandlers = []; onDisconnectHandlers = []; } // This function is called on the client port when the ACK is received from background. // It updates the port's state, completing the connection. function _finalize(win) { remoteWindow = win; // <-- This is the crucial part: learn the destination _ready = true; _drainBuffer(); } return { name, sender: { id: portId, }, onMessage: { addListener(fn) { onMessageHandlers.push(fn); }, removeListener(fn) { onMessageHandlers = onMessageHandlers.filter((x) => x !== fn); }, }, onDisconnect: { addListener(fn) { onDisconnectHandlers.push(fn); }, removeListener(fn) { onDisconnectHandlers = onDisconnectHandlers.filter((x) => x !== fn); }, }, postMessage, disconnect, // Internal methods used by the runtime _receive, _disconnect, _finalize, // Expose the finalizer for the ACK handler }; } function connect(connectInfo = {}) { if (type === "background") { throw new Error("Background must use onConnect, not connect()"); } const name = connectInfo.name || ""; const portId = nextPortId++; // create the client side port // remoteWindow is initially null; it will be set by _finalize upon ACK. const clientPort = makePort("client", portId, name, null); ports[portId] = clientPort; // fire the connect event across the bus bus.emit("__PORT_CONNECT__", { portId, name }); return clientPort; } function onConnect(fn) { if (type !== "background") { throw new Error("connect event only fires in background"); } onConnectListeners.push(fn); } return { // rpc: sendMessage, onMessage: { addListener(fn) { msgListeners.push(fn); }, removeListener(fn) { const i = msgListeners.indexOf(fn); if (i >= 0) msgListeners.splice(i, 1); }, }, // port API: connect, onConnect: { addListener(fn) { onConnect(fn); }, removeListener(fn) { const i = onConnectListeners.indexOf(fn); if (i >= 0) onConnectListeners.splice(i, 1); }, }, }; } // #region Abstraction layer Handle postmesage for (function () { const pendingRequests = new Map(); // requestId -> { resolve, reject, timeout } let nextRequestId = 1; window.addEventListener("message", async (event) => { const { type, requestId, method, args } = event.data; if (type === "abstraction-request") { try { let result; switch (method) { case "_storageSet": result = await _storageSet(args[0]); break; case "_storageGet": result = await _storageGet(args[0]); break; case "_storageRemove": result = await _storageRemove(args[0]); break; case "_storageClear": result = await _storageClear(); break; case "_cookieList": result = await _cookieList(args[0]); break; case "_cookieSet": result = await _cookieSet(args[0]); break; case "_cookieDelete": result = await _cookieDelete(args[0]); break; case "_fetch": result = await _fetch(args[0], args[1]); break; case "_registerMenuCommand": result = _registerMenuCommand(args[0], args[1]); break; case "_openTab": result = _openTab(args[0], args[1]); break; case "_initStorage": result = await _initStorage(); break; default: throw new Error(`Unknown abstraction method: ${method}`); } event.source.postMessage({ type: "abstraction-response", requestId, success: true, result, }); } catch (error) { event.source.postMessage({ type: "abstraction-response", requestId, success: false, error: { message: error.message, stack: error.stack, }, }); } } }); _log("[PostMessage Handler] Abstraction layer message handler initialized"); })(); // #endregion // #region Abstraction Layer Userscript Target async function _storageSet(items) { try { for (const key in items) { if (items.hasOwnProperty(key)) { await GM_setValue(key, items[key]); } } return Promise.resolve(); } catch (e) { _error("GM_setValue error:", e); return Promise.reject(e); } } async function _storageGet(keys) { if (!keys) { keys = null; } if ( Array.isArray(keys) && (keys.length === 0 || [null, undefined].includes(keys[0])) ) { keys = null; } try { const results = {}; let keyList = []; let defaults = {}; let requestedKeys = []; if (keys === null) { keyList = await GM_listValues(); requestedKeys = [...keyList]; } else if (typeof keys === "string") { keyList = [keys]; requestedKeys = [keys]; } else if (Array.isArray(keys)) { keyList = keys; requestedKeys = [...keys]; } else if (typeof keys === "object" && keys !== null) { keyList = Object.keys(keys); requestedKeys = [...keyList]; defaults = keys; } else { _error("_storageGet error: Invalid keys format", keys); return Promise.reject(new Error("Invalid keys format for get")); } for (const key of keyList) { const defaultValue = defaults.hasOwnProperty(key) ? defaults[key] : undefined; const storedValue = await GM_getValue(key, defaultValue); results[key] = storedValue; } const finalResult = {}; for (const key of requestedKeys) { if (results.hasOwnProperty(key)) { finalResult[key] = results[key]; } else if (defaults.hasOwnProperty(key)) { finalResult[key] = defaults[key]; } } return Promise.resolve(finalResult); } catch (e) { _error("GM_getValue/GM_listValues error:", e); return Promise.reject(e); } } async function _storageRemove(keysToRemove) { try { let keyList = []; if (typeof keysToRemove === "string") { keyList = [keysToRemove]; } else if (Array.isArray(keysToRemove)) { keyList = keysToRemove; } else { _error("_storageRemove error: Invalid keys format", keysToRemove); return Promise.reject(new Error("Invalid keys format for remove")); } for (const key of keyList) { await GM_deleteValue(key); } return Promise.resolve(); } catch (e) { _error("GM_deleteValue error:", e); return Promise.reject(e); } } async function _storageClear() { try { const keys = await GM_listValues(); await Promise.all(keys.map((key) => GM_deleteValue(key))); return Promise.resolve(); } catch (e) { _error("GM_listValues/GM_deleteValue error during clear:", e); return Promise.reject(e); } } async function _cookieList(details) { return new Promise((resolve, reject) => { if (typeof GM_cookie === "undefined" || !GM_cookie.list) { return reject(new Error("GM_cookie.list is not available.")); } GM_cookie.list(details, (cookies, error) => { if (error) { return reject(new Error(error)); } resolve(cookies); }); }); } async function _cookieSet(details) { return new Promise((resolve, reject) => { if (typeof GM_cookie === "undefined" || !GM_cookie.set) { return reject(new Error("GM_cookie.set is not available.")); } GM_cookie.set(details, (error) => { if (error) { return reject(new Error(error)); } resolve(); }); }); } async function _cookieDelete(details) { return new Promise((resolve, reject) => { if (typeof GM_cookie === "undefined" || !GM_cookie.delete) { return reject(new Error("GM_cookie.delete is not available.")); } GM_cookie.delete(details, (error) => { if (error) { return reject(new Error(error)); } resolve(); }); }); } async function _fetch(url, options = {}) { return new Promise((resolve, reject) => { try { GM_xmlhttpRequest({ method: options.method || "GET", url: url, headers: options.headers || {}, data: options.body, responseType: options.responseType, timeout: options.timeout || 0, binary: options.responseType === "blob" || options.responseType === "arraybuffer", onload: function (response) { const responseHeaders = {}; if (response.responseHeaders) { response.responseHeaders .trim() .split("\\r\\n") .forEach((header) => { const parts = header.match(/^([^:]+):\s*(.*)$/); if (parts && parts.length === 3) { responseHeaders[parts[1].toLowerCase()] = parts[2]; } }); } const mockResponse = { ok: response.status >= 200 && response.status < 300, status: response.status, statusText: response.statusText || (response.status >= 200 && response.status < 300 ? "OK" : ""), url: response.finalUrl || url, headers: new Headers(responseHeaders), text: () => Promise.resolve(response.responseText), json: () => { try { return Promise.resolve(JSON.parse(response.responseText)); } catch (e) { return Promise.reject(new SyntaxError("Could not parse JSON")); } }, blob: () => { if (response.response instanceof Blob) { return Promise.resolve(response.response); } return Promise.reject( new Error("Requires responseType:'blob' in GM_xmlhttpRequest") ); }, arrayBuffer: () => { if (response.response instanceof ArrayBuffer) { return Promise.resolve(response.response); } return Promise.reject( new Error( "Requires responseType:'arraybuffer' in GM_xmlhttpRequest" ) ); }, clone: function () { const cloned = { ...this }; cloned.text = () => Promise.resolve(response.responseText); cloned.json = () => this.json(); cloned.blob = () => this.blob(); cloned.arrayBuffer = () => this.arrayBuffer(); return cloned; }, }; if (mockResponse.ok) { resolve(mockResponse); } else { const error = new Error(`HTTP error! status: ${response.status}`); error.response = mockResponse; reject(error); } }, onerror: function (response) { reject( new Error( `GM_xmlhttpRequest network error: ${ response.statusText || "Unknown Error" }` ) ); }, onabort: function () { reject(new Error("GM_xmlhttpRequest aborted")); }, ontimeout: function () { reject(new Error("GM_xmlhttpRequest timed out")); }, }); } catch (e) { _error("_fetch (GM_xmlhttpRequest) error:", e); reject(e); } }); } function _registerMenuCommand(name, func) { if (typeof GM_registerMenuCommand === "function") { try { GM_registerMenuCommand(name, func); } catch (e) { _error("GM_registerMenuCommand failed:", e); } } else { _warn("GM_registerMenuCommand not available."); } } function _openTab(url, active) { if (typeof GM_openInTab === "function") { try { GM_openInTab(url, { loadInBackground: !active }); } catch (e) { _error("GM_openInTab failed:", e); } } else { _warn("GM_openInTab not available, using window.open as fallback."); try { window.open(url); } catch (e) { _error("window.open fallback failed:", e); } } } async function _initStorage() { return Promise.resolve(); } const EXTENSION_ASSETS_MAP = { "popup.html": "\r\n\r\n\r\n \r\n \r\n \r\n Return YouTube Dislike\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n \r\n

\r\n Return YouTube Dislike\r\n

\r\n

\r\n by Dmitry Selivanov & Community\r\n

\r\n\r\n \r\n \r\n Donate\r\n \r\n \r\n
\r\n \r\n \r\n \r\n
\r\n \r\n Change Log\r\n \r\n\r\n
\r\n
\r\n

API Status:

\r\n \r\n\r\n
\r\n
\r\n
\r\n\r\n \r\n \r\n \r\n
\r\n \r\n \r\n
\r\n\r\n \r\n
\r\n \r\n \r\n
\r\n \r\n
\r\n
\r\n \r\n \r\n
\r\n
\r\n
\r\n \r\n
\r\n \r\n
\r\n
\r\n \r\n \r\n  \r\n  \r\n
\r\n
\r\n \r\n \r\n \r\n Percentage in like/dislike bar tooltip.\r\n \r\n
\r\n Percent mode:\r\n \r\n
\r\n
\r\n \r\n \r\n\r\n" }; // #endregion // #endregion // #region Polyfill Implementation function buildPolyfill({ isBackground = false, isOtherPage = false } = {}) { // Generate a unique context ID for this polyfill instance const contextType = isBackground ? "background" : isOtherPage ? "options" : "content"; const contextId = `${contextType}_${Math.random() .toString(36) .substring(2, 15)}`; const IS_IFRAME = "false" === "true"; const BUS = (function () { if (globalThis.__BUS) { return globalThis.__BUS; } globalThis.__BUS = createEventBus( "return-youtube-dislike", IS_IFRAME ? "iframe" : "page", ); return globalThis.__BUS; })(); const RUNTIME = createRuntime(isBackground ? "background" : "tab", BUS); const createNoopListeners = () => ({ addListener: (callback) => { _log("addListener", callback); }, removeListener: (callback) => { _log("removeListener", callback); }, }); // TODO: Stub const storageChangeListeners = new Set(); function broadcastStorageChange(changes, areaName) { storageChangeListeners.forEach((listener) => { listener(changes, areaName); }); } let REQ_PERMS = []; // #region Chrome polyfill let chrome = { extension: { isAllowedIncognitoAccess: () => Promise.resolve(true), sendMessage: (...args) => _messagingHandler.sendMessage(...args), }, permissions: { // TODO: Remove origin permission means exclude from origin in startup (when checking for content scripts) request: (permissions, callback) => { _log("permissions.request", permissions, callback); if (Array.isArray(permissions)) { REQ_PERMS = [...REQ_PERMS, ...permissions]; } if (typeof callback === "function") { callback(permissions); } return Promise.resolve(permissions); }, contains: (permissions, callback) => { if (typeof callback === "function") { callback(true); } return Promise.resolve(true); }, getAll: () => { return Promise.resolve({ permissions: EXTENSION_PERMISSIONS, origins: ORIGIN_PERMISSIONS, }); }, onAdded: createNoopListeners(), onRemoved: createNoopListeners(), }, i18n: { getUILanguage: () => { return USED_LOCALE || "en"; }, getMessage: (key, substitutions = []) => { if (typeof substitutions === "string") { substitutions = [substitutions]; } if (typeof LOCALE_KEYS !== "undefined" && LOCALE_KEYS[key]) { return LOCALE_KEYS[key].message?.replace( /\$(\d+)/g, (match, p1) => substitutions[p1 - 1] || match, ); } return key; }, }, alarms: { onAlarm: createNoopListeners(), create: () => { _log("alarms.create", arguments); }, get: () => { _log("alarms.get", arguments); }, }, runtime: { ...RUNTIME, onInstalled: createNoopListeners(), onStartup: createNoopListeners(), // TODO: Postmessage to parent to open options page or call openOptionsPage openOptionsPage: () => { // const url = chrome.runtime.getURL(OPTIONS_PAGE_PATH); // console.log("openOptionsPage", _openTab, url, EXTENSION_ASSETS_MAP); // _openTab(url); if (typeof openOptionsPage === "function") { openOptionsPage(); } else if (window.parent) { window.parent.postMessage({ type: "openOptionsPage" }, "*"); } else { _warn("openOptionsPage not available."); } }, getManifest: () => { // The manifest object will be injected into the scope where buildPolyfill is called if (typeof INJECTED_MANIFEST !== "undefined") { return JSON.parse(JSON.stringify(INJECTED_MANIFEST)); // Return deep copy } _warn("INJECTED_MANIFEST not found for chrome.runtime.getManifest"); return { name: "Unknown", version: "0.0", manifest_version: 2 }; }, getURL: (path) => { if (!path) return ""; if (path.startsWith("/")) { path = path.substring(1); } if (typeof _createAssetUrl === "function") { return _createAssetUrl(path); } _warn( `chrome.runtime.getURL fallback for '${path}'. Assets may not be available.`, ); // Attempt a relative path resolution (highly context-dependent and likely wrong) try { if (window.location.protocol.startsWith("http")) { return new URL(path, window.location.href).toString(); } } catch (e) { /* ignore error, fallback */ } return path; }, id: "polyfilled-extension-" + Math.random().toString(36).substring(2, 15), lastError: null, setUninstallURL: () => {}, setUpdateURL: () => {}, getPlatformInfo: async () => { const platform = { os: "unknown", arch: "unknown", nacl_arch: "unknown", }; if (typeof navigator !== "undefined") { const userAgent = navigator.userAgent.toLowerCase(); if (userAgent.includes("mac")) platform.os = "mac"; else if (userAgent.includes("win")) platform.os = "win"; else if (userAgent.includes("linux")) platform.os = "linux"; else if (userAgent.includes("android")) platform.os = "android"; else if (userAgent.includes("ios")) platform.os = "ios"; if (userAgent.includes("x86_64") || userAgent.includes("amd64")) { platform.arch = "x86-64"; } else if (userAgent.includes("i386") || userAgent.includes("i686")) { platform.arch = "x86-32"; } else if (userAgent.includes("arm")) { platform.arch = "arm"; } } return platform; }, getBrowserInfo: async () => { const info = { name: "unknown", version: "unknown", buildID: "unknown", }; if (typeof navigator !== "undefined") { const userAgent = navigator.userAgent; if (userAgent.includes("Chrome")) { info.name = "Chrome"; const match = userAgent.match(/Chrome\/([0-9.]+)/); if (match) info.version = match[1]; } else if (userAgent.includes("Firefox")) { info.name = "Firefox"; const match = userAgent.match(/Firefox\/([0-9.]+)/); if (match) info.version = match[1]; } else if (userAgent.includes("Safari")) { info.name = "Safari"; const match = userAgent.match(/Version\/([0-9.]+)/); if (match) info.version = match[1]; } } return info; }, }, storage: { local: { get: function (keys, callback) { if (typeof _storageGet !== "function") throw new Error("_storageGet not defined"); const promise = _storageGet(keys); if (typeof callback === "function") { promise .then((result) => { try { callback(result); } catch (e) { _error("Error in storage.get callback:", e); } }) .catch((error) => { _error("Storage.get error:", error); callback({}); }); return; } return promise; }, set: function (items, callback) { if (typeof _storageSet !== "function") throw new Error("_storageSet not defined"); const promise = _storageSet(items).then((result) => { broadcastStorageChange(items, "local"); return result; }); if (typeof callback === "function") { promise .then((result) => { try { callback(result); } catch (e) { _error("Error in storage.set callback:", e); } }) .catch((error) => { _error("Storage.set error:", error); callback(); }); return; } return promise; }, remove: function (keys, callback) { if (typeof _storageRemove !== "function") throw new Error("_storageRemove not defined"); const promise = _storageRemove(keys).then((result) => { const changes = {}; const keyList = Array.isArray(keys) ? keys : [keys]; keyList.forEach((key) => { changes[key] = { oldValue: undefined, newValue: undefined }; }); broadcastStorageChange(changes, "local"); return result; }); if (typeof callback === "function") { promise .then((result) => { try { callback(result); } catch (e) { _error("Error in storage.remove callback:", e); } }) .catch((error) => { _error("Storage.remove error:", error); callback(); }); return; } return promise; }, clear: function (callback) { if (typeof _storageClear !== "function") throw new Error("_storageClear not defined"); const promise = _storageClear().then((result) => { broadcastStorageChange({}, "local"); return result; }); if (typeof callback === "function") { promise .then((result) => { try { callback(result); } catch (e) { _error("Error in storage.clear callback:", e); } }) .catch((error) => { _error("Storage.clear error:", error); callback(); }); return; } return promise; }, onChanged: { addListener: (callback) => { storageChangeListeners.add(callback); }, removeListener: (callback) => { storageChangeListeners.delete(callback); }, }, }, sync: { get: function (keys, callback) { _warn("chrome.storage.sync polyfill maps to local"); return chrome.storage.local.get(keys, callback); }, set: function (items, callback) { _warn("chrome.storage.sync polyfill maps to local"); const promise = chrome.storage.local.set(items).then((result) => { broadcastStorageChange(items, "sync"); return result; }); if (typeof callback === "function") { promise .then((result) => { try { callback(result); } catch (e) { _error("Error in storage.sync.set callback:", e); } }) .catch((error) => { _error("Storage.sync.set error:", error); callback(); }); return; } return promise; }, remove: function (keys, callback) { _warn("chrome.storage.sync polyfill maps to local"); const promise = chrome.storage.local.remove(keys).then((result) => { const changes = {}; const keyList = Array.isArray(keys) ? keys : [keys]; keyList.forEach((key) => { changes[key] = { oldValue: undefined, newValue: undefined }; }); broadcastStorageChange(changes, "sync"); return result; }); if (typeof callback === "function") { promise .then((result) => { try { callback(result); } catch (e) { _error("Error in storage.sync.remove callback:", e); } }) .catch((error) => { _error("Storage.sync.remove error:", error); callback(); }); return; } return promise; }, clear: function (callback) { _warn("chrome.storage.sync polyfill maps to local"); const promise = chrome.storage.local.clear().then((result) => { broadcastStorageChange({}, "sync"); return result; }); if (typeof callback === "function") { promise .then((result) => { try { callback(result); } catch (e) { _error("Error in storage.sync.clear callback:", e); } }) .catch((error) => { _error("Storage.sync.clear error:", error); callback(); }); return; } return promise; }, onChanged: { addListener: (callback) => { storageChangeListeners.add(callback); }, removeListener: (callback) => { storageChangeListeners.delete(callback); }, }, }, onChanged: { addListener: (callback) => { storageChangeListeners.add(callback); }, removeListener: (callback) => { storageChangeListeners.delete(callback); }, }, managed: { get: function (keys, callback) { _warn("chrome.storage.managed polyfill is read-only empty."); const promise = Promise.resolve({}); if (typeof callback === "function") { promise.then((result) => { try { callback(result); } catch (e) { _error("Error in storage.managed.get callback:", e); } }); return; } return promise; }, }, }, cookies: (function () { const cookieChangeListeners = new Set(); function broadcastCookieChange(changeInfo) { cookieChangeListeners.forEach((listener) => { try { listener(changeInfo); } catch (e) { _error("Error in cookies.onChanged listener:", e); } }); } function handlePromiseCallback(promise, callback) { if (typeof callback === "function") { promise .then((result) => callback(result)) .catch((error) => { // chrome.runtime.lastError = { message: error.message }; // TODO: Implement lastError _error(error); callback(); // Call with undefined on error }); return; } return promise; } return { get: function (details, callback) { if (typeof _cookieList !== "function") { return handlePromiseCallback( Promise.reject(new Error("_cookieList not defined")), callback, ); } const promise = _cookieList({ url: details.url, name: details.name, storeId: details.storeId, partitionKey: details.partitionKey, }).then((cookies) => { if (!cookies || cookies.length === 0) { return null; } // Sort by path length (longest first), then creation time (earliest first, if available) cookies.sort((a, b) => { const pathLenDiff = (b.path || "").length - (a.path || "").length; if (pathLenDiff !== 0) return pathLenDiff; return (a.creationTime || 0) - (b.creationTime || 0); }); return cookies[0]; }); return handlePromiseCallback(promise, callback); }, getAll: function (details, callback) { if (typeof _cookieList !== "function") { return handlePromiseCallback( Promise.reject(new Error("_cookieList not defined")), callback, ); } if (details.partitionKey) { _warn( "cookies.getAll: partitionKey is not fully supported in this environment.", ); } const promise = _cookieList(details); return handlePromiseCallback(promise, callback); }, set: function (details, callback) { const promise = (async () => { if ( typeof _cookieSet !== "function" || typeof _cookieList !== "function" ) { throw new Error("_cookieSet or _cookieList not defined"); } if (details.partitionKey) { _warn( "cookies.set: partitionKey is not fully supported in this environment.", ); } const getDetails = { url: details.url, name: details.name, storeId: details.storeId, }; const oldCookies = await _cookieList(getDetails); const oldCookie = oldCookies && oldCookies[0]; if (oldCookie) { broadcastCookieChange({ cause: "overwrite", cookie: oldCookie, removed: true, }); } await _cookieSet(details); const newCookies = await _cookieList(getDetails); const newCookie = newCookies && newCookies[0]; if (newCookie) { broadcastCookieChange({ cause: "explicit", cookie: newCookie, removed: false, }); } return newCookie || null; })(); return handlePromiseCallback(promise, callback); }, remove: function (details, callback) { const promise = (async () => { if ( typeof _cookieDelete !== "function" || typeof _cookieList !== "function" ) { throw new Error("_cookieDelete or _cookieList not defined"); } const oldCookies = await _cookieList(details); const oldCookie = oldCookies && oldCookies[0]; if (!oldCookie) return null; // Nothing to remove await _cookieDelete(details); broadcastCookieChange({ cause: "explicit", cookie: oldCookie, removed: true, }); return { url: details.url, name: details.name, storeId: details.storeId || "0", partitionKey: details.partitionKey, }; })(); return handlePromiseCallback(promise, callback); }, getAllCookieStores: function (callback) { const promise = Promise.resolve([ { id: "0", tabIds: [1] }, // Mock store for the current context ]); return handlePromiseCallback(promise, callback); }, getPartitionKey: function (details, callback) { _warn( "chrome.cookies.getPartitionKey is not supported in this environment.", ); const promise = Promise.resolve({ partitionKey: {} }); // Return empty partition key return handlePromiseCallback(promise, callback); }, onChanged: { addListener: (callback) => { if (typeof callback === "function") { cookieChangeListeners.add(callback); } }, removeListener: (callback) => { cookieChangeListeners.delete(callback); }, }, }; })(), tabs: { query: async (queryInfo) => { _warn("chrome.tabs.query polyfill only returns current tab info."); const dummyId = Math.floor(Math.random() * 1000) + 1; return [ { id: dummyId, url: CURRENT_LOCATION, active: true, windowId: 1, status: "complete", }, ]; }, create: async ({ url, active = true }) => { _log(`[Polyfill tabs.create] URL: ${url}`); if (typeof _openTab !== "function") throw new Error("_openTab not defined"); _openTab(url, active); const dummyId = Math.floor(Math.random() * 1000) + 1001; return Promise.resolve({ id: dummyId, url: url, active, windowId: 1, }); }, sendMessage: async (tabId, message) => { _warn( `chrome.tabs.sendMessage polyfill (to tab ${tabId}) redirects to runtime.sendMessage (current context).`, ); return chrome.runtime.sendMessage(message); }, onActivated: createNoopListeners(), onUpdated: createNoopListeners(), onRemoved: createNoopListeners(), onReplaced: createNoopListeners(), onCreated: createNoopListeners(), onMoved: createNoopListeners(), onDetached: createNoopListeners(), onAttached: createNoopListeners(), }, windows: { onFocusChanged: createNoopListeners(), onCreated: createNoopListeners(), onRemoved: createNoopListeners(), onFocused: createNoopListeners(), onFocus: createNoopListeners(), onBlur: createNoopListeners(), onFocused: createNoopListeners(), }, notifications: { create: async (notificationId, options) => { try { let id = notificationId; let notificationOptions = options; if (typeof notificationId === "object" && notificationId !== null) { notificationOptions = notificationId; id = "notification_" + Math.random().toString(36).substring(2, 15); } else if (typeof notificationId === "string" && options) { id = notificationId; notificationOptions = options; } else { throw new Error("Invalid parameters for notifications.create"); } if (!notificationOptions || typeof notificationOptions !== "object") { throw new Error("Notification options must be an object"); } const { title, message, iconUrl, type = "basic", } = notificationOptions; if (!title || !message) { throw new Error("Notification must have title and message"); } if ("Notification" in window) { if (Notification.permission === "granted") { const notification = new Notification(title, { body: message, icon: iconUrl, tag: id, }); _log(`[Notifications] Created notification: ${id}`); return id; } else if (Notification.permission === "default") { const permission = await Notification.requestPermission(); if (permission === "granted") { const notification = new Notification(title, { body: message, icon: iconUrl, tag: id, }); _log( `[Notifications] Created notification after permission: ${id}`, ); return id; } else { _warn("[Notifications] Permission denied for notifications"); return id; } } else { _warn("[Notifications] Notifications are blocked"); return id; } } else { _warn( "[Notifications] Native notifications not supported, using console fallback", ); _log(`[Notification] ${title}: ${message}`); return id; } } catch (error) { _error("[Notifications] Error creating notification:", error.message); throw error; } }, clear: async (notificationId) => { _log(`[Notifications] Clear notification: ${notificationId}`); // For native notifications, there's no direct way to clear by ID // This is a limitation of the Web Notifications API return true; }, getAll: async () => { _warn("[Notifications] getAll not fully supported in polyfill"); return {}; }, getPermissionLevel: async () => { if ("Notification" in window) { const permission = Notification.permission; return { level: permission === "granted" ? "granted" : "denied" }; } return { level: "denied" }; }, }, contextMenus: { create: (createProperties, callback) => { try { if (!createProperties || typeof createProperties !== "object") { throw new Error("Context menu create properties must be an object"); } const { id, title, contexts = ["page"], onclick } = createProperties; const menuId = id || `menu_${Math.random().toString(36).substring(2, 15)}`; if (!title || typeof title !== "string") { throw new Error("Context menu must have a title"); } // Store menu items for potential use if (!window._polyfillContextMenus) { window._polyfillContextMenus = new Map(); } window._polyfillContextMenus.set(menuId, { id: menuId, title, contexts, onclick, enabled: createProperties.enabled !== false, }); _log( `[ContextMenus] Created context menu item: ${title} (${menuId})`, ); // Try to register a menu command as fallback if (typeof _registerMenuCommand === "function") { try { _registerMenuCommand( title, onclick || (() => { _log(`Context menu clicked: ${title}`); }), ); } catch (e) { _warn( "[ContextMenus] Failed to register as menu command:", e.message, ); } } if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } return menuId; } catch (error) { _error("[ContextMenus] Error creating context menu:", error.message); if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } throw error; } }, update: (id, updateProperties, callback) => { try { if ( !window._polyfillContextMenus || !window._polyfillContextMenus.has(id) ) { throw new Error(`Context menu item not found: ${id}`); } const menuItem = window._polyfillContextMenus.get(id); Object.assign(menuItem, updateProperties); _log(`[ContextMenus] Updated context menu item: ${id}`); if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } } catch (error) { _error("[ContextMenus] Error updating context menu:", error.message); if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } } }, remove: (menuItemId, callback) => { try { if ( window._polyfillContextMenus && window._polyfillContextMenus.has(menuItemId) ) { window._polyfillContextMenus.delete(menuItemId); _log(`[ContextMenus] Removed context menu item: ${menuItemId}`); } else { _warn( `[ContextMenus] Context menu item not found for removal: ${menuItemId}`, ); } if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } } catch (error) { _error("[ContextMenus] Error removing context menu:", error.message); if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } } }, removeAll: (callback) => { try { if (window._polyfillContextMenus) { const count = window._polyfillContextMenus.size; window._polyfillContextMenus.clear(); _log(`[ContextMenus] Removed all ${count} context menu items`); } if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } } catch (error) { _error( "[ContextMenus] Error removing all context menus:", error.message, ); if (callback && typeof callback === "function") { setTimeout(() => callback(), 0); } } }, onClicked: { addListener: (callback) => { if (!window._polyfillContextMenuListeners) { window._polyfillContextMenuListeners = new Set(); } window._polyfillContextMenuListeners.add(callback); _log("[ContextMenus] Added click listener"); }, removeListener: (callback) => { if (window._polyfillContextMenuListeners) { window._polyfillContextMenuListeners.delete(callback); _log("[ContextMenus] Removed click listener"); } }, }, }, }; const tc = (fn) => { try { fn(); } catch (e) {} }; const loggingProxyHandler = (_key) => ({ get(target, key, receiver) { tc(() => _log(`[${contextType}] [CHROME - ${_key}] Getting ${key}`)); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { tc(() => _log(`[${contextType}] [CHROME - ${_key}] Setting ${key} to ${value}`), ); return Reflect.set(target, key, value, receiver); }, has(target, key) { tc(() => _log(`[${contextType}] [CHROME - ${_key}] Checking if ${key} exists`), ); return Reflect.has(target, key); }, }); chrome = Object.fromEntries( Object.entries(chrome).map(([key, value]) => [ key, new Proxy(value, loggingProxyHandler(key)), ]), ); // Alias browser to chrome for common Firefox pattern const browser = new Proxy(chrome, loggingProxyHandler); const oldGlobalThis = globalThis; const oldWindow = window; const oldSelf = self; const oldGlobal = globalThis; const __globalsStorage = {}; const TO_MODIFY = [oldGlobalThis, oldWindow, oldSelf, oldGlobal]; const set = (k, v) => { __globalsStorage[k] = v; TO_MODIFY.forEach((target) => { target[k] = v; }); }; const proxyHandler = { get(target, key, receiver) { const fns = [ () => __globalsStorage[key], () => Reflect.get(target, key, target), () => target[key], ]; const out = fns .map((f) => { try { let out = f(); return out; } catch (e) { return undefined; } }) .find((f) => f !== undefined); if (typeof out === "function") { return out.bind(target); } return out; }, set(target, key, value, receiver) { try { tc(() => _log(`[${contextType}] Setting ${key} to ${value}`)); set(key, value); return Reflect.set(target, key, value, receiver); } catch (e) { _error("Error setting", key, value, e); try { target[key] = value; return true; } catch (e) { _error("Error setting", key, value, e); } return false; } }, has(target, key) { try { return key in __globalsStorage || key in target; } catch (e) { _error("Error has", key, e); try { return key in __globalsStorage || key in target; } catch (e) { _error("Error has", key, e); } return false; } }, getOwnPropertyDescriptor(target, key) { try { if (key in __globalsStorage) { return { configurable: true, enumerable: true, writable: true, value: __globalsStorage[key], }; } // fall back to the real globalThis const desc = Reflect.getOwnPropertyDescriptor(target, key); // ensure it's configurable so the with‑scope binding logic can override it if (desc && !desc.configurable) { desc.configurable = true; } return desc; } catch (e) { _error("Error getOwnPropertyDescriptor", key, e); return { configurable: true, enumerable: true, writable: true, value: undefined, }; } }, defineProperty(target, key, descriptor) { try { // Normalize descriptor to avoid mixed accessor & data attributes const hasAccessor = "get" in descriptor || "set" in descriptor; if (hasAccessor) { // Build a clean descriptor without value/writable when accessors present const normalized = { configurable: "configurable" in descriptor ? descriptor.configurable : true, enumerable: "enumerable" in descriptor ? descriptor.enumerable : false, }; if ("get" in descriptor) normalized.get = descriptor.get; if ("set" in descriptor) normalized.set = descriptor.set; // Store accessor references for inspection but avoid breaking invariants set(key, { get: descriptor.get, set: descriptor.set, }); return Reflect.defineProperty(target, key, normalized); } // Data descriptor path set(key, descriptor.value); return Reflect.defineProperty(target, key, descriptor); } catch (e) { _error("Error defineProperty", key, descriptor, e); return false; } }, }; // Create proxies once proxyHandler is defined const proxyWindow = new Proxy(oldWindow, proxyHandler); const proxyGlobalThis = new Proxy(oldGlobalThis, proxyHandler); const proxyGlobal = new Proxy(oldGlobal, proxyHandler); const proxySelf = new Proxy(oldSelf, proxyHandler); // Seed storage with core globals so lookups succeed inside `with` blocks Object.assign(__globalsStorage, { chrome, browser, window: proxyWindow, globalThis: proxyGlobalThis, global: proxyGlobal, self: proxySelf, document: oldWindow.document, }); const __globals = { chrome, browser, window: proxyWindow, globalThis: proxyGlobalThis, global: proxyGlobal, self: proxySelf, __globals: __globalsStorage, }; __globals.contextId = contextId; __globals.contextType = contextType; __globals.module = undefined; __globals.amd = undefined; __globals.define = undefined; __globals.importScripts = (...args) => { _log("importScripts", args); }; return __globals; } if (typeof window !== 'undefined') { window.buildPolyfill = buildPolyfill; } // #endregion // #endregion // #endregion // #region Background Script Environment const START_BACKGROUND_SCRIPT = (function(){ const backgroundPolyfill = buildPolyfill({ isBackground: true }); const scriptName = "Return YouTube Dislike"; const debug = "[Return YouTube Dislike]"; _log(debug + ' Executing background scripts...'); function executeBackgroundScripts(){ with(backgroundPolyfill){ // BG: ryd.background.js /******/ (() => { // webpackBootstrap var __webpack_exports__ = {}; const apiUrl = "https://returnyoutubedislikeapi.com"; const voteDisabledIconName = "icon_hold128.png"; const defaultIconName = "icon128.png"; let api; /** stores extension's global config */ let extConfig = { disableVoteSubmission: false, disableLogging: true, coloredThumbs: false, coloredBar: false, colorTheme: "classic", // classic, accessible, neon numberDisplayFormat: "compactShort", // compactShort, compactLong, standard numberDisplayReformatLikes: false, // use existing (native) likes number }; if (isChrome()) api = chrome; else if (isFirefox()) api = browser; initExtConfig(); api.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.message === "get_auth_token") { chrome.identity.getAuthToken({ interactive: true }, function (token) { console.log(token); chrome.identity.getProfileUserInfo(function (userInfo) { console.log(JSON.stringify(userInfo)); }); }); } else if (request.message === "log_off") { // chrome.identity.clearAllCachedAuthTokens(() => console.log("logged off")); } else if (request.message == "set_state") { // chrome.identity.getAuthToken({ interactive: true }, function (token) { let token = ""; fetch(`${apiUrl}/votes?videoId=${request.videoId}&likeCount=${request.likeCount || ""}`, { method: "GET", headers: { Accept: "application/json", }, }) .then((response) => response.json()) .then((response) => { sendResponse(response); }) .catch(); return true; } else if (request.message == "send_links") { toSend = toSend.concat(request.videoIds.filter((x) => !sentIds.has(x))); if (toSend.length >= 20) { fetch(`${apiUrl}/votes`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(toSend), }); for (const toSendUrl of toSend) { sentIds.add(toSendUrl); } toSend = []; } } else if (request.message == "register") { register(); return true; } else if (request.message == "send_vote") { sendVote(request.videoId, request.vote); return true; } }); api.runtime.onInstalled.addListener((details) => { if ( // No need to show changelog if its was a browser update (and not extension update) details.reason === "browser_update" || // Chromium (e.g., Google Chrome Cannary) uses this name instead of the one above for some reason details.reason === "chrome_update" || // No need to show changelog if developer just reloaded the extension details.reason === "update" ) { return; } else if (details.reason == "install") { api.tabs.create({ url: api.runtime.getURL("/changelog/3/changelog_3.0.html"), }); } }); // api.storage.sync.get(['lastShowChangelogVersion'], (details) => { // if (extConfig.showUpdatePopup === true && // details.lastShowChangelogVersion !== chrome.runtime.getManifest().version // ) { // // keep it inside get to avoid race condition // api.storage.sync.set({'lastShowChangelogVersion ': chrome.runtime.getManifest().version}); // // wait until async get runs & don't steal tab focus // api.tabs.create({url: api.runtime.getURL("/changelog/3/changelog_3.0.html"), active: false}); // } // }); async function sendVote(videoId, vote) { api.storage.sync.get(null, async (storageResult) => { if (!storageResult.userId || !storageResult.registrationConfirmed) { await register(); } let voteResponse = await fetch(`${apiUrl}/interact/vote`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ userId: storageResult.userId, videoId, value: vote, }), }); if (voteResponse.status == 401) { await register(); await sendVote(videoId, vote); return; } const voteResponseJson = await voteResponse.json(); const solvedPuzzle = await solvePuzzle(voteResponseJson); if (!solvedPuzzle.solution) { await sendVote(videoId, vote); return; } await fetch(`${apiUrl}/interact/confirmVote`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ ...solvedPuzzle, userId: storageResult.userId, videoId, }), }); }); } async function register() { const userId = generateUserID(); api.storage.sync.set({ userId }); const registrationResponse = await fetch(`${apiUrl}/puzzle/registration?userId=${userId}`, { method: "GET", headers: { Accept: "application/json", }, }).then((response) => response.json()); const solvedPuzzle = await solvePuzzle(registrationResponse); if (!solvedPuzzle.solution) { await register(); return; } const result = await fetch(`${apiUrl}/puzzle/registration?userId=${userId}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(solvedPuzzle), }).then((response) => response.json()); if (result === true) { return api.storage.sync.set({ registrationConfirmed: true }); } } api.storage.sync.get(null, async (res) => { if (!res || !res.userId || !res.registrationConfirmed) { await register(); } }); const sentIds = new Set(); let toSend = []; function countLeadingZeroes(uInt8View, limit) { let zeroes = 0; let value = 0; for (let i = 0; i < uInt8View.length; i++) { value = uInt8View[i]; if (value === 0) { zeroes += 8; } else { let count = 1; if (value >>> 4 === 0) { count += 4; value <<= 4; } if (value >>> 6 === 0) { count += 2; value <<= 2; } zeroes += count - (value >>> 7); break; } if (zeroes >= limit) { break; } } return zeroes; } async function solvePuzzle(puzzle) { let challenge = Uint8Array.from(atob(puzzle.challenge), (c) => c.charCodeAt(0)); let buffer = new ArrayBuffer(20); let uInt8View = new Uint8Array(buffer); let uInt32View = new Uint32Array(buffer); let maxCount = Math.pow(2, puzzle.difficulty) * 3; for (let i = 4; i < 20; i++) { uInt8View[i] = challenge[i - 4]; } for (let i = 0; i < maxCount; i++) { uInt32View[0] = i; let hash = await crypto.subtle.digest("SHA-512", buffer); let hashUint8 = new Uint8Array(hash); if (countLeadingZeroes(hashUint8) >= puzzle.difficulty) { return { solution: btoa(String.fromCharCode.apply(null, uInt8View.slice(0, 4))), }; } } return {}; } function generateUserID(length = 36) { const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; if (crypto && crypto.getRandomValues) { const values = new Uint32Array(length); crypto.getRandomValues(values); for (let i = 0; i < length; i++) { result += charset[values[i] % charset.length]; } return result; } else { for (let i = 0; i < length; i++) { result += charset[Math.floor(Math.random() * charset.length)]; } return result; } } function storageChangeHandler(changes, area) { if (changes.disableVoteSubmission !== undefined) { handleDisableVoteSubmissionChangeEvent(changes.disableVoteSubmission.newValue); } if (changes.coloredThumbs !== undefined) { handleColoredThumbsChangeEvent(changes.coloredThumbs.newValue); } if (changes.coloredBar !== undefined) { handleColoredBarChangeEvent(changes.coloredBar.newValue); } if (changes.colorTheme !== undefined) { handleColorThemeChangeEvent(changes.colorTheme.newValue); } if (changes.numberDisplayFormat !== undefined) { handleNumberDisplayFormatChangeEvent(changes.numberDisplayFormat.newValue); } if (changes.numberDisplayReformatLikes !== undefined) { handleNumberDisplayReformatLikesChangeEvent(changes.numberDisplayReformatLikes.newValue); } if (changes.disableLogging !== undefined) { handleDisableLoggingChangeEvent(changes.disableLogging.newValue); } if (changes.showTooltipPercentage !== undefined) { handleShowTooltipPercentageChangeEvent(changes.showTooltipPercentage.newValue); } if (changes.numberDisplayReformatLikes !== undefined) { handleNumberDisplayReformatLikesChangeEvent(changes.numberDisplayReformatLikes.newValue); } } function handleDisableVoteSubmissionChangeEvent(value) { extConfig.disableVoteSubmission = value; if (value === true) { changeIcon(voteDisabledIconName); } else { changeIcon(defaultIconName); } } function handleDisableLoggingChangeEvent(value) { extConfig.disableLogging = value; } function handleNumberDisplayFormatChangeEvent(value) { extConfig.numberDisplayFormat = value; } function handleShowTooltipPercentageChangeEvent(value) { extConfig.showTooltipPercentage = value; } function handleTooltipPercentageModeChangeEvent(value) { if (!value) { value = "dash_like"; } extConfig.tooltipPercentageMode = value; } function changeIcon(iconName) { if (api.action !== undefined) api.action.setIcon({ path: "/icons/" + iconName }); else if (api.browserAction !== undefined) api.browserAction.setIcon({ path: "/icons/" + iconName }); else console.log("changing icon is not supported"); } function handleColoredThumbsChangeEvent(value) { extConfig.coloredThumbs = value; } function handleColoredBarChangeEvent(value) { extConfig.coloredBar = value; } function handleColorThemeChangeEvent(value) { if (!value) { value = "classic"; } extConfig.colorTheme = value; } function handleNumberDisplayReformatLikesChangeEvent(value) { extConfig.numberDisplayReformatLikes = value; } api.storage.onChanged.addListener(storageChangeHandler); function initExtConfig() { initializeDisableVoteSubmission(); initializeDisableLogging(); initializeColoredThumbs(); initializeColoredBar(); initializeColorTheme(); initializeNumberDisplayFormat(); initializeNumberDisplayReformatLikes(); initializeTooltipPercentage(); initializeTooltipPercentageMode(); } function initializeDisableVoteSubmission() { api.storage.sync.get(["disableVoteSubmission"], (res) => { if (res.disableVoteSubmission === undefined) { api.storage.sync.set({ disableVoteSubmission: false }); } else { extConfig.disableVoteSubmission = res.disableVoteSubmission; if (res.disableVoteSubmission) changeIcon(voteDisabledIconName); } }); } function initializeDisableLogging() { api.storage.sync.get(["disableLogging"], (res) => { if (res.disableLogging === undefined) { api.storage.sync.set({ disableLogging: true }); } else { extConfig.disableLogging = res.disableLogging; } }); } function initializeColoredThumbs() { api.storage.sync.get(["coloredThumbs"], (res) => { if (res.coloredThumbs === undefined) { api.storage.sync.set({ coloredThumbs: false }); } else { extConfig.coloredThumbs = res.coloredThumbs; } }); } function initializeColoredBar() { api.storage.sync.get(["coloredBar"], (res) => { if (res.coloredBar === undefined) { api.storage.sync.set({ coloredBar: false }); } else { extConfig.coloredBar = res.coloredBar; } }); } function initializeColorTheme() { api.storage.sync.get(["colorTheme"], (res) => { if (res.colorTheme === undefined) { api.storage.sync.set({ colorTheme: false }); } else { extConfig.colorTheme = res.colorTheme; } }); } function initializeNumberDisplayFormat() { api.storage.sync.get(["numberDisplayFormat"], (res) => { if (res.numberDisplayFormat === undefined) { api.storage.sync.set({ numberDisplayFormat: "compactShort" }); } else { extConfig.numberDisplayFormat = res.numberDisplayFormat; } }); } function initializeTooltipPercentage() { api.storage.sync.get(["showTooltipPercentage"], (res) => { if (res.showTooltipPercentage === undefined) { api.storage.sync.set({ showTooltipPercentage: false }); } else { extConfig.showTooltipPercentage = res.showTooltipPercentage; } }); } function initializeTooltipPercentageMode() { api.storage.sync.get(["tooltipPercentageMode"], (res) => { if (res.tooltipPercentageMode === undefined) { api.storage.sync.set({ tooltipPercentageMode: "dash_like" }); } else { extConfig.tooltipPercentageMode = res.tooltipPercentageMode; } }); } function initializeNumberDisplayReformatLikes() { api.storage.sync.get(["numberDisplayReformatLikes"], (res) => { if (res.numberDisplayReformatLikes === undefined) { api.storage.sync.set({ numberDisplayReformatLikes: false }); } else { extConfig.numberDisplayReformatLikes = res.numberDisplayReformatLikes; } }); } function isChrome() { return typeof chrome !== "undefined" && typeof chrome.runtime !== "undefined"; } function isFirefox() { return typeof browser !== "undefined" && typeof browser.runtime !== "undefined"; } /******/ })() ; } } executeBackgroundScripts.call(backgroundPolyfill); _log(debug + ' Background scripts execution complete.'); }); setTimeout(() => { // Wait for things to be defined START_BACKGROUND_SCRIPT(); }, 10); _log("START_BACKGROUND_SCRIPT", START_BACKGROUND_SCRIPT); // End background script environment // #endregion // #region Orchestration Logic // Other globals currently defined at this spot: SCRIPT_NAME, _log, _warn, _error const INJECTED_MANIFEST = {"manifest_version":3,"name":"Return YouTube Dislike","version":"3.0.0.18","description":"Returns ability to see dislikes","permissions":["storage"],"optional_permissions":[],"content_scripts":[{"matches":["*://youtube.com/*","*://www.youtube.com/*","*://m.youtube.com/*"],"exclude_matches":["*://*.music.youtube.com/*"],"js":["ryd.content-script.js"],"css":["content-style.css"]}],"options_ui":{"page":"popup.html","open_in_tab":false},"browser_action":{},"page_action":{},"action":{"default_popup":"popup.html"},"icons":{"48":"icons/icon48.png","128":"icons/icon128.png"},"web_accessible_resources":[{"resources":["ryd.script.js","menu-fixer.js"],"matches":["*://*.youtube.com/*"]}],"background":{"service_worker":"ryd.background.js"},"_id":"return-youtube-dislike"}; const CONTENT_SCRIPT_CONFIGS_FOR_MATCHING = [ { "matches": [ "*://youtube.com/*", "*://www.youtube.com/*", "*://m.youtube.com/*" ] } ]; const OPTIONS_PAGE_PATH = "popup.html"; const POPUP_PAGE_PATH = "popup.html"; const EXTENSION_ICON = "data:image/png;base64,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"; const extensionCssData = { "content-style.css": ":root {\r\n /* --yt-spec-icon-disabled: #f44 !important;\r\n --yt-spec-text-primary: #4f4 !important; */\r\n /* --yt-spec-general-background-a: #000 !important;\r\n --yt-spec-general-background-b: #000 !important;\r\n --yt-spec-general-background-c: #000 !important;\r\n --yt-spec-brand-background-solid: #000 !important;\r\n --yt-spec-brand-background-primary: #000 !important;\r\n --yt-spec-brand-background-secondary: #000 !important; */\r\n}\r\n\r\n/* html:not(.style-scope)[dark], :not(.style-scope)[dark] {\r\n --yt-spec-general-background-a: #000 !important;\r\n --yt-spec-general-background-b: #000 !important;\r\n --yt-spec-general-background-c: #000 !important;\r\n --yt-spec-brand-background-solid: #000 !important;\r\n --yt-spec-brand-background-primary: #000 !important;\r\n --yt-spec-brand-background-secondary: #000 !important;\r\n} */\r\n\r\n#ryd-bar-container {\r\n background: var(--yt-spec-icon-disabled);\r\n border-radius: 2px;\r\n}\r\n\r\n#ryd-bar {\r\n background: var(--yt-spec-text-primary);\r\n border-radius: 2px;\r\n transition: all 0.15s ease-in-out;\r\n}\r\n\r\n.ryd-tooltip {\r\n display: block;\r\n height: 2px;\r\n}\r\n\r\n.ryd-tooltip-old-design {\r\n position: relative;\r\n top: 9px;\r\n}\r\n\r\n.ryd-tooltip-new-design {\r\n position: absolute;\r\n bottom: -10px;\r\n}\r\n\r\n.ryd-tooltip-bar-container {\r\n width: 100%;\r\n height: 2px;\r\n position: absolute;\r\n padding-top: 6px;\r\n padding-bottom: 12px;\r\n top: -6px;\r\n}\r\n\r\n/* required to make the ratio bar visible in the new design */\r\nytd-menu-renderer.ytd-watch-metadata {\r\n overflow-y: visible !important;\r\n}\r\n\r\n#top-level-buttons-computed {\r\n position: relative !important;\r\n}\r\n"}; const LOCALE_KEYS = {"extensionName":{"message":"Return YouTube Dislike"},"extensionNameBeta":{"message":"Return YouTube Dislike Beta"},"extensionDesc":{"message":"Returns ability to see dislikes"},"textDeveloper":{"message":"by Dmitry Selivanov & Community"},"linkWebsite":{"message":"Website"},"linkFAQ":{"message":"FAQ"},"linkDonate":{"message":"Donate"},"linkHelp":{"message":"Help"},"linkChangelog":{"message":"Change Log"},"legendSettings":{"message":"Settings"},"textSettings":{"message":"Disable like/dislike submission"},"textLikesDisabled":{"message":"Disabled by Owner"},"textSettingsHover":{"message":"Stops counting your likes and dislikes."},"textRoundingNumbers":{"message":"Round down like/dislike stats (default YouTube behavior)"},"textRoundingNumbersHover":{"message":"Show rounded down stats."},"textConsistentFormat":{"message":"Make likes and dislikes format consistent"},"textConsistentFormatHover":{"message":"Re-format like numbers."},"textNumberFormat":{"message":"Number format:"},"textColorizeRatioBar":{"message":"Colorize ratio bar"},"textColorizeRatioBarHover":{"message":"Use custom colors for ratio bar."},"textColorizeThumbs":{"message":"Colorize thumbs"},"textColorizeThumbsHover":{"message":"Use custom colors for thumb icons."},"textColorTheme":{"message":"Color theme:"},"textColorTheme1":{"message":"Classic"},"textColorTheme2":{"message":"Accessible"},"textColorTheme3":{"message":"Neon"},"textTempUnavailable":{"message":"Temporarily Unavailable"},"textUpdate":{"message":"Update to"},"version30installed":{"message":"Version 3.0.0.18 installed"},"whatsnew":{"message":"What's new"},"shortsSupport":{"message":"YouTube Shorts Support"},"customColors":{"message":"Custom colors for dislike bar and buttons"},"customNumberFormats":{"message":"Custom number formats"},"considerDonating":{"message":"The only thing that keeps the extension running is your donations, please consider supporting the project."},"roundNumbers":{"message":"Show rounded down numbers"},"roundNumbersHover":{"message":"Round down numbers (default YouTube behavior)."},"reformatLikes":{"message":"Re-format like numbers"},"reformatLikesHover":{"message":"Make likes and dislikes format consistent."},"numberFormat":{"message":"Number format:"},"colorizeRatio":{"message":"Colorize ratio bar"},"colorizeRatioHover":{"message":"Use custom colors for the ratio bar."},"colorizeThumbs":{"message":"Colorize thumbs"},"colorizeThumbsHover":{"message":"Use custom colors for the thumb icons."},"colorTheme":{"message":"Color theme:"}}; const USED_LOCALE = "en"; const CURRENT_LOCATION = window.location.href; const convertMatchPatternToRegExp = function convertMatchPatternToRegExp(pattern) { if (pattern === "") return new RegExp(".*"); try { const singleEscapedPattern = convertMatchPatternToRegExpString(pattern).replace(/\\\\/g, "\\"); return new RegExp(singleEscapedPattern); } catch (error) { debug("Error converting match pattern to RegExp: %s, Error: %s", pattern, error.message); return new RegExp("$."); } }; const convertMatchPatternToRegExpString = function convertMatchPatternToRegExpString(pattern) { function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\\\$&"); } if (typeof pattern !== "string" || !pattern) return "$."; const schemeMatch = pattern.match(/^(\*|https?|file|ftp):\/\//); if (!schemeMatch) return "$."; const scheme = schemeMatch[1]; pattern = pattern.substring(schemeMatch[0].length); const schemeRegex = scheme === "*" ? "https?|file|ftp" : scheme, hostMatch = pattern.match(/^([^\/]+)/); if (!hostMatch) return "$."; const host = hostMatch[1]; pattern = pattern.substring(host.length); let hostRegex; if (host === "*") hostRegex = "[^/]+"; else if (host.startsWith("*.")) hostRegex = "(?:[^\\/]+\\.)?" + escapeRegex(host.substring(2)); else hostRegex = escapeRegex(host); let pathRegex = pattern; if (!pathRegex.startsWith("/")) pathRegex = "/" + pathRegex; pathRegex = pathRegex.split("*").map(escapeRegex).join(".*"); if (pathRegex === "/.*") pathRegex = "(?:/.*)?"; else pathRegex = pathRegex + "(?:[?#]|$)"; return `^${schemeRegex}:\\/\\/${hostRegex}${pathRegex}`; }; const ALL_PERMISSIONS = [ ...(INJECTED_MANIFEST.permissions || []), ...(INJECTED_MANIFEST.optional_permissions || []), ...(INJECTED_MANIFEST.host_permissions || []), ...(INJECTED_MANIFEST.content_scripts ?.map((cs) => cs.matches || []) ?.flat() || []), ]; const isOrigin = (perm) => { if ( perm.startsWith("*://") || perm.startsWith("http://") || perm.startsWith("https://") ) { return true; } return false; }; const ORIGIN_PERMISSIONS = ALL_PERMISSIONS.filter(isOrigin); const EXTENSION_PERMISSIONS = ALL_PERMISSIONS.filter((perm) => !isOrigin(perm)); function _testBlobCSP() { try { const code = `console.log("Blob CSP test");`; const blob = new Blob([code], { type: "application/javascript" }); const blobUrl = URL.createObjectURL(blob); const script = document.createElement("script"); script.src = blobUrl; let blocked = false; script.onerror = () => { blocked = true; }; document.head.appendChild(script); return new Promise((resolve) => { setTimeout(() => { resolve(!blocked); document.head.removeChild(script); URL.revokeObjectURL(blobUrl); }, 100); }); } catch (e) { return Promise.resolve(false); } } let CAN_USE_BLOB_CSP = false; const waitForDOMEnd = () => { if (document.readyState === "loading") { return new Promise((resolve) => document.addEventListener("DOMContentLoaded", resolve, { once: true }) ); } return Promise.resolve(); }; waitForDOMEnd().then(() => { _testBlobCSP().then((result) => { CAN_USE_BLOB_CSP = result; }); }); function _base64ToBlob(base64, mimeType = "application/octet-stream") { const binary = atob(base64); const len = binary.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i); return new Blob([bytes], { type: mimeType }); } function _getMimeTypeFromPath(p) { const ext = (p.split(".").pop() || "").toLowerCase(); const map = { html: "text/html", htm: "text/html", js: "text/javascript", css: "text/css", json: "application/json", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", svg: "image/svg+xml", webp: "image/webp", ico: "image/x-icon", woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", otf: "font/otf", eot: "application/vnd.ms-fontobject", }; return map[ext] || "application/octet-stream"; } function _isTextAsset(ext) { return ["html", "htm", "js", "css", "json", "svg", "txt", "xml"].includes( ext ); } function _createAssetUrl(path = "") { if (path.startsWith("/")) path = path.slice(1); const assetData = EXTENSION_ASSETS_MAP[path]; if (typeof assetData === "undefined") { _warn("[runtime.getURL] Asset not found for", path); return path; } const mime = _getMimeTypeFromPath(path); const ext = (path.split(".").pop() || "").toLowerCase(); if (CAN_USE_BLOB_CSP) { let blob; if (_isTextAsset(ext)) { blob = new Blob([assetData], { type: mime }); } else { blob = _base64ToBlob(assetData, mime); } return URL.createObjectURL(blob); } else { if (_isTextAsset(ext)) { return `data:${mime};base64,${btoa(assetData)}`; } else { return `data:${mime};base64,${assetData}`; } } } function _matchGlobPattern(pattern, path) { if (!pattern || !path) return false; pattern = pattern.replace(/\\/g, "/"); path = path.replace(/\\/g, "/"); if (pattern === path) return true; let regexPattern = pattern .replace(/[.+?^${}()|[\]\\]/g, "\\$&") // Escape regex chars .replace(/\*\*/g, "__DOUBLESTAR__") // Temporarily replace ** .replace(/\*/g, "[^/]*") // * matches any chars except / .replace(/__DOUBLESTAR__/g, ".*"); // ** matches any chars including / regexPattern = "^" + regexPattern + "$"; try { const regex = new RegExp(regexPattern); return regex.test(path); } catch (e) { _error(`Invalid glob pattern: ${pattern}`, e); return false; } } function _isWebAccessibleResource(resourcePath, webAccessibleResources) { if ( !Array.isArray(webAccessibleResources) || webAccessibleResources.length === 0 ) { return false; } // Normalize the resource path const normalizedPath = resourcePath.replace(/\\/g, "/").replace(/^\/+/, ""); for (const webAccessibleResource of webAccessibleResources) { let patterns = []; // Handle both manifest v2 and v3 formats if (typeof webAccessibleResource === "string") { // Manifest v2 format: array of strings patterns = [webAccessibleResource]; } else if ( webAccessibleResource && Array.isArray(webAccessibleResource.resources) ) { // Manifest v3 format: objects with resources array patterns = webAccessibleResource.resources; } // Check if the path matches any pattern for (const pattern of patterns) { if (_matchGlobPattern(pattern, normalizedPath)) { return true; } } } return false; } window._matchGlobPattern = _matchGlobPattern; window._isWebAccessibleResource = _isWebAccessibleResource; // This function contains all the CSS injection and JS execution, // ordered by run_at timing internally using await. // #region Script Execution Logic async function executeAllScripts(globalThis, extensionCssData) { const {chrome, browser, global, window, self} = globalThis; const scriptName = "Return YouTube Dislike"; _log(`Starting execution phases...`); // #region Document Start if (typeof document !== 'undefined') { _log(`Executing document-start phase...`); const scriptPaths = []; _log(` Executing JS (start): ${scriptPaths}`); try { // Keep variables from being redeclared for global scope, but also make them apply to global scope. (Theoretically) with (globalThis){; ;} } catch(e) { _error(` Error executing scripts ${scriptPaths}`, e); } } else { _log(`Skipping document-start phase (no document).`); } // #endregion // #region Wait for Document End DOMContentLoaded --- if (typeof document !== 'undefined' && document.readyState === 'loading') { _log(`Waiting for DOMContentLoaded...`); await new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, { once: true })); _log(`DOMContentLoaded fired.`); } else if (typeof document !== 'undefined') { _log(`DOMContentLoaded already passed or not applicable.`); } // #endregion // #region Document End if (typeof document !== 'undefined') { _log(`Executing document-end phase...`); const scriptPaths = []; _log(` Executing JS (end): ${scriptPaths}`); try { // Keep variables from being redeclared for global scope, but also make them apply to global scope. (Theoretically) with (globalThis){; ;} } catch(e) { _error(` Error executing scripts ${scriptPaths}`, e); } } else { _log(`Skipping document-end phase (no document).`); } // #endregion // #region Wait for Document Idle _log(`Waiting for document idle state...`); if (typeof window !== 'undefined' && typeof window.requestIdleCallback === 'function') { await new Promise(resolve => window.requestIdleCallback(resolve, { timeout: 2000 })); // 2-second timeout fallback _log(`requestIdleCallback fired or timed out.`); } else { // Fallback: wait a short period after DOMContentLoaded/current execution if requestIdleCallback is unavailable await new Promise(resolve => setTimeout(resolve, 50)); _log(`Idle fallback timer completed.`); } // #endregion // #region Document Idle if (typeof document !== 'undefined') { _log(`Executing document-idle phase...`); const cssKey_0 = "content-style.css"; try { if (extensionCssData[cssKey_0]) { _log(` Injecting CSS (idle): ${cssKey_0}`); const style = document.createElement('style'); style.textContent = extensionCssData[cssKey_0]; (document.head || document.documentElement).appendChild(style); } else { console.warn(` CSS not found (idle): ${cssKey_0}`); } } catch(e) { _error(` Failed injecting CSS (${cssKey_0}) in phase idle`, e, extensionCssData); } const scriptPaths = ["ryd.content-script.js"]; _log(` Executing JS (idle): ${scriptPaths}`); try { // Keep variables from being redeclared for global scope, but also make them apply to global scope. (Theoretically) with (globalThis){; // START: ryd.content-script.js /******/ (() => { // webpackBootstrap /******/ "use strict"; /******/ var __webpack_modules__ = ({ /***/ 805: /***/ ((module, __unused_webpack___webpack_exports__, __webpack_require__) => { __webpack_require__.a(module, async (__webpack_handle_async_dependencies__, __webpack_async_result__) => { try { /* harmony import */ var _src_buttons__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(423); /* harmony import */ var _src_state__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(935); /* harmony import */ var _src_utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(67); /* harmony import */ var _src_events__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(27); // #endregion // #region Import Button Functions --- // #endregion // #region Import State Functions --- // #endregion // #region Import Video Browser Functions --- await (0,_src_state__WEBPACK_IMPORTED_MODULE_1__/* .initExtConfig */ .qg)(); let jsInitChecktimer = null; let isSetInitialStateDone = false; async function setEventListeners(evt) { async function checkForJS_Finish() { try { if ((0,_src_state__WEBPACK_IMPORTED_MODULE_1__/* .isShorts */ .ol)() || ((0,_src_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getButtons */ .hS)()?.offsetParent && (0,_src_utils__WEBPACK_IMPORTED_MODULE_2__/* .isVideoLoaded */ .x8)())) { clearInterval(jsInitChecktimer); jsInitChecktimer = null; (0,_src_events__WEBPACK_IMPORTED_MODULE_3__/* .createSmartimationObserver */ .Q$)(); (0,_src_events__WEBPACK_IMPORTED_MODULE_3__/* .addLikeDislikeEventListener */ .G_)(); await (0,_src_state__WEBPACK_IMPORTED_MODULE_1__/* .setInitialState */ .KY)(); isSetInitialStateDone = true; (0,_src_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.onChanged.addListener(_src_events__WEBPACK_IMPORTED_MODULE_3__/* .storageChangeHandler */ .F6); } } catch (exception) { if (!isSetInitialStateDone) { (0,_src_utils__WEBPACK_IMPORTED_MODULE_2__/* .cLog */ .D2)("error"); await (0,_src_state__WEBPACK_IMPORTED_MODULE_1__/* .setInitialState */ .KY)(); } } } if (jsInitChecktimer !== null) clearInterval(jsInitChecktimer); jsInitChecktimer = setInterval(await checkForJS_Finish, 111); } await setEventListeners(); document.addEventListener("yt-navigate-finish", async function (event) { if (jsInitChecktimer !== null) clearInterval(jsInitChecktimer); await setEventListeners(); }); const s = document.createElement("script"); s.src = chrome.runtime.getURL("menu-fixer.js"); s.onload = function () { this.remove(); }; // see also "Dynamic values in the injected code" section in this answer (document.head || document.documentElement).appendChild(s); __webpack_async_result__(); } catch(e) { __webpack_async_result__(e); } }, 1); /***/ }), /***/ 910: /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ k: () => (/* binding */ createRateBar) /* harmony export */ }); /* harmony import */ var _buttons__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(423); /* harmony import */ var _state__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(935); /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(67); function createRateBar(likes, dislikes) { let rateBar = document.getElementById("ryd-bar-container"); if (!(0,_state__WEBPACK_IMPORTED_MODULE_1__/* .isLikesDisabled */ .$L)()) { // sometimes rate bar is hidden if (rateBar && !(0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .isInViewport */ .v4)(rateBar)) { rateBar.remove(); rateBar = null; } const widthPx = parseFloat(window.getComputedStyle((0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getLikeButton */ .yN)()).width) + parseFloat(window.getComputedStyle((0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getDislikeButton */ .x8)()).width) + ((0,_state__WEBPACK_IMPORTED_MODULE_1__/* .isRoundedDesign */ .MB)() ? 0 : 8); const widthPercent = likes + dislikes > 0 ? (likes / (likes + dislikes)) * 100 : 50; var likePercentage = parseFloat(widthPercent.toFixed(1)); const dislikePercentage = (100 - likePercentage).toLocaleString(); likePercentage = likePercentage.toLocaleString(); if (_state__WEBPACK_IMPORTED_MODULE_1__/* .extConfig */ .zO.showTooltipPercentage) { var tooltipInnerHTML; switch (_state__WEBPACK_IMPORTED_MODULE_1__/* .extConfig */ .zO.tooltipPercentageMode) { case "dash_dislike": tooltipInnerHTML = `${likes.toLocaleString()} / ${dislikes.toLocaleString()}  -  ${dislikePercentage}%`; break; case "both": tooltipInnerHTML = `${likePercentage}% / ${dislikePercentage}%`; break; case "only_like": tooltipInnerHTML = `${likePercentage}%`; break; case "only_dislike": tooltipInnerHTML = `${dislikePercentage}%`; break; default: // dash_like tooltipInnerHTML = `${likes.toLocaleString()} / ${dislikes.toLocaleString()}  -  ${likePercentage}%`; } } else { tooltipInnerHTML = `${likes.toLocaleString()} / ${dislikes.toLocaleString()}`; } if (!(0,_state__WEBPACK_IMPORTED_MODULE_1__/* .isShorts */ .ol)()) { if (!rateBar && !(0,_state__WEBPACK_IMPORTED_MODULE_1__/* .isMobile */ .tq)()) { let colorLikeStyle = ""; let colorDislikeStyle = ""; if (_state__WEBPACK_IMPORTED_MODULE_1__/* .extConfig */ .zO.coloredBar) { colorLikeStyle = "; background-color: " + (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getColorFromTheme */ .t4)(true); colorDislikeStyle = "; background-color: " + (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getColorFromTheme */ .t4)(false); } let actions = (0,_state__WEBPACK_IMPORTED_MODULE_1__/* .isNewDesign */ .am)() && (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getButtons */ .hS)().id === "top-level-buttons-computed" ? (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getButtons */ .hS)() : document.getElementById("menu-container"); ( actions || document.querySelector("ytm-slim-video-action-bar-renderer") ).insertAdjacentHTML( "beforeend", `
${tooltipInnerHTML}
`, ); if ((0,_state__WEBPACK_IMPORTED_MODULE_1__/* .isNewDesign */ .am)()) { // Add border between info and comments let descriptionAndActionsElement = document.getElementById("top-row"); descriptionAndActionsElement.style.borderBottom = "1px solid var(--yt-spec-10-percent-layer)"; descriptionAndActionsElement.style.paddingBottom = "10px"; // Fix like/dislike ratio bar offset in new UI document.getElementById("actions-inner").style.width = "revert"; if ((0,_state__WEBPACK_IMPORTED_MODULE_1__/* .isRoundedDesign */ .MB)()) { document.getElementById("actions").style.flexDirection = "row-reverse"; } } } else { document.querySelector(`.ryd-tooltip`).style.width = widthPx + "px"; document.getElementById("ryd-bar").style.width = widthPercent + "%"; document.querySelector("#ryd-dislike-tooltip > #tooltip").innerHTML = tooltipInnerHTML; if (_state__WEBPACK_IMPORTED_MODULE_1__/* .extConfig */ .zO.coloredBar) { document.getElementById("ryd-bar-container").style.backgroundColor = (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getColorFromTheme */ .t4)(false); document.getElementById("ryd-bar").style.backgroundColor = (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getColorFromTheme */ .t4)(true); } } } } else { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .cLog */ .D2)("removing bar"); if (rateBar) { rateBar.parentNode.removeChild(rateBar); } } } /***/ }), /***/ 423: /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ $1: () => (/* binding */ checkForSignInButton), /* harmony export */ Eq: () => (/* binding */ getLikeTextContainer), /* harmony export */ hS: () => (/* binding */ getButtons), /* harmony export */ x8: () => (/* binding */ getDislikeButton), /* harmony export */ xP: () => (/* binding */ getDislikeTextContainer), /* harmony export */ yN: () => (/* binding */ getLikeButton) /* harmony export */ }); /* harmony import */ var _state__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(935); /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(67); function getButtons() { //--- If Watching Youtube Shorts: ---// if ((0,_state__WEBPACK_IMPORTED_MODULE_0__/* .isShorts */ .ol)()) { let elements = (0,_state__WEBPACK_IMPORTED_MODULE_0__/* .isMobile */ .tq)() ? (0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .querySelectorAll */ .Wb)(_state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.selectors.buttons.shorts.mobile) : (0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .querySelectorAll */ .Wb)(_state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.selectors.buttons.shorts.desktop); for (let element of elements) { //YouTube Shorts can have multiple like/dislike buttons when scrolling through videos //However, only one of them should be visible (no matter how you zoom) if ((0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .isInViewport */ .v4)(element)) { return element; } } } //--- If Watching On Mobile: ---// if ((0,_state__WEBPACK_IMPORTED_MODULE_0__/* .isMobile */ .tq)()) { return document.querySelector(_state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.selectors.buttons.regular.mobile); } //--- If Menu Element Is Displayed: ---// if ((0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .querySelector */ .R2)(_state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.selectors.menuContainer)?.offsetParent === null) { return (0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .querySelector */ .R2)(_state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.selectors.buttons.regular.desktopMenu); //--- If Menu Element Isn't Displayed: ---// } else { return (0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .querySelector */ .R2)(_state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.selectors.buttons.regular.desktopNoMenu); } } function getLikeButton() { return getButtons().children[0].tagName === "YTD-SEGMENTED-LIKE-DISLIKE-BUTTON-RENDERER" ? (0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .querySelector */ .R2)(_state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.selectors.buttons.likeButton.segmented) ?? (0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .querySelector */ .R2)( _state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.selectors.buttons.likeButton.segmentedGetButtons, getButtons(), ) : (0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .querySelector */ .R2)( _state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.selectors.buttons.likeButton.notSegmented, getButtons(), ); } function getLikeTextContainer() { return (0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .querySelector */ .R2)(_state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.selectors.likeTextContainer, getLikeButton()); } function getDislikeButton() { return getButtons().children[0].tagName === "YTD-SEGMENTED-LIKE-DISLIKE-BUTTON-RENDERER" ? (0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .querySelector */ .R2)(_state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.selectors.buttons.dislikeButton.segmented) ?? (0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .querySelector */ .R2)( _state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.selectors.buttons.dislikeButton.segmentedGetButtons, getButtons(), ) : (0,_state__WEBPACK_IMPORTED_MODULE_0__/* .isShorts */ .ol)() ? (0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .querySelector */ .R2)(["#dislike-button"], getButtons()) : (0,_utils__WEBPACK_IMPORTED_MODULE_1__/* .querySelector */ .R2)( _state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.selectors.buttons.dislikeButton.notSegmented, getButtons(), ); } function createDislikeTextContainer() { const textNodeClone = ( getLikeButton().querySelector( ".yt-spec-button-shape-next__button-text-content", ) || getLikeButton().querySelector("button > div[class*='cbox']") || ( getLikeButton().querySelector('div > span[role="text"]') || document.querySelector( 'button > div.yt-spec-button-shape-next__button-text-content > span[role="text"]', ) ).parentNode ).cloneNode(true); const insertPreChild = getDislikeButton().querySelector("button"); insertPreChild.insertBefore(textNodeClone, null); getDislikeButton() .querySelector("button") .classList.remove("yt-spec-button-shape-next--icon-button"); getDislikeButton() .querySelector("button") .classList.add("yt-spec-button-shape-next--icon-leading"); if (textNodeClone.querySelector("span[role='text']") === null) { const span = document.createElement("span"); span.setAttribute("role", "text"); while (textNodeClone.firstChild) { textNodeClone.removeChild(textNodeClone.firstChild); } textNodeClone.appendChild(span); } textNodeClone.innerText = ""; return textNodeClone; } function getDislikeTextContainer() { let result; for (const selector of _state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.selectors.dislikeTextContainer) { result = getDislikeButton().querySelector(selector); if (result !== null) { break; } } if (result == null) { result = createDislikeTextContainer(); } return result; } function checkForSignInButton() { if ( document.querySelector( "a[href^='https://accounts.google.com/ServiceLogin']", ) ) { return true; } else { return false; } } /***/ }), /***/ 27: /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ F6: () => (/* binding */ storageChangeHandler), /* harmony export */ G_: () => (/* binding */ addLikeDislikeEventListener), /* harmony export */ Q$: () => (/* binding */ createSmartimationObserver) /* harmony export */ }); /* unused harmony exports sendVote, likeClicked, dislikeClicked */ /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(67); /* harmony import */ var _buttons__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(423); /* harmony import */ var _state__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(935); /* harmony import */ var _bar__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(910); function sendVote(vote) { if (_state__WEBPACK_IMPORTED_MODULE_2__/* .extConfig */ .zO.disableVoteSubmission !== true) { (0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .getBrowser */ .qs)().runtime.sendMessage({ message: "send_vote", vote: vote, videoId: (0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .getVideoId */ .gJ)(window.location.href), }); } } function updateDOMDislikes() { (0,_state__WEBPACK_IMPORTED_MODULE_2__/* .setDislikes */ .r6)((0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .numberFormat */ .Y4)(_state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.dislikes)); (0,_bar__WEBPACK_IMPORTED_MODULE_3__/* .createRateBar */ .k)(_state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.likes, _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.dislikes); } function likeClicked() { if ((0,_buttons__WEBPACK_IMPORTED_MODULE_1__/* .checkForSignInButton */ .$1)() === false) { if (_state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.previousState === _state__WEBPACK_IMPORTED_MODULE_2__/* .DISLIKED_STATE */ .Gv) { sendVote(1); if (_state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.dislikes > 0) _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.dislikes--; _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.likes++; updateDOMDislikes(); _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.previousState = _state__WEBPACK_IMPORTED_MODULE_2__/* .LIKED_STATE */ .AV; } else if (_state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.previousState === _state__WEBPACK_IMPORTED_MODULE_2__/* .NEUTRAL_STATE */ .kQ) { sendVote(1); _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.likes++; updateDOMDislikes(); _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.previousState = _state__WEBPACK_IMPORTED_MODULE_2__/* .LIKED_STATE */ .AV; } else if ((_state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.previousState = _state__WEBPACK_IMPORTED_MODULE_2__/* .LIKED_STATE */ .AV)) { sendVote(0); if (_state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.likes > 0) _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.likes--; updateDOMDislikes(); _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.previousState = _state__WEBPACK_IMPORTED_MODULE_2__/* .NEUTRAL_STATE */ .kQ; } if (_state__WEBPACK_IMPORTED_MODULE_2__/* .extConfig */ .zO.numberDisplayReformatLikes === true) { const nativeLikes = (0,_state__WEBPACK_IMPORTED_MODULE_2__/* .getLikeCountFromButton */ .m8)(); if (nativeLikes !== false) { (0,_state__WEBPACK_IMPORTED_MODULE_2__/* .setLikes */ .Xq)((0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .numberFormat */ .Y4)(nativeLikes)); } } } } function dislikeClicked() { if ((0,_buttons__WEBPACK_IMPORTED_MODULE_1__/* .checkForSignInButton */ .$1)() == false) { if (_state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.previousState === _state__WEBPACK_IMPORTED_MODULE_2__/* .NEUTRAL_STATE */ .kQ) { sendVote(-1); _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.dislikes++; updateDOMDislikes(); _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.previousState = _state__WEBPACK_IMPORTED_MODULE_2__/* .DISLIKED_STATE */ .Gv; } else if (_state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.previousState === _state__WEBPACK_IMPORTED_MODULE_2__/* .DISLIKED_STATE */ .Gv) { sendVote(0); if (_state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.dislikes > 0) _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.dislikes--; updateDOMDislikes(); _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.previousState = _state__WEBPACK_IMPORTED_MODULE_2__/* .NEUTRAL_STATE */ .kQ; } else if (_state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.previousState === _state__WEBPACK_IMPORTED_MODULE_2__/* .LIKED_STATE */ .AV) { sendVote(-1); if (_state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.likes > 0) _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.likes--; _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.dislikes++; updateDOMDislikes(); _state__WEBPACK_IMPORTED_MODULE_2__/* .storedData */ .vk.previousState = _state__WEBPACK_IMPORTED_MODULE_2__/* .DISLIKED_STATE */ .Gv; if (_state__WEBPACK_IMPORTED_MODULE_2__/* .extConfig */ .zO.numberDisplayReformatLikes === true) { const nativeLikes = (0,_state__WEBPACK_IMPORTED_MODULE_2__/* .getLikeCountFromButton */ .m8)(); if (nativeLikes !== false) { (0,_state__WEBPACK_IMPORTED_MODULE_2__/* .setLikes */ .Xq)((0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .numberFormat */ .Y4)(nativeLikes)); } } } } } function addLikeDislikeEventListener() { if (window.rydPreNavigateLikeButton !== (0,_buttons__WEBPACK_IMPORTED_MODULE_1__/* .getLikeButton */ .yN)()) { (0,_buttons__WEBPACK_IMPORTED_MODULE_1__/* .getLikeButton */ .yN)().addEventListener("click", likeClicked); (0,_buttons__WEBPACK_IMPORTED_MODULE_1__/* .getLikeButton */ .yN)().addEventListener("touchstart", likeClicked); if ((0,_buttons__WEBPACK_IMPORTED_MODULE_1__/* .getDislikeButton */ .x8)()) { (0,_buttons__WEBPACK_IMPORTED_MODULE_1__/* .getDislikeButton */ .x8)().addEventListener("click", dislikeClicked); (0,_buttons__WEBPACK_IMPORTED_MODULE_1__/* .getDislikeButton */ .x8)().addEventListener("touchstart", dislikeClicked); (0,_buttons__WEBPACK_IMPORTED_MODULE_1__/* .getDislikeButton */ .x8)().addEventListener("focusin", updateDOMDislikes); (0,_buttons__WEBPACK_IMPORTED_MODULE_1__/* .getDislikeButton */ .x8)().addEventListener("focusout", updateDOMDislikes); } window.rydPreNavigateLikeButton = (0,_buttons__WEBPACK_IMPORTED_MODULE_1__/* .getLikeButton */ .yN)(); } } let smartimationObserver = null; function createSmartimationObserver() { if (!smartimationObserver) { smartimationObserver = (0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .createObserver */ .zo)( { attributes: true, subtree: true, childList: true, }, updateDOMDislikes, ); smartimationObserver.container = null; } const smartimationContainer = (0,_buttons__WEBPACK_IMPORTED_MODULE_1__/* .getButtons */ .hS)().querySelector("yt-smartimation"); if ( smartimationContainer && smartimationObserver.container != smartimationContainer ) { (0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .cLog */ .D2)("Initializing smartimation mutation observer"); smartimationObserver.disconnect(); smartimationObserver.observe(smartimationContainer); smartimationObserver.container = smartimationContainer; } } function storageChangeHandler(changes, area) { if (changes.disableVoteSubmission !== undefined) { handleDisableVoteSubmissionChangeEvent( changes.disableVoteSubmission.newValue, ); } if (changes.coloredThumbs !== undefined) { handleColoredThumbsChangeEvent(changes.coloredThumbs.newValue); } if (changes.coloredBar !== undefined) { handleColoredBarChangeEvent(changes.coloredBar.newValue); } if (changes.colorTheme !== undefined) { handleColorThemeChangeEvent(changes.colorTheme.newValue); } if (changes.numberDisplayFormat !== undefined) { handleNumberDisplayFormatChangeEvent(changes.numberDisplayFormat.newValue); } if (changes.numberDisplayReformatLikes !== undefined) { handleNumberDisplayReformatLikesChangeEvent( changes.numberDisplayReformatLikes.newValue, ); } } function handleDisableVoteSubmissionChangeEvent(value) { _state__WEBPACK_IMPORTED_MODULE_2__/* .extConfig */ .zO.disableVoteSubmission = value; } function handleColoredThumbsChangeEvent(value) { _state__WEBPACK_IMPORTED_MODULE_2__/* .extConfig */ .zO.coloredThumbs = value; } function handleColoredBarChangeEvent(value) { _state__WEBPACK_IMPORTED_MODULE_2__/* .extConfig */ .zO.coloredBar = value; } function handleColorThemeChangeEvent(value) { if (!value) value = "classic"; _state__WEBPACK_IMPORTED_MODULE_2__/* .extConfig */ .zO.colorTheme = value; } function handleNumberDisplayFormatChangeEvent(value) { _state__WEBPACK_IMPORTED_MODULE_2__/* .extConfig */ .zO.numberDisplayFormat = value; } function handleNumberDisplayReformatLikesChangeEvent(value) { _state__WEBPACK_IMPORTED_MODULE_2__/* .extConfig */ .zO.numberDisplayReformatLikes = value; } /***/ }), /***/ 935: /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ $L: () => (/* binding */ isLikesDisabled), /* harmony export */ AV: () => (/* binding */ LIKED_STATE), /* harmony export */ Gv: () => (/* binding */ DISLIKED_STATE), /* harmony export */ KY: () => (/* binding */ setInitialState), /* harmony export */ MB: () => (/* binding */ isRoundedDesign), /* harmony export */ Xq: () => (/* binding */ setLikes), /* harmony export */ am: () => (/* binding */ isNewDesign), /* harmony export */ kQ: () => (/* binding */ NEUTRAL_STATE), /* harmony export */ m8: () => (/* binding */ getLikeCountFromButton), /* harmony export */ ol: () => (/* binding */ isShorts), /* harmony export */ qg: () => (/* binding */ initExtConfig), /* harmony export */ r6: () => (/* binding */ setDislikes), /* harmony export */ tq: () => (/* binding */ isMobile), /* harmony export */ vk: () => (/* binding */ storedData), /* harmony export */ zO: () => (/* binding */ extConfig) /* harmony export */ }); /* unused harmony exports isVideoDisliked, isVideoLiked, getState, setState */ /* harmony import */ var _buttons__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(423); /* harmony import */ var _bar__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(910); /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(67); //TODO: Do not duplicate here and in ryd.background.js const apiUrl = "https://returnyoutubedislikeapi.com"; const LIKED_STATE = "LIKED_STATE"; const DISLIKED_STATE = "DISLIKED_STATE"; const NEUTRAL_STATE = "NEUTRAL_STATE"; let extConfig = { disableVoteSubmission: false, disableLogging: false, coloredThumbs: false, coloredBar: false, colorTheme: "classic", numberDisplayFormat: "compactShort", showTooltipPercentage: false, tooltipPercentageMode: "dash_like", numberDisplayReformatLikes: false, selectors: { dislikeTextContainer: [], likeTextContainer: [], buttons: { shorts: { mobile: [], desktop: [], }, regular: { mobile: [], desktopMenu: [], desktopNoMenu: [], }, likeButton: { segmented: [], segmentedGetButtons: [], notSegmented: [], }, dislikeButton: { segmented: [], segmentedGetButtons: [], notSegmented: [], }, }, menuContainer: [], roundedDesign: [], }, }; let storedData = { likes: 0, dislikes: 0, previousState: NEUTRAL_STATE, }; function isMobile() { return location.hostname == "m.youtube.com"; } function isShorts() { return location.pathname.startsWith("/shorts"); } function isNewDesign() { return document.getElementById("comment-teaser") !== null; } function isRoundedDesign() { return (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .querySelector */ .R2)(extConfig.selectors.roundedDesign) !== null; } let shortsObserver = null; if (isShorts() && !shortsObserver) { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .cLog */ .D2)("Initializing shorts mutation observer"); shortsObserver = (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .createObserver */ .zo)( { attributes: true, }, (mutationList) => { mutationList.forEach((mutation) => { if ( mutation.type === "attributes" && mutation.target.nodeName === "TP-YT-PAPER-BUTTON" && mutation.target.id === "button" ) { // cLog('Short thumb button status changed'); if (mutation.target.getAttribute("aria-pressed") === "true") { mutation.target.style.color = mutation.target.parentElement.parentElement.id === "like-button" ? (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getColorFromTheme */ .t4)(true) : (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getColorFromTheme */ .t4)(false); } else { mutation.target.style.color = "unset"; } return; } (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .cLog */ .D2)("Unexpected mutation observer event: " + mutation.target + mutation.type); }); }, ); } function isLikesDisabled() { // return true if the like button's text doesn't contain any number if (isMobile()) { return /^\D*$/.test((0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getButtons */ .hS)().children[0].querySelector(".button-renderer-text").innerText); } return /^\D*$/.test((0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getLikeTextContainer */ .Eq)().innerText); } function isVideoLiked() { if (isMobile()) { return (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getLikeButton */ .yN)().querySelector("button").getAttribute("aria-label") === "true"; } return ( (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getLikeButton */ .yN)().classList.contains("style-default-active") || (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getLikeButton */ .yN)().querySelector("button")?.getAttribute("aria-pressed") === "true" ); } function isVideoDisliked() { if (isMobile()) { return (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getDislikeButton */ .x8)().querySelector("button").getAttribute("aria-label") === "true"; } return ( (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getDislikeButton */ .x8)().classList.contains("style-default-active") || (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getDislikeButton */ .x8)().querySelector("button")?.getAttribute("aria-pressed") === "true" ); } function getState(storedData) { if (isVideoLiked()) { return { current: LIKED_STATE, previous: storedData.previousState }; } if (isVideoDisliked()) { return { current: DISLIKED_STATE, previous: storedData.previousState }; } return { current: NEUTRAL_STATE, previous: storedData.previousState }; } //--- Sets The Likes And Dislikes Values ---// function setLikes(likesCount) { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .cLog */ .D2)(`SET likes ${likesCount}`); (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getLikeTextContainer */ .Eq)().innerText = likesCount; } function setDislikes(dislikesCount) { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .cLog */ .D2)(`SET dislikes ${dislikesCount}`); const _container = (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getDislikeTextContainer */ .xP)(); _container?.removeAttribute("is-empty"); let _dislikeText if (!isLikesDisabled()) { if (isMobile()) { (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getButtons */ .hS)().children[1].querySelector(".button-renderer-text").innerText = dislikesCount; return; } _dislikeText = dislikesCount; } else { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .cLog */ .D2)("likes count disabled by creator"); if (isMobile()) { (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getButtons */ .hS)().children[1].querySelector(".button-renderer-text").innerText = (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .localize */ .NC)("TextLikesDisabled"); return; } _dislikeText = (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .localize */ .NC)("TextLikesDisabled"); } if (_dislikeText != null && _container?.innerText !== _dislikeText) { _container.innerText = _dislikeText; } } function getLikeCountFromButton() { try { if (isShorts()) { //Youtube Shorts don't work with this query. It's not necessary; we can skip it and still see the results. //It should be possible to fix this function, but it's not critical to showing the dislike count. return false; } let likeButton = (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getLikeButton */ .yN)().querySelector("yt-formatted-string#text") ?? (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getLikeButton */ .yN)().querySelector("button"); let likesStr = likeButton.getAttribute("aria-label").replace(/\D/g, ""); return likesStr.length > 0 ? parseInt(likesStr) : false; } catch { return false; } } function processResponse(response, storedData) { const formattedDislike = (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .numberFormat */ .Y4)(response.dislikes); setDislikes(formattedDislike); if (extConfig.numberDisplayReformatLikes === true) { const nativeLikes = getLikeCountFromButton(); if (nativeLikes !== false) { setLikes((0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .numberFormat */ .Y4)(nativeLikes)); } } storedData.dislikes = parseInt(response.dislikes); storedData.likes = getLikeCountFromButton() || parseInt(response.likes); (0,_bar__WEBPACK_IMPORTED_MODULE_1__/* .createRateBar */ .k)(storedData.likes, storedData.dislikes); if (extConfig.coloredThumbs === true) { if (isShorts()) { // for shorts, leave deactivated buttons in default color let shortLikeButton = (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getLikeButton */ .yN)().querySelector("tp-yt-paper-button#button"); let shortDislikeButton = (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getDislikeButton */ .x8)().querySelector("tp-yt-paper-button#button"); if (shortLikeButton.getAttribute("aria-pressed") === "true") { shortLikeButton.style.color = (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getColorFromTheme */ .t4)(true); } if (shortDislikeButton.getAttribute("aria-pressed") === "true") { shortDislikeButton.style.color = (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getColorFromTheme */ .t4)(false); } shortsObserver.observe(shortLikeButton); shortsObserver.observe(shortDislikeButton); } else { (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getLikeButton */ .yN)().style.color = (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getColorFromTheme */ .t4)(true); (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getDislikeButton */ .x8)().style.color = (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getColorFromTheme */ .t4)(false); } } //Temporary disabling this - it breaks all places where getButtons()[1] is used // createStarRating(response.rating, isMobile()); } // Tells the user if the API is down function displayError(error) { (0,_buttons__WEBPACK_IMPORTED_MODULE_0__/* .getDislikeTextContainer */ .xP)().innerText = (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .localize */ .NC)("textTempUnavailable"); } async function setState(storedData) { storedData.previousState = isVideoDisliked() ? DISLIKED_STATE : isVideoLiked() ? LIKED_STATE : NEUTRAL_STATE; let statsSet = false; (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .cLog */ .D2)("Video is loaded. Adding buttons..."); let videoId = (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getVideoId */ .gJ)(window.location.href); let likeCount = getLikeCountFromButton() || null; let response = await fetch(`${apiUrl}/votes?videoId=${videoId}&likeCount=${likeCount || ""}`, { method: "GET", headers: { Accept: "application/json", }, }) .then((response) => { if (!response.ok) displayError(response.error); return response; }) .then((response) => response.json()) .catch(displayError); (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .cLog */ .D2)("response from api:"); (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .cLog */ .D2)(JSON.stringify(response)); if (response !== undefined && !("traceId" in response) && !statsSet) { processResponse(response, storedData); } } async function setInitialState() { await setState(storedData); } async function initExtConfig() { initializeDisableVoteSubmission(); initializeDisableLogging(); initializeColoredThumbs(); initializeColoredBar(); initializeColorTheme(); initializeNumberDisplayFormat(); initializeTooltipPercentage(); initializeTooltipPercentageMode(); initializeNumberDisplayReformatLikes(); await initializeSelectors(); } async function initializeSelectors() { console.log("initializing selectors"); let result = await fetch(`${apiUrl}/configs/selectors`, { method: "GET", headers: { Accept: "application/json", }, }) .then((response) => response.json()) .catch((error) => {}); extConfig.selectors = result ?? extConfig.selectors; console.log(result); } function initializeDisableVoteSubmission() { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.get(["disableVoteSubmission"], (res) => { if (res.disableVoteSubmission === undefined) { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.set({ disableVoteSubmission: false }); } else { extConfig.disableVoteSubmission = res.disableVoteSubmission; } }); } function initializeDisableLogging() { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.get(["disableLogging"], (res) => { if (res.disableLogging === undefined) { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.set({ disableLogging: true }); } else { extConfig.disableLogging = res.disableLogging; } }); } function initializeColoredThumbs() { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.get(["coloredThumbs"], (res) => { if (res.coloredThumbs === undefined) { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.set({ coloredThumbs: false }); } else { extConfig.coloredThumbs = res.coloredThumbs; } }); } function initializeColoredBar() { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.get(["coloredBar"], (res) => { if (res.coloredBar === undefined) { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.set({ coloredBar: false }); } else { extConfig.coloredBar = res.coloredBar; } }); } function initializeColorTheme() { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.get(["colorTheme"], (res) => { if (res.colorTheme === undefined) { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.set({ colorTheme: false }); } else { extConfig.colorTheme = res.colorTheme; } }); } function initializeNumberDisplayFormat() { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.get(["numberDisplayFormat"], (res) => { if (res.numberDisplayFormat === undefined) { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.set({ numberDisplayFormat: "compactShort" }); } else { extConfig.numberDisplayFormat = res.numberDisplayFormat; } }); } function initializeTooltipPercentage() { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.get(["showTooltipPercentage"], (res) => { if (res.showTooltipPercentage === undefined) { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.set({ showTooltipPercentage: false }); } else { extConfig.showTooltipPercentage = res.showTooltipPercentage; } }); } function initializeTooltipPercentageMode() { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.get(["tooltipPercentageMode"], (res) => { if (res.tooltipPercentageMode === undefined) { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.set({ tooltipPercentageMode: "dash_like" }); } else { extConfig.tooltipPercentageMode = res.tooltipPercentageMode; } }); } function initializeNumberDisplayReformatLikes() { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.get(["numberDisplayReformatLikes"], (res) => { if (res.numberDisplayReformatLikes === undefined) { (0,_utils__WEBPACK_IMPORTED_MODULE_2__/* .getBrowser */ .qs)().storage.sync.set({ numberDisplayReformatLikes: false }); } else { extConfig.numberDisplayReformatLikes = res.numberDisplayReformatLikes; } }); } /***/ }), /***/ 67: /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ D2: () => (/* binding */ cLog), /* harmony export */ NC: () => (/* binding */ localize), /* harmony export */ R2: () => (/* binding */ querySelector), /* harmony export */ Wb: () => (/* binding */ querySelectorAll), /* harmony export */ Y4: () => (/* binding */ numberFormat), /* harmony export */ gJ: () => (/* binding */ getVideoId), /* harmony export */ qs: () => (/* binding */ getBrowser), /* harmony export */ t4: () => (/* binding */ getColorFromTheme), /* harmony export */ v4: () => (/* binding */ isInViewport), /* harmony export */ x8: () => (/* binding */ isVideoLoaded), /* harmony export */ zo: () => (/* binding */ createObserver) /* harmony export */ }); /* unused harmony export getNumberFormatter */ /* harmony import */ var _state__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(935); function numberFormat(numberState) { return getNumberFormatter(_state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.numberDisplayFormat).format(numberState); } function getNumberFormatter(optionSelect) { let userLocales; if (document.documentElement.lang) { userLocales = document.documentElement.lang; } else if (navigator.language) { userLocales = navigator.language; } else { try { userLocales = new URL( Array.from(document.querySelectorAll("head > link[rel='search']")) ?.find((n) => n?.getAttribute("href")?.includes("?locale=")) ?.getAttribute("href"), )?.searchParams?.get("locale"); } catch { cLog("Cannot find browser locale. Use en as default for number formatting."); userLocales = "en"; } } let formatterNotation; let formatterCompactDisplay; switch (optionSelect) { case "compactLong": formatterNotation = "compact"; formatterCompactDisplay = "long"; break; case "standard": formatterNotation = "standard"; formatterCompactDisplay = "short"; break; case "compactShort": default: formatterNotation = "compact"; formatterCompactDisplay = "short"; } const formatter = Intl.NumberFormat(userLocales, { notation: formatterNotation, compactDisplay: formatterCompactDisplay, }); return formatter; } function localize(localeString) { return chrome.i18n.getMessage(localeString); } function getBrowser() { if (typeof chrome !== "undefined" && typeof chrome.runtime !== "undefined") { return chrome; } else if (typeof browser !== "undefined" && typeof browser.runtime !== "undefined") { return browser; } else { console.log("browser is not supported"); return false; } } function getVideoId(url) { const urlObject = new URL(url); const pathname = urlObject.pathname; if (pathname.startsWith("/clip")) { return (document.querySelector("meta[itemprop='videoId']") || document.querySelector("meta[itemprop='identifier']")) .content; } else { if (pathname.startsWith("/shorts")) { return pathname.slice(8); } return urlObject.searchParams.get("v"); } } function isInViewport(element) { const rect = element.getBoundingClientRect(); const height = innerHeight || document.documentElement.clientHeight; const width = innerWidth || document.documentElement.clientWidth; return ( // When short (channel) is ignored, the element (like/dislike AND short itself) is // hidden with a 0 DOMRect. In this case, consider it outside of Viewport !(rect.top == 0 && rect.left == 0 && rect.bottom == 0 && rect.right == 0) && rect.top >= 0 && rect.left >= 0 && rect.bottom <= height && rect.right <= width ); } function isVideoLoaded() { const videoId = getVideoId(window.location.href); return ( // desktop: spring 2024 UI document.querySelector(`ytd-watch-grid[video-id='${videoId}']`) !== null || // desktop: older UI document.querySelector(`ytd-watch-flexy[video-id='${videoId}']`) !== null || // mobile: no video-id attribute document.querySelector('#player[loading="false"]:not([hidden])') !== null ); } function cLog(message, writer) { if (!_state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.disableLogging) { message = `[return youtube dislike]: ${message}`; if (writer) { writer(message); } else { console.log(message); } } } function getColorFromTheme(voteIsLike) { let colorString; switch (_state__WEBPACK_IMPORTED_MODULE_0__/* .extConfig */ .zO.colorTheme) { case "accessible": if (voteIsLike === true) { colorString = "dodgerblue"; } else { colorString = "gold"; } break; case "neon": if (voteIsLike === true) { colorString = "aqua"; } else { colorString = "magenta"; } break; case "classic": default: if (voteIsLike === true) { colorString = "lime"; } else { colorString = "red"; } } return colorString; } function querySelector(selectors, element) { let result; for (const selector of selectors) { result = (element ?? document).querySelector(selector); if (result !== null) { return result; } } } function querySelectorAll(selectors) { let result; for (const selector of selectors) { result = document.querySelectorAll(selector); if (result.length !== 0) { return result; } } return result; } function createObserver(options, callback) { const observerWrapper = new Object(); observerWrapper.options = options; observerWrapper.observer = new MutationObserver(callback); observerWrapper.observe = function (element) { this.observer.observe(element, this.options); }; observerWrapper.disconnect = function () { this.observer.disconnect(); }; return observerWrapper; } /***/ }) /******/ }); /************************************************************************/ /******/ // The module cache /******/ var __webpack_module_cache__ = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ var cachedModule = __webpack_module_cache__[moduleId]; /******/ if (cachedModule !== undefined) { /******/ return cachedModule.exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = __webpack_module_cache__[moduleId] = { /******/ // no module.id needed /******/ // no module.loaded needed /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /************************************************************************/ /******/ /* webpack/runtime/async module */ /******/ (() => { /******/ var webpackQueues = typeof Symbol === "function" ? Symbol("webpack queues") : "__webpack_queues__"; /******/ var webpackExports = typeof Symbol === "function" ? Symbol("webpack exports") : "__webpack_exports__"; /******/ var webpackError = typeof Symbol === "function" ? Symbol("webpack error") : "__webpack_error__"; /******/ var resolveQueue = (queue) => { /******/ if(queue && queue.d < 1) { /******/ queue.d = 1; /******/ queue.forEach((fn) => (fn.r--)); /******/ queue.forEach((fn) => (fn.r-- ? fn.r++ : fn())); /******/ } /******/ } /******/ var wrapDeps = (deps) => (deps.map((dep) => { /******/ if(dep !== null && typeof dep === "object") { /******/ if(dep[webpackQueues]) return dep; /******/ if(dep.then) { /******/ var queue = []; /******/ queue.d = 0; /******/ dep.then((r) => { /******/ obj[webpackExports] = r; /******/ resolveQueue(queue); /******/ }, (e) => { /******/ obj[webpackError] = e; /******/ resolveQueue(queue); /******/ }); /******/ var obj = {}; /******/ obj[webpackQueues] = (fn) => (fn(queue)); /******/ return obj; /******/ } /******/ } /******/ var ret = {}; /******/ ret[webpackQueues] = x => {}; /******/ ret[webpackExports] = dep; /******/ return ret; /******/ })); /******/ __webpack_require__.a = (module, body, hasAwait) => { /******/ var queue; /******/ hasAwait && ((queue = []).d = -1); /******/ var depQueues = new Set(); /******/ var exports = module.exports; /******/ var currentDeps; /******/ var outerResolve; /******/ var reject; /******/ var promise = new Promise((resolve, rej) => { /******/ reject = rej; /******/ outerResolve = resolve; /******/ }); /******/ promise[webpackExports] = exports; /******/ promise[webpackQueues] = (fn) => (queue && fn(queue), depQueues.forEach(fn), promise["catch"](x => {})); /******/ module.exports = promise; /******/ body((deps) => { /******/ currentDeps = wrapDeps(deps); /******/ var fn; /******/ var getResult = () => (currentDeps.map((d) => { /******/ if(d[webpackError]) throw d[webpackError]; /******/ return d[webpackExports]; /******/ })) /******/ var promise = new Promise((resolve) => { /******/ fn = () => (resolve(getResult)); /******/ fn.r = 0; /******/ var fnQueue = (q) => (q !== queue && !depQueues.has(q) && (depQueues.add(q), q && !q.d && (fn.r++, q.push(fn)))); /******/ currentDeps.map((dep) => (dep[webpackQueues](fnQueue))); /******/ }); /******/ return fn.r ? promise : getResult(); /******/ }, (err) => ((err ? reject(promise[webpackError] = err) : outerResolve(exports)), resolveQueue(queue))); /******/ queue && queue.d < 0 && (queue.d = 0); /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/define property getters */ /******/ (() => { /******/ // define getter functions for harmony exports /******/ __webpack_require__.d = (exports, definition) => { /******/ for(var key in definition) { /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); /******/ } /******/ } /******/ }; /******/ })(); /******/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ (() => { /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) /******/ })(); /******/ /************************************************************************/ /******/ /******/ // startup /******/ // Load entry module and return exports /******/ // This entry module used 'module' so it can't be inlined /******/ var __webpack_exports__ = __webpack_require__(805); /******/ /******/ })() ; // END: ryd.content-script.js ;} } catch(e) { _error(` Error executing scripts ${scriptPaths}`, e); } } else { _log(`Skipping document-idle phase (no document).`); } _log(`All execution phases complete, re-firing load events.`); document.dispatchEvent(new Event("DOMContentLoaded", { bubbles: true, cancelable: true })); } // #endregion // #region Event Listener No changes needed here --- window.addEventListener("message", (event) => { if (event.data.type === "openOptionsPage") { openOptionsPage(); } if (event.data.type === "openPopupPage") { openPopupPage(); } if (event.data.type === "closeOptionsPage") { closeOptionsModal(); } if (event.data.type === "closePopupPage") { closePopupModal(); } }); // #endregion // #region Refactored Modal Closing Functions Promise-based --- function closeOptionsModal() { return new Promise((resolve) => { const DURATION = 100; const backdrop = document.getElementById("extension-options-backdrop"); const modal = document.getElementById("extension-options-modal"); if (!backdrop || !modal) { return resolve(); } modal.style.animation = `modalCloseAnimation ${DURATION / 1000}s ease-out forwards`; backdrop.style.animation = `backdropFadeOut ${DURATION / 1000}s ease-out forwards`; setTimeout(() => { if (confirm("Close options and reload the page?")) { window.location.reload(); // Note: This will stop further execution } else { backdrop.remove(); } resolve(); }, DURATION); }); } function closePopupModal() { return new Promise((resolve) => { const DURATION = 100; const backdrop = document.getElementById("extension-popup-backdrop"); const modal = document.getElementById("extension-popup-modal"); if (!backdrop || !modal) { return resolve(); } modal.style.animation = `modalCloseAnimation ${DURATION / 1000}s ease-out forwards`; backdrop.style.animation = `backdropFadeOut ${DURATION / 1000}s ease-out forwards`; setTimeout(() => { backdrop.remove(); resolve(); }, DURATION); }); } // #endregion // #region Simplified Public API Functions --- async function openPopupPage() { if (!POPUP_PAGE_PATH || typeof EXTENSION_ASSETS_MAP === "undefined") { _warn("No popup page available."); return; } await openModal({ type: "popup", pagePath: POPUP_PAGE_PATH, defaultTitle: "Extension Popup", closeFn: closePopupModal, }); } async function openOptionsPage() { if (!OPTIONS_PAGE_PATH || typeof EXTENSION_ASSETS_MAP === "undefined") { _warn("No options page available."); return; } await openModal({ type: "options", pagePath: OPTIONS_PAGE_PATH, defaultTitle: "Extension Options", closeFn: closeOptionsModal, }); } // #endregion // #region Generic Modal Logic Style Injection --- let stylesInjected = false; function injectGlobalStyles() { if (stylesInjected) return; stylesInjected = true; const styles = ` .extension-backdrop { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.13); backdrop-filter: blur(3px); z-index: 2147483646; display: flex; align-items: center; justify-content: center; animation: backdropFadeIn 0.3s ease-out forwards; } .extension-modal { z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; --background: #ffffff; --rad: 10px; --border: #666; --border-thickness: 2px; display: flex; flex-direction: column; overflow: hidden; animation: modalOpenAnimation 0.3s ease-out forwards; } /* Size specific styles */ .extension-modal.popup-size { width: 400px; height: 600px; max-width: calc(100vw - 40px); max-height: calc(100vh - 40px); } .extension-modal.options-size { width: calc(100vw - 80px); height: calc(100vh - 80px); max-width: 1200px; max-height: 800px; } /* Common modal components */ .extension-modal .modal-header { display: flex; justify-content: space-between; align-items: flex-end; padding: 0 16px; position: relative; flex-shrink: 0; } .extension-modal .tab { padding: 12px 16px; color: #606266; display: flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; border-radius: var(--rad) var(--rad) 0 0; transition: background-color 0.2s ease; user-select: none; } .extension-modal .tab.active, .extension-modal .tab.close-button { background-color: var(--background); border: var(--border-thickness) solid var(--border); border-bottom-color: var(--background); margin-bottom: -1px; z-index: 1; color: #303133; font-weight: 500; } .extension-modal .tab.close-button { padding: 8px; } .extension-modal .tab.close-button:hover { background-color: #f5f7fa; } .extension-modal .tab svg { stroke: currentColor; } .extension-modal .tab.active img { width: 16px; height: 16px; } .extension-modal .tab.close-button svg { width: 20px; height: 20px; } .extension-modal .modal-content { flex-grow: 1; position: relative; border-radius: var(--rad); overflow: hidden; bottom: calc(var(--border-thickness) - 1px); border: var(--border-thickness) solid var(--border); } .extension-modal .modal-content iframe { width: 100%; height: 100%; border: 0; background: white; } /* Animations */ @keyframes backdropFadeIn { from { opacity: 0; backdrop-filter: blur(0px); } to { opacity: 1; backdrop-filter: blur(3px); } } @keyframes backdropFadeOut { from { opacity: 1; backdrop-filter: blur(3px); } to { opacity: 0; backdrop-filter: blur(0px); } } @keyframes modalOpenAnimation { from { transform: scaleY(0.8); opacity: 0; } to { transform: scaleY(1); opacity: 1; } } @keyframes modalCloseAnimation { from { transform: scaleY(1); opacity: 1; } to { transform: scaleY(0.8); opacity: 0; } } `; const styleSheet = document.createElement("style"); styleSheet.id = "extension-global-styles"; styleSheet.innerText = styles; document.head.appendChild(styleSheet); } async function openModal(config) { injectGlobalStyles(); const { type, pagePath, defaultTitle, closeFn } = config; const html = EXTENSION_ASSETS_MAP[pagePath]; if (!html) { _warn(`${defaultTitle} HTML not found in asset map`); return; } const backdropId = `extension-${type}-backdrop`; const modalId = `extension-${type}-modal`; const sizeClass = `${type}-size`; // #endregion // #region Smoothly close the other modal if it s open --- const otherType = type === "popup" ? "options" : "popup"; const otherBackdrop = document.getElementById( `extension-${otherType}-backdrop` ); if (otherBackdrop) { // Await the correct close function await (otherType === "popup" ? closePopupModal() : closeOptionsModal()); } let backdrop = document.getElementById(backdropId); let modal, iframe; if (!backdrop) { backdrop = document.createElement("div"); backdrop.id = backdropId; backdrop.className = "extension-backdrop"; modal = document.createElement("div"); modal.id = modalId; modal.className = `extension-modal ${sizeClass}`; const extensionName = INJECTED_MANIFEST.name || defaultTitle; const iconSrc = EXTENSION_ICON || "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBzdHJva2Utd2lkdGg9IjIiIGZpbGw9Im5vbmUiIHN0cm9rZT0iY3VycmVudENvbG9yIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIHN0cm9rZT0ibm9uZSIgZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik00IDdoM2ExIDEgMCAwIDAgMSAtMXYtMWEyIDIgMCAwIDEgNCAwdjFhMSAxIDAgMCAwIDEgMWgzYTEgMSAwIDAgMSAxIDF2M2ExIDEgMCAwIDAgMSAxaDFhMiAyIDAgMCAxIDAgNGgtMWExIDEgMCAwIDAgLTEgMXYzYTEgMSAwIDAgMSAtMSAxaC0zYTEgMSAwIDAgMSAtMSAtMXYtMWEyIDIgMCAwIDAgLTQgMHYxYTEgMSAwIDAgMSAtMSAxaC0zYTEgMSAwIDAgMSAtMSAtMXYtM2ExIDEgMCAwIDEgMSAtMWgxYTIgMiAwIDAgMCAwIC00aC0xYTEgMSAwIDAgMSAtMSAtMXYtM2ExIDEgMCAwIDEgMSAtMSIgLz48L3N2Zz4="; modal.innerHTML = ` `; backdrop.appendChild(modal); backdrop.addEventListener("click", (e) => { if (e.target === backdrop) closeFn(); }); modal.querySelector(".close-button").addEventListener("click", closeFn); document.body.appendChild(backdrop); iframe = modal.querySelector("iframe"); } else { // If it already exists, just make sure it's visible backdrop.style.display = "flex"; modal = backdrop.querySelector(".extension-modal"); iframe = modal.querySelector("iframe"); } // Load content into iframe try { const polyfillString = generateCompletePolyfillForIframe(); const doc = new DOMParser().parseFromString(html, "text/html"); const script = doc.createElement("script"); script.textContent = polyfillString; doc.head.insertAdjacentElement("afterbegin", script); iframe.srcdoc = doc.documentElement.outerHTML; } catch (e) { _error("Error generating complete polyfill for iframe", e); iframe.srcdoc = html; } } function generateCompletePolyfillForIframe() { const polyfillString = "\n// -- Messaging implementation\n\nfunction createEventBus(\n scopeId,\n type = \"page\", // \"page\" or \"iframe\"\n { allowedOrigin = \"*\", children = [], parentWindow = null } = {}\n) {\n if (!scopeId) throw new Error(\"createEventBus requires a scopeId\");\n\n const handlers = {};\n\n function handleIncoming(ev) {\n if (allowedOrigin !== \"*\" && ev.origin !== allowedOrigin) return;\n\n const msg = ev.data;\n if (!msg || msg.__eventBus !== true || msg.scopeId !== scopeId) return;\n\n const { event, payload } = msg;\n\n // PAGE: if it's an INIT from an iframe, adopt it\n if (type === \"page\" && event === \"__INIT__\") {\n const win = ev.source;\n if (win && !children.includes(win)) {\n children.push(win);\n }\n return;\n }\n\n (handlers[event] || []).forEach((fn) =>\n fn(payload, { origin: ev.origin, source: ev.source })\n );\n }\n\n window.addEventListener(\"message\", handleIncoming);\n\n function emitTo(win, event, payload) {\n const envelope = {\n __eventBus: true,\n scopeId,\n event,\n payload,\n };\n win.postMessage(envelope, allowedOrigin);\n }\n\n // IFRAME: announce to page on startup\n if (type === \"iframe\") {\n setTimeout(() => {\n const pw = parentWindow || window.parent;\n if (pw && pw.postMessage) {\n emitTo(pw, \"__INIT__\", null);\n }\n }, 0);\n }\n\n return {\n on(event, fn) {\n handlers[event] = handlers[event] || [];\n handlers[event].push(fn);\n },\n off(event, fn) {\n if (!handlers[event]) return;\n handlers[event] = handlers[event].filter((h) => h !== fn);\n },\n /**\n * Emits an event.\n * @param {string} event - The event name.\n * @param {any} payload - The event payload.\n * @param {object} [options] - Emission options.\n * @param {Window} [options.to] - A specific window to target. If provided, message is ONLY sent to the target.\n */\n emit(event, payload, { to } = {}) {\n // If a specific target window is provided, send only to it and DO NOT dispatch locally.\n // This prevents a port from receiving its own messages.\n if (to) {\n if (to && typeof to.postMessage === \"function\") {\n emitTo(to, event, payload);\n }\n return; // Exit after targeted send.\n }\n\n // For broadcast messages (no 'to' target), dispatch locally first.\n (handlers[event] || []).forEach((fn) =>\n fn(payload, { origin: location.origin, source: window })\n );\n\n // Then propagate the broadcast to other windows.\n if (type === \"page\") {\n children.forEach((win) => emitTo(win, event, payload));\n } else {\n const pw = parentWindow || window.parent;\n if (pw && pw.postMessage) {\n emitTo(pw, event, payload);\n }\n }\n },\n };\n}\n\nfunction createRuntime(type = \"background\", bus) {\n let nextId = 1;\n const pending = {};\n const msgListeners = [];\n\n let nextPortId = 1;\n const ports = {};\n const onConnectListeners = [];\n\n function parseArgs(args) {\n let target, message, options, callback;\n const arr = [...args];\n if (arr.length === 0) {\n throw new Error(\"sendMessage requires at least one argument\");\n }\n if (arr.length === 1) {\n return { message: arr[0] };\n }\n // last object could be options\n if (\n arr.length &&\n typeof arr[arr.length - 1] === \"object\" &&\n !Array.isArray(arr[arr.length - 1])\n ) {\n options = arr.pop();\n }\n // last function is callback\n if (arr.length && typeof arr[arr.length - 1] === \"function\") {\n callback = arr.pop();\n }\n if (\n arr.length === 2 &&\n (typeof arr[0] === \"string\" || typeof arr[0] === \"number\")\n ) {\n [target, message] = arr;\n } else {\n [message] = arr;\n }\n return { target, message, options, callback };\n }\n\n if (type === \"background\") {\n bus.on(\"__REQUEST__\", ({ id, message }, { source }) => {\n let responded = false,\n isAsync = false;\n function sendResponse(resp) {\n if (responded) return;\n responded = true;\n // Target the response directly back to the window that sent the request.\n bus.emit(\"__RESPONSE__\", { id, response: resp }, { to: source });\n }\n const results = msgListeners\n .map((fn) => {\n try {\n // msg, sender, sendResponse\n const ret = fn(message, { id, tab: { id: source } }, sendResponse);\n if (ret === true || (ret && typeof ret.then === \"function\")) {\n isAsync = true;\n return ret;\n }\n return ret;\n } catch (e) {\n _error(e);\n }\n })\n .filter((r) => r !== undefined);\n\n const promises = results.filter((r) => r && typeof r.then === \"function\");\n if (!isAsync && promises.length === 0) {\n const out = results.length === 1 ? results[0] : results;\n sendResponse(out);\n } else if (promises.length) {\n Promise.all(promises).then((vals) => {\n if (!responded) {\n const out = vals.length === 1 ? vals[0] : vals;\n sendResponse(out);\n }\n });\n }\n });\n }\n\n if (type !== \"background\") {\n bus.on(\"__RESPONSE__\", ({ id, response }) => {\n const entry = pending[id];\n if (!entry) return;\n entry.resolve(response);\n if (entry.callback) entry.callback(response);\n delete pending[id];\n });\n }\n\n function sendMessage(...args) {\n // Background should be able to send message to itself\n // if (type === \"background\") {\n // throw new Error(\"Background cannot sendMessage to itself\");\n // }\n const { target, message, callback } = parseArgs(args);\n const id = nextId++;\n const promise = new Promise((resolve) => {\n pending[id] = { resolve, callback };\n bus.emit(\"__REQUEST__\", { id, message });\n });\n return promise;\n }\n\n bus.on(\"__PORT_CONNECT__\", ({ portId, name }, { source }) => {\n if (type !== \"background\") return;\n const backgroundPort = makePort(\"background\", portId, name, source);\n ports[portId] = backgroundPort;\n\n onConnectListeners.forEach((fn) => fn(backgroundPort));\n\n // send back a CONNECT_ACK so the client can\n // start listening on its end:\n bus.emit(\"__PORT_CONNECT_ACK__\", { portId, name }, { to: source });\n });\n\n // Clients handle the ACK and finalize their Port object by learning the remote window.\n bus.on(\"__PORT_CONNECT_ACK__\", ({ portId, name }, { source }) => {\n if (type === \"background\") return; // ignore\n const p = ports[portId];\n if (!p) return;\n // Call the port's internal finalize method to complete the handshake\n if (p._finalize) {\n p._finalize(source);\n }\n });\n\n // Any port message travels via \"__PORT_MESSAGE__\"\n bus.on(\"__PORT_MESSAGE__\", (envelope, { source }) => {\n const { portId } = envelope;\n const p = ports[portId];\n if (!p) return;\n p._receive(envelope, source);\n });\n\n // Any port disconnect:\n bus.on(\"__PORT_DISCONNECT__\", ({ portId }) => {\n const p = ports[portId];\n if (!p) return;\n p._disconnect();\n delete ports[portId];\n });\n\n // Refactored makePort to correctly manage internal state and the connection handshake.\n function makePort(side, portId, name, remoteWindow) {\n let onMessageHandlers = [];\n let onDisconnectHandlers = [];\n let buffer = [];\n // Unique instance ID for this port instance\n const instanceId = Math.random().toString(36).slice(2) + Date.now();\n // These state variables are part of the closure and are updated by _finalize\n let _ready = side === \"background\";\n\n function _drainBuffer() {\n buffer.forEach((m) => _post(m));\n buffer = [];\n }\n\n function _post(msg) {\n // Always use the 'to' parameter for port messages, making them directional.\n // Include senderInstanceId\n bus.emit(\n \"__PORT_MESSAGE__\",\n { portId, msg, senderInstanceId: instanceId },\n { to: remoteWindow }\n );\n }\n\n function postMessage(msg) {\n if (!_ready) {\n buffer.push(msg);\n } else {\n _post(msg);\n }\n }\n\n function _receive(envelope, source) {\n // envelope: { msg, senderInstanceId }\n if (envelope.senderInstanceId === instanceId) return; // Don't dispatch to self\n onMessageHandlers.forEach((fn) =>\n fn(envelope.msg, { id: portId, tab: { id: source } })\n );\n }\n\n function disconnect() {\n // Also use the 'to' parameter for disconnect messages\n bus.emit(\"__PORT_DISCONNECT__\", { portId }, { to: remoteWindow });\n _disconnect();\n delete ports[portId];\n }\n\n function _disconnect() {\n onDisconnectHandlers.forEach((fn) => fn());\n onMessageHandlers = [];\n onDisconnectHandlers = [];\n }\n\n // This function is called on the client port when the ACK is received from background.\n // It updates the port's state, completing the connection.\n function _finalize(win) {\n remoteWindow = win; // <-- This is the crucial part: learn the destination\n _ready = true;\n _drainBuffer();\n }\n\n return {\n name,\n sender: {\n id: portId,\n },\n onMessage: {\n addListener(fn) {\n onMessageHandlers.push(fn);\n },\n removeListener(fn) {\n onMessageHandlers = onMessageHandlers.filter((x) => x !== fn);\n },\n },\n onDisconnect: {\n addListener(fn) {\n onDisconnectHandlers.push(fn);\n },\n removeListener(fn) {\n onDisconnectHandlers = onDisconnectHandlers.filter((x) => x !== fn);\n },\n },\n postMessage,\n disconnect,\n // Internal methods used by the runtime\n _receive,\n _disconnect,\n _finalize, // Expose the finalizer for the ACK handler\n };\n }\n\n function connect(connectInfo = {}) {\n if (type === \"background\") {\n throw new Error(\"Background must use onConnect, not connect()\");\n }\n const name = connectInfo.name || \"\";\n const portId = nextPortId++;\n // create the client side port\n // remoteWindow is initially null; it will be set by _finalize upon ACK.\n const clientPort = makePort(\"client\", portId, name, null);\n ports[portId] = clientPort;\n\n // fire the connect event across the bus\n bus.emit(\"__PORT_CONNECT__\", { portId, name });\n return clientPort;\n }\n\n function onConnect(fn) {\n if (type !== \"background\") {\n throw new Error(\"connect event only fires in background\");\n }\n onConnectListeners.push(fn);\n }\n\n return {\n // rpc:\n sendMessage,\n onMessage: {\n addListener(fn) {\n msgListeners.push(fn);\n },\n removeListener(fn) {\n const i = msgListeners.indexOf(fn);\n if (i >= 0) msgListeners.splice(i, 1);\n },\n },\n\n // port API:\n connect,\n onConnect: {\n addListener(fn) {\n onConnect(fn);\n },\n removeListener(fn) {\n const i = onConnectListeners.indexOf(fn);\n if (i >= 0) onConnectListeners.splice(i, 1);\n },\n },\n };\n}\n\n\n// --- Abstraction Layer: PostMessage Target\n\nlet nextRequestId = 1;\nconst pendingRequests = new Map(); // requestId -> { resolve, reject, timeout }\n\nfunction sendAbstractionRequest(method, args = []) {\n return new Promise((resolve, reject) => {\n const requestId = nextRequestId++;\n\n const timeout = setTimeout(() => {\n pendingRequests.delete(requestId);\n reject(new Error(`PostMessage request timeout for method: ${method}`));\n }, 10000);\n\n pendingRequests.set(requestId, { resolve, reject, timeout });\n\n window.parent.postMessage({\n type: \"abstraction-request\",\n requestId,\n method,\n args,\n });\n });\n}\n\nwindow.addEventListener(\"message\", (event) => {\n const { type, requestId, success, result, error } = event.data;\n\n if (type === \"abstraction-response\") {\n const pending = pendingRequests.get(requestId);\n if (pending) {\n clearTimeout(pending.timeout);\n pendingRequests.delete(requestId);\n\n if (success) {\n pending.resolve(result);\n } else {\n const err = new Error(error.message);\n err.stack = error.stack;\n pending.reject(err);\n }\n }\n }\n});\n\nasync function _storageSet(items) {\n return sendAbstractionRequest(\"_storageSet\", [items]);\n}\n\nasync function _storageGet(keys) {\n return sendAbstractionRequest(\"_storageGet\", [keys]);\n}\n\nasync function _storageRemove(keysToRemove) {\n return sendAbstractionRequest(\"_storageRemove\", [keysToRemove]);\n}\n\nasync function _storageClear() {\n return sendAbstractionRequest(\"_storageClear\");\n}\n\nasync function _cookieList(details) {\n return sendAbstractionRequest(\"_cookieList\", [details]);\n}\n\nasync function _cookieSet(details) {\n return sendAbstractionRequest(\"_cookieSet\", [details]);\n}\n\nasync function _cookieDelete(details) {\n return sendAbstractionRequest(\"_cookieDelete\", [details]);\n}\n\nasync function _fetch(url, options) {\n return sendAbstractionRequest(\"_fetch\", [url, options]);\n}\n\nfunction _registerMenuCommand(name, func) {\n _warn(\"_registerMenuCommand called from iframe context:\", name);\n return sendAbstractionRequest(\"_registerMenuCommand\", [\n name,\n func.toString(),\n ]);\n}\n\nfunction _openTab(url, active) {\n return sendAbstractionRequest(\"_openTab\", [url, active]);\n}\n\nasync function _initStorage() {\n return sendAbstractionRequest(\"_initStorage\");\n}\n\n\nconst EXTENSION_ASSETS_MAP = {{EXTENSION_ASSETS_MAP}};\n\n// -- Polyfill Implementation\nfunction buildPolyfill({ isBackground = false, isOtherPage = false } = {}) {\n // Generate a unique context ID for this polyfill instance\n const contextType = isBackground\n ? \"background\"\n : isOtherPage\n ? \"options\"\n : \"content\";\n const contextId = `${contextType}_${Math.random()\n .toString(36)\n .substring(2, 15)}`;\n\n const IS_IFRAME = \"true\" === \"true\";\n const BUS = (function () {\n if (globalThis.__BUS) {\n return globalThis.__BUS;\n }\n globalThis.__BUS = createEventBus(\n \"return-youtube-dislike\",\n IS_IFRAME ? \"iframe\" : \"page\",\n );\n return globalThis.__BUS;\n })();\n const RUNTIME = createRuntime(isBackground ? \"background\" : \"tab\", BUS);\n const createNoopListeners = () => ({\n addListener: (callback) => {\n _log(\"addListener\", callback);\n },\n removeListener: (callback) => {\n _log(\"removeListener\", callback);\n },\n });\n // TODO: Stub\n const storageChangeListeners = new Set();\n function broadcastStorageChange(changes, areaName) {\n storageChangeListeners.forEach((listener) => {\n listener(changes, areaName);\n });\n }\n\n let REQ_PERMS = [];\n\n // --- Chrome polyfill\n let chrome = {\n extension: {\n isAllowedIncognitoAccess: () => Promise.resolve(true),\n sendMessage: (...args) => _messagingHandler.sendMessage(...args),\n },\n permissions: {\n // TODO: Remove origin permission means exclude from origin in startup (when checking for content scripts)\n request: (permissions, callback) => {\n _log(\"permissions.request\", permissions, callback);\n if (Array.isArray(permissions)) {\n REQ_PERMS = [...REQ_PERMS, ...permissions];\n }\n if (typeof callback === \"function\") {\n callback(permissions);\n }\n return Promise.resolve(permissions);\n },\n contains: (permissions, callback) => {\n if (typeof callback === \"function\") {\n callback(true);\n }\n return Promise.resolve(true);\n },\n getAll: () => {\n return Promise.resolve({\n permissions: EXTENSION_PERMISSIONS,\n origins: ORIGIN_PERMISSIONS,\n });\n },\n onAdded: createNoopListeners(),\n onRemoved: createNoopListeners(),\n },\n i18n: {\n getUILanguage: () => {\n return USED_LOCALE || \"en\";\n },\n getMessage: (key, substitutions = []) => {\n if (typeof substitutions === \"string\") {\n substitutions = [substitutions];\n }\n if (typeof LOCALE_KEYS !== \"undefined\" && LOCALE_KEYS[key]) {\n return LOCALE_KEYS[key].message?.replace(\n /\\$(\\d+)/g,\n (match, p1) => substitutions[p1 - 1] || match,\n );\n }\n return key;\n },\n },\n alarms: {\n onAlarm: createNoopListeners(),\n create: () => {\n _log(\"alarms.create\", arguments);\n },\n get: () => {\n _log(\"alarms.get\", arguments);\n },\n },\n runtime: {\n ...RUNTIME,\n onInstalled: createNoopListeners(),\n onStartup: createNoopListeners(),\n // TODO: Postmessage to parent to open options page or call openOptionsPage\n openOptionsPage: () => {\n // const url = chrome.runtime.getURL(OPTIONS_PAGE_PATH);\n // console.log(\"openOptionsPage\", _openTab, url, EXTENSION_ASSETS_MAP);\n // _openTab(url);\n if (typeof openOptionsPage === \"function\") {\n openOptionsPage();\n } else if (window.parent) {\n window.parent.postMessage({ type: \"openOptionsPage\" }, \"*\");\n } else {\n _warn(\"openOptionsPage not available.\");\n }\n },\n getManifest: () => {\n // The manifest object will be injected into the scope where buildPolyfill is called\n if (typeof INJECTED_MANIFEST !== \"undefined\") {\n return JSON.parse(JSON.stringify(INJECTED_MANIFEST)); // Return deep copy\n }\n _warn(\"INJECTED_MANIFEST not found for chrome.runtime.getManifest\");\n return { name: \"Unknown\", version: \"0.0\", manifest_version: 2 };\n },\n getURL: (path) => {\n if (!path) return \"\";\n if (path.startsWith(\"/\")) {\n path = path.substring(1);\n }\n\n if (typeof _createAssetUrl === \"function\") {\n return _createAssetUrl(path);\n }\n\n _warn(\n `chrome.runtime.getURL fallback for '${path}'. Assets may not be available.`,\n );\n // Attempt a relative path resolution (highly context-dependent and likely wrong)\n try {\n if (window.location.protocol.startsWith(\"http\")) {\n return new URL(path, window.location.href).toString();\n }\n } catch (e) {\n /* ignore error, fallback */\n }\n return path;\n },\n id: \"polyfilled-extension-\" + Math.random().toString(36).substring(2, 15),\n lastError: null,\n setUninstallURL: () => {},\n setUpdateURL: () => {},\n getPlatformInfo: async () => {\n const platform = {\n os: \"unknown\",\n arch: \"unknown\",\n nacl_arch: \"unknown\",\n };\n\n if (typeof navigator !== \"undefined\") {\n const userAgent = navigator.userAgent.toLowerCase();\n if (userAgent.includes(\"mac\")) platform.os = \"mac\";\n else if (userAgent.includes(\"win\")) platform.os = \"win\";\n else if (userAgent.includes(\"linux\")) platform.os = \"linux\";\n else if (userAgent.includes(\"android\")) platform.os = \"android\";\n else if (userAgent.includes(\"ios\")) platform.os = \"ios\";\n\n if (userAgent.includes(\"x86_64\") || userAgent.includes(\"amd64\")) {\n platform.arch = \"x86-64\";\n } else if (userAgent.includes(\"i386\") || userAgent.includes(\"i686\")) {\n platform.arch = \"x86-32\";\n } else if (userAgent.includes(\"arm\")) {\n platform.arch = \"arm\";\n }\n }\n\n return platform;\n },\n getBrowserInfo: async () => {\n const info = {\n name: \"unknown\",\n version: \"unknown\",\n buildID: \"unknown\",\n };\n\n if (typeof navigator !== \"undefined\") {\n const userAgent = navigator.userAgent;\n if (userAgent.includes(\"Chrome\")) {\n info.name = \"Chrome\";\n const match = userAgent.match(/Chrome\\/([0-9.]+)/);\n if (match) info.version = match[1];\n } else if (userAgent.includes(\"Firefox\")) {\n info.name = \"Firefox\";\n const match = userAgent.match(/Firefox\\/([0-9.]+)/);\n if (match) info.version = match[1];\n } else if (userAgent.includes(\"Safari\")) {\n info.name = \"Safari\";\n const match = userAgent.match(/Version\\/([0-9.]+)/);\n if (match) info.version = match[1];\n }\n }\n\n return info;\n },\n },\n storage: {\n local: {\n get: function (keys, callback) {\n if (typeof _storageGet !== \"function\")\n throw new Error(\"_storageGet not defined\");\n\n const promise = _storageGet(keys);\n\n if (typeof callback === \"function\") {\n promise\n .then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.get callback:\", e);\n }\n })\n .catch((error) => {\n _error(\"Storage.get error:\", error);\n callback({});\n });\n return;\n }\n\n return promise;\n },\n set: function (items, callback) {\n if (typeof _storageSet !== \"function\")\n throw new Error(\"_storageSet not defined\");\n\n const promise = _storageSet(items).then((result) => {\n broadcastStorageChange(items, \"local\");\n return result;\n });\n\n if (typeof callback === \"function\") {\n promise\n .then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.set callback:\", e);\n }\n })\n .catch((error) => {\n _error(\"Storage.set error:\", error);\n callback();\n });\n return;\n }\n\n return promise;\n },\n remove: function (keys, callback) {\n if (typeof _storageRemove !== \"function\")\n throw new Error(\"_storageRemove not defined\");\n\n const promise = _storageRemove(keys).then((result) => {\n const changes = {};\n const keyList = Array.isArray(keys) ? keys : [keys];\n keyList.forEach((key) => {\n changes[key] = { oldValue: undefined, newValue: undefined };\n });\n broadcastStorageChange(changes, \"local\");\n return result;\n });\n\n if (typeof callback === \"function\") {\n promise\n .then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.remove callback:\", e);\n }\n })\n .catch((error) => {\n _error(\"Storage.remove error:\", error);\n callback();\n });\n return;\n }\n\n return promise;\n },\n clear: function (callback) {\n if (typeof _storageClear !== \"function\")\n throw new Error(\"_storageClear not defined\");\n\n const promise = _storageClear().then((result) => {\n broadcastStorageChange({}, \"local\");\n return result;\n });\n\n if (typeof callback === \"function\") {\n promise\n .then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.clear callback:\", e);\n }\n })\n .catch((error) => {\n _error(\"Storage.clear error:\", error);\n callback();\n });\n return;\n }\n\n return promise;\n },\n onChanged: {\n addListener: (callback) => {\n storageChangeListeners.add(callback);\n },\n removeListener: (callback) => {\n storageChangeListeners.delete(callback);\n },\n },\n },\n sync: {\n get: function (keys, callback) {\n _warn(\"chrome.storage.sync polyfill maps to local\");\n return chrome.storage.local.get(keys, callback);\n },\n set: function (items, callback) {\n _warn(\"chrome.storage.sync polyfill maps to local\");\n\n const promise = chrome.storage.local.set(items).then((result) => {\n broadcastStorageChange(items, \"sync\");\n return result;\n });\n\n if (typeof callback === \"function\") {\n promise\n .then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.sync.set callback:\", e);\n }\n })\n .catch((error) => {\n _error(\"Storage.sync.set error:\", error);\n callback();\n });\n return;\n }\n\n return promise;\n },\n remove: function (keys, callback) {\n _warn(\"chrome.storage.sync polyfill maps to local\");\n\n const promise = chrome.storage.local.remove(keys).then((result) => {\n const changes = {};\n const keyList = Array.isArray(keys) ? keys : [keys];\n keyList.forEach((key) => {\n changes[key] = { oldValue: undefined, newValue: undefined };\n });\n broadcastStorageChange(changes, \"sync\");\n return result;\n });\n\n if (typeof callback === \"function\") {\n promise\n .then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.sync.remove callback:\", e);\n }\n })\n .catch((error) => {\n _error(\"Storage.sync.remove error:\", error);\n callback();\n });\n return;\n }\n\n return promise;\n },\n clear: function (callback) {\n _warn(\"chrome.storage.sync polyfill maps to local\");\n\n const promise = chrome.storage.local.clear().then((result) => {\n broadcastStorageChange({}, \"sync\");\n return result;\n });\n\n if (typeof callback === \"function\") {\n promise\n .then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.sync.clear callback:\", e);\n }\n })\n .catch((error) => {\n _error(\"Storage.sync.clear error:\", error);\n callback();\n });\n return;\n }\n\n return promise;\n },\n onChanged: {\n addListener: (callback) => {\n storageChangeListeners.add(callback);\n },\n removeListener: (callback) => {\n storageChangeListeners.delete(callback);\n },\n },\n },\n onChanged: {\n addListener: (callback) => {\n storageChangeListeners.add(callback);\n },\n removeListener: (callback) => {\n storageChangeListeners.delete(callback);\n },\n },\n managed: {\n get: function (keys, callback) {\n _warn(\"chrome.storage.managed polyfill is read-only empty.\");\n\n const promise = Promise.resolve({});\n\n if (typeof callback === \"function\") {\n promise.then((result) => {\n try {\n callback(result);\n } catch (e) {\n _error(\"Error in storage.managed.get callback:\", e);\n }\n });\n return;\n }\n\n return promise;\n },\n },\n },\n cookies: (function () {\n const cookieChangeListeners = new Set();\n function broadcastCookieChange(changeInfo) {\n cookieChangeListeners.forEach((listener) => {\n try {\n listener(changeInfo);\n } catch (e) {\n _error(\"Error in cookies.onChanged listener:\", e);\n }\n });\n }\n\n function handlePromiseCallback(promise, callback) {\n if (typeof callback === \"function\") {\n promise\n .then((result) => callback(result))\n .catch((error) => {\n // chrome.runtime.lastError = { message: error.message }; // TODO: Implement lastError\n _error(error);\n callback(); // Call with undefined on error\n });\n return;\n }\n return promise;\n }\n\n return {\n get: function (details, callback) {\n if (typeof _cookieList !== \"function\") {\n return handlePromiseCallback(\n Promise.reject(new Error(\"_cookieList not defined\")),\n callback,\n );\n }\n const promise = _cookieList({\n url: details.url,\n name: details.name,\n storeId: details.storeId,\n partitionKey: details.partitionKey,\n }).then((cookies) => {\n if (!cookies || cookies.length === 0) {\n return null;\n }\n // Sort by path length (longest first), then creation time (earliest first, if available)\n cookies.sort((a, b) => {\n const pathLenDiff = (b.path || \"\").length - (a.path || \"\").length;\n if (pathLenDiff !== 0) return pathLenDiff;\n return (a.creationTime || 0) - (b.creationTime || 0);\n });\n return cookies[0];\n });\n return handlePromiseCallback(promise, callback);\n },\n\n getAll: function (details, callback) {\n if (typeof _cookieList !== \"function\") {\n return handlePromiseCallback(\n Promise.reject(new Error(\"_cookieList not defined\")),\n callback,\n );\n }\n if (details.partitionKey) {\n _warn(\n \"cookies.getAll: partitionKey is not fully supported in this environment.\",\n );\n }\n const promise = _cookieList(details);\n return handlePromiseCallback(promise, callback);\n },\n\n set: function (details, callback) {\n const promise = (async () => {\n if (\n typeof _cookieSet !== \"function\" ||\n typeof _cookieList !== \"function\"\n ) {\n throw new Error(\"_cookieSet or _cookieList not defined\");\n }\n if (details.partitionKey) {\n _warn(\n \"cookies.set: partitionKey is not fully supported in this environment.\",\n );\n }\n\n const getDetails = {\n url: details.url,\n name: details.name,\n storeId: details.storeId,\n };\n const oldCookies = await _cookieList(getDetails);\n const oldCookie = oldCookies && oldCookies[0];\n\n if (oldCookie) {\n broadcastCookieChange({\n cause: \"overwrite\",\n cookie: oldCookie,\n removed: true,\n });\n }\n\n await _cookieSet(details);\n const newCookies = await _cookieList(getDetails);\n const newCookie = newCookies && newCookies[0];\n\n if (newCookie) {\n broadcastCookieChange({\n cause: \"explicit\",\n cookie: newCookie,\n removed: false,\n });\n }\n return newCookie || null;\n })();\n return handlePromiseCallback(promise, callback);\n },\n\n remove: function (details, callback) {\n const promise = (async () => {\n if (\n typeof _cookieDelete !== \"function\" ||\n typeof _cookieList !== \"function\"\n ) {\n throw new Error(\"_cookieDelete or _cookieList not defined\");\n }\n const oldCookies = await _cookieList(details);\n const oldCookie = oldCookies && oldCookies[0];\n\n if (!oldCookie) return null; // Nothing to remove\n\n await _cookieDelete(details);\n\n broadcastCookieChange({\n cause: \"explicit\",\n cookie: oldCookie,\n removed: true,\n });\n\n return {\n url: details.url,\n name: details.name,\n storeId: details.storeId || \"0\",\n partitionKey: details.partitionKey,\n };\n })();\n return handlePromiseCallback(promise, callback);\n },\n\n getAllCookieStores: function (callback) {\n const promise = Promise.resolve([\n { id: \"0\", tabIds: [1] }, // Mock store for the current context\n ]);\n return handlePromiseCallback(promise, callback);\n },\n\n getPartitionKey: function (details, callback) {\n _warn(\n \"chrome.cookies.getPartitionKey is not supported in this environment.\",\n );\n const promise = Promise.resolve({ partitionKey: {} }); // Return empty partition key\n return handlePromiseCallback(promise, callback);\n },\n\n onChanged: {\n addListener: (callback) => {\n if (typeof callback === \"function\") {\n cookieChangeListeners.add(callback);\n }\n },\n removeListener: (callback) => {\n cookieChangeListeners.delete(callback);\n },\n },\n };\n })(),\n tabs: {\n query: async (queryInfo) => {\n _warn(\"chrome.tabs.query polyfill only returns current tab info.\");\n const dummyId = Math.floor(Math.random() * 1000) + 1;\n return [\n {\n id: dummyId,\n url: CURRENT_LOCATION,\n active: true,\n windowId: 1,\n status: \"complete\",\n },\n ];\n },\n create: async ({ url, active = true }) => {\n _log(`[Polyfill tabs.create] URL: ${url}`);\n if (typeof _openTab !== \"function\")\n throw new Error(\"_openTab not defined\");\n _openTab(url, active);\n const dummyId = Math.floor(Math.random() * 1000) + 1001;\n return Promise.resolve({\n id: dummyId,\n url: url,\n active,\n windowId: 1,\n });\n },\n sendMessage: async (tabId, message) => {\n _warn(\n `chrome.tabs.sendMessage polyfill (to tab ${tabId}) redirects to runtime.sendMessage (current context).`,\n );\n return chrome.runtime.sendMessage(message);\n },\n onActivated: createNoopListeners(),\n onUpdated: createNoopListeners(),\n onRemoved: createNoopListeners(),\n onReplaced: createNoopListeners(),\n onCreated: createNoopListeners(),\n onMoved: createNoopListeners(),\n onDetached: createNoopListeners(),\n onAttached: createNoopListeners(),\n },\n windows: {\n onFocusChanged: createNoopListeners(),\n onCreated: createNoopListeners(),\n onRemoved: createNoopListeners(),\n onFocused: createNoopListeners(),\n onFocus: createNoopListeners(),\n onBlur: createNoopListeners(),\n onFocused: createNoopListeners(),\n },\n notifications: {\n create: async (notificationId, options) => {\n try {\n let id = notificationId;\n let notificationOptions = options;\n\n if (typeof notificationId === \"object\" && notificationId !== null) {\n notificationOptions = notificationId;\n id = \"notification_\" + Math.random().toString(36).substring(2, 15);\n } else if (typeof notificationId === \"string\" && options) {\n id = notificationId;\n notificationOptions = options;\n } else {\n throw new Error(\"Invalid parameters for notifications.create\");\n }\n\n if (!notificationOptions || typeof notificationOptions !== \"object\") {\n throw new Error(\"Notification options must be an object\");\n }\n\n const {\n title,\n message,\n iconUrl,\n type = \"basic\",\n } = notificationOptions;\n\n if (!title || !message) {\n throw new Error(\"Notification must have title and message\");\n }\n\n if (\"Notification\" in window) {\n if (Notification.permission === \"granted\") {\n const notification = new Notification(title, {\n body: message,\n icon: iconUrl,\n tag: id,\n });\n\n _log(`[Notifications] Created notification: ${id}`);\n return id;\n } else if (Notification.permission === \"default\") {\n const permission = await Notification.requestPermission();\n if (permission === \"granted\") {\n const notification = new Notification(title, {\n body: message,\n icon: iconUrl,\n tag: id,\n });\n _log(\n `[Notifications] Created notification after permission: ${id}`,\n );\n return id;\n } else {\n _warn(\"[Notifications] Permission denied for notifications\");\n return id;\n }\n } else {\n _warn(\"[Notifications] Notifications are blocked\");\n return id;\n }\n } else {\n _warn(\n \"[Notifications] Native notifications not supported, using console fallback\",\n );\n _log(`[Notification] ${title}: ${message}`);\n return id;\n }\n } catch (error) {\n _error(\"[Notifications] Error creating notification:\", error.message);\n throw error;\n }\n },\n clear: async (notificationId) => {\n _log(`[Notifications] Clear notification: ${notificationId}`);\n // For native notifications, there's no direct way to clear by ID\n // This is a limitation of the Web Notifications API\n return true;\n },\n getAll: async () => {\n _warn(\"[Notifications] getAll not fully supported in polyfill\");\n return {};\n },\n getPermissionLevel: async () => {\n if (\"Notification\" in window) {\n const permission = Notification.permission;\n return { level: permission === \"granted\" ? \"granted\" : \"denied\" };\n }\n return { level: \"denied\" };\n },\n },\n contextMenus: {\n create: (createProperties, callback) => {\n try {\n if (!createProperties || typeof createProperties !== \"object\") {\n throw new Error(\"Context menu create properties must be an object\");\n }\n\n const { id, title, contexts = [\"page\"], onclick } = createProperties;\n const menuId =\n id || `menu_${Math.random().toString(36).substring(2, 15)}`;\n\n if (!title || typeof title !== \"string\") {\n throw new Error(\"Context menu must have a title\");\n }\n\n // Store menu items for potential use\n if (!window._polyfillContextMenus) {\n window._polyfillContextMenus = new Map();\n }\n\n window._polyfillContextMenus.set(menuId, {\n id: menuId,\n title,\n contexts,\n onclick,\n enabled: createProperties.enabled !== false,\n });\n\n _log(\n `[ContextMenus] Created context menu item: ${title} (${menuId})`,\n );\n\n // Try to register a menu command as fallback\n if (typeof _registerMenuCommand === \"function\") {\n try {\n _registerMenuCommand(\n title,\n onclick ||\n (() => {\n _log(`Context menu clicked: ${title}`);\n }),\n );\n } catch (e) {\n _warn(\n \"[ContextMenus] Failed to register as menu command:\",\n e.message,\n );\n }\n }\n\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n\n return menuId;\n } catch (error) {\n _error(\"[ContextMenus] Error creating context menu:\", error.message);\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n throw error;\n }\n },\n update: (id, updateProperties, callback) => {\n try {\n if (\n !window._polyfillContextMenus ||\n !window._polyfillContextMenus.has(id)\n ) {\n throw new Error(`Context menu item not found: ${id}`);\n }\n\n const menuItem = window._polyfillContextMenus.get(id);\n Object.assign(menuItem, updateProperties);\n\n _log(`[ContextMenus] Updated context menu item: ${id}`);\n\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n } catch (error) {\n _error(\"[ContextMenus] Error updating context menu:\", error.message);\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n }\n },\n remove: (menuItemId, callback) => {\n try {\n if (\n window._polyfillContextMenus &&\n window._polyfillContextMenus.has(menuItemId)\n ) {\n window._polyfillContextMenus.delete(menuItemId);\n _log(`[ContextMenus] Removed context menu item: ${menuItemId}`);\n } else {\n _warn(\n `[ContextMenus] Context menu item not found for removal: ${menuItemId}`,\n );\n }\n\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n } catch (error) {\n _error(\"[ContextMenus] Error removing context menu:\", error.message);\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n }\n },\n removeAll: (callback) => {\n try {\n if (window._polyfillContextMenus) {\n const count = window._polyfillContextMenus.size;\n window._polyfillContextMenus.clear();\n _log(`[ContextMenus] Removed all ${count} context menu items`);\n }\n\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n } catch (error) {\n _error(\n \"[ContextMenus] Error removing all context menus:\",\n error.message,\n );\n if (callback && typeof callback === \"function\") {\n setTimeout(() => callback(), 0);\n }\n }\n },\n onClicked: {\n addListener: (callback) => {\n if (!window._polyfillContextMenuListeners) {\n window._polyfillContextMenuListeners = new Set();\n }\n window._polyfillContextMenuListeners.add(callback);\n _log(\"[ContextMenus] Added click listener\");\n },\n removeListener: (callback) => {\n if (window._polyfillContextMenuListeners) {\n window._polyfillContextMenuListeners.delete(callback);\n _log(\"[ContextMenus] Removed click listener\");\n }\n },\n },\n },\n };\n\n const tc = (fn) => {\n try {\n fn();\n } catch (e) {}\n };\n const loggingProxyHandler = (_key) => ({\n get(target, key, receiver) {\n tc(() => _log(`[${contextType}] [CHROME - ${_key}] Getting ${key}`));\n return Reflect.get(target, key, receiver);\n },\n set(target, key, value, receiver) {\n tc(() =>\n _log(`[${contextType}] [CHROME - ${_key}] Setting ${key} to ${value}`),\n );\n return Reflect.set(target, key, value, receiver);\n },\n has(target, key) {\n tc(() =>\n _log(`[${contextType}] [CHROME - ${_key}] Checking if ${key} exists`),\n );\n return Reflect.has(target, key);\n },\n });\n chrome = Object.fromEntries(\n Object.entries(chrome).map(([key, value]) => [\n key,\n new Proxy(value, loggingProxyHandler(key)),\n ]),\n );\n\n // Alias browser to chrome for common Firefox pattern\n const browser = new Proxy(chrome, loggingProxyHandler);\n\n const oldGlobalThis = globalThis;\n const oldWindow = window;\n const oldSelf = self;\n const oldGlobal = globalThis;\n const __globalsStorage = {};\n\n const TO_MODIFY = [oldGlobalThis, oldWindow, oldSelf, oldGlobal];\n const set = (k, v) => {\n __globalsStorage[k] = v;\n TO_MODIFY.forEach((target) => {\n target[k] = v;\n });\n };\n const proxyHandler = {\n get(target, key, receiver) {\n const fns = [\n () => __globalsStorage[key],\n () => Reflect.get(target, key, target),\n () => target[key],\n ];\n const out = fns\n .map((f) => {\n try {\n let out = f();\n return out;\n } catch (e) {\n return undefined;\n }\n })\n .find((f) => f !== undefined);\n if (typeof out === \"function\") {\n return out.bind(target);\n }\n return out;\n },\n set(target, key, value, receiver) {\n try {\n tc(() => _log(`[${contextType}] Setting ${key} to ${value}`));\n set(key, value);\n return Reflect.set(target, key, value, receiver);\n } catch (e) {\n _error(\"Error setting\", key, value, e);\n try {\n target[key] = value;\n return true;\n } catch (e) {\n _error(\"Error setting\", key, value, e);\n }\n return false;\n }\n },\n has(target, key) {\n try {\n return key in __globalsStorage || key in target;\n } catch (e) {\n _error(\"Error has\", key, e);\n try {\n return key in __globalsStorage || key in target;\n } catch (e) {\n _error(\"Error has\", key, e);\n }\n return false;\n }\n },\n getOwnPropertyDescriptor(target, key) {\n try {\n if (key in __globalsStorage) {\n return {\n configurable: true,\n enumerable: true,\n writable: true,\n value: __globalsStorage[key],\n };\n }\n // fall back to the real globalThis\n const desc = Reflect.getOwnPropertyDescriptor(target, key);\n // ensure it's configurable so the with‑scope binding logic can override it\n if (desc && !desc.configurable) {\n desc.configurable = true;\n }\n return desc;\n } catch (e) {\n _error(\"Error getOwnPropertyDescriptor\", key, e);\n return {\n configurable: true,\n enumerable: true,\n writable: true,\n value: undefined,\n };\n }\n },\n\n defineProperty(target, key, descriptor) {\n try {\n // Normalize descriptor to avoid mixed accessor & data attributes\n const hasAccessor = \"get\" in descriptor || \"set\" in descriptor;\n\n if (hasAccessor) {\n // Build a clean descriptor without value/writable when accessors present\n const normalized = {\n configurable:\n \"configurable\" in descriptor ? descriptor.configurable : true,\n enumerable:\n \"enumerable\" in descriptor ? descriptor.enumerable : false,\n };\n if (\"get\" in descriptor) normalized.get = descriptor.get;\n if (\"set\" in descriptor) normalized.set = descriptor.set;\n\n // Store accessor references for inspection but avoid breaking invariants\n set(key, {\n get: descriptor.get,\n set: descriptor.set,\n });\n\n return Reflect.defineProperty(target, key, normalized);\n }\n\n // Data descriptor path\n set(key, descriptor.value);\n return Reflect.defineProperty(target, key, descriptor);\n } catch (e) {\n _error(\"Error defineProperty\", key, descriptor, e);\n return false;\n }\n },\n };\n\n // Create proxies once proxyHandler is defined\n const proxyWindow = new Proxy(oldWindow, proxyHandler);\n const proxyGlobalThis = new Proxy(oldGlobalThis, proxyHandler);\n const proxyGlobal = new Proxy(oldGlobal, proxyHandler);\n const proxySelf = new Proxy(oldSelf, proxyHandler);\n\n // Seed storage with core globals so lookups succeed inside `with` blocks\n Object.assign(__globalsStorage, {\n chrome,\n browser,\n window: proxyWindow,\n globalThis: proxyGlobalThis,\n global: proxyGlobal,\n self: proxySelf,\n document: oldWindow.document,\n });\n\n const __globals = {\n chrome,\n browser,\n window: proxyWindow,\n globalThis: proxyGlobalThis,\n global: proxyGlobal,\n self: proxySelf,\n __globals: __globalsStorage,\n };\n\n __globals.contextId = contextId;\n __globals.contextType = contextType;\n __globals.module = undefined;\n __globals.amd = undefined;\n __globals.define = undefined;\n __globals.importScripts = (...args) => {\n _log(\"importScripts\", args);\n };\n\n return __globals;\n}\n\n\nif (typeof window !== 'undefined') {\n window.buildPolyfill = buildPolyfill;\n}\n" let newMap = JSON.parse(JSON.stringify(EXTENSION_ASSETS_MAP)); delete newMap[OPTIONS_PAGE_PATH]; const PASS_ON = Object.fromEntries( Object.entries({ LOCALE_KEYS, INJECTED_MANIFEST, USED_LOCALE, EXTENSION_ICON, CURRENT_LOCATION, OPTIONS_PAGE_PATH, CAN_USE_BLOB_CSP, ALL_PERMISSIONS, ORIGIN_PERMISSIONS, EXTENSION_PERMISSIONS, SCRIPT_NAME, _base64ToBlob, _getMimeTypeFromPath, _isTextAsset, _createAssetUrl, _matchGlobPattern, _isWebAccessibleResource, _log, _warn, _error, }).map((i) => { let out = [...i]; if (typeof i[1] === "function") { out[1] = i[1].toString(); } else { out[1] = JSON.stringify(i[1]); } return out; }) ); _log(PASS_ON); return ` ${Object.entries(PASS_ON) .map( (i) => `const ${i[0]} = ${i[1]};\nwindow[${JSON.stringify(i[0])}] = ${i[0]}` ) .join("\n")} _log("Initialized polyfill", {${Object.keys(PASS_ON).join(", ")}}) ${polyfillString.replaceAll("{{EXTENSION_ASSETS_MAP}}", `JSON.parse(unescape(atob("${btoa(encodeURIComponent(JSON.stringify(EXTENSION_ASSETS_MAP)))}")))`)} // Initialize the polyfill context for options page const polyfillCtx = buildPolyfill({ isOtherPage: true }); const APPLY_TO = [window, self, globalThis]; for (const obj of APPLY_TO) { obj.chrome = polyfillCtx.chrome; obj.browser = polyfillCtx.browser; obj.INJECTED_MANIFEST = ${JSON.stringify(INJECTED_MANIFEST)}; } `; } async function main() { _log(`Initializing...`, performance.now()); if (typeof _initStorage === "function") { try { _initStorage() .then(() => { _log(`Storage initialized.`); }) .catch((e) => { _error("Error during storage initialization:", e); }); } catch (e) { _error("Error during storage initialization:", e); } } _log(`Starting content scripts...`); const currentUrl = window.location.href; let shouldRunAnyScript = false; _log(`Checking URL: ${currentUrl}`); if ( CONTENT_SCRIPT_CONFIGS_FOR_MATCHING && CONTENT_SCRIPT_CONFIGS_FOR_MATCHING.length > 0 ) { for (const config of CONTENT_SCRIPT_CONFIGS_FOR_MATCHING) { if ( config.matches && config.matches.some((pattern) => { try { const regex = convertMatchPatternToRegExp(pattern); if (regex.test(currentUrl)) { return true; } return false; } catch (e) { _error(`Error testing match pattern "${pattern}":`, e); return false; } }) ) { shouldRunAnyScript = true; _log(`URL match found via config:`, config); break; } } } else { _log(`No content script configurations found in manifest data.`); } if (shouldRunAnyScript) { let polyfillContext; try { polyfillContext = buildPolyfill({ isBackground: false }); } catch (e) { _error(`Failed to build polyfill:`, e); return; } _log(`Polyfill built. Executing combined script logic...`); // async function executeAllScripts({chrome, browser, global, window, globalThis, self, __globals}, extensionCssData) { await executeAllScripts.call( polyfillContext.globalThis, polyfillContext, extensionCssData ); } else { _log( `No matching content script patterns for this URL. No scripts will be executed.` ); } if (OPTIONS_PAGE_PATH) { if (typeof _registerMenuCommand === "function") { try { _registerMenuCommand("Open Options", openOptionsPage); _log(`Options menu command registered.`); } catch (e) { _error("Failed to register menu command", e); } } } if (POPUP_PAGE_PATH) { if (typeof _registerMenuCommand === "function") { try { _registerMenuCommand("Open Popup", openPopupPage); _log(`Popup menu command registered.`); } catch (e) { _error("Failed to register popup menu command", e); } } } _log(`Initialization sequence complete.`); } main()//.catch((e) => _error(`Error during script initialization:`, e)); try { const fnKey = "OPEN_OPTIONS_PAGE_" + String(SCRIPT_NAME).replace(/\s+/g, "_"); window[fnKey] = openOptionsPage; } catch (e) {} try { const fnKey = "OPEN_POPUP_PAGE_" + String(SCRIPT_NAME).replace(/\s+/g, "_"); window[fnKey] = openPopupPage; } catch (e) {} })(); // #endregion // #endregion // #endregion