// ==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 \r\n
\r\n \r\n \r\n \r\n
\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"
};
// #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",
`
`,
);
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