// ==UserScript==
// @name Iridium for YouTube
// @version 2.1.2
// @description Take control of YouTube and boost your user experience with Iridium
// @namespace iridium-for-youtube
// @author Converter Script
// @match *://www.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,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABSBJREFUeNrUWktsG0UYnsnaUXjGVZS0gEQj0UO5IKuq1F4A3yIuODcOSDQ5IK41NyQk8AGJW+wrQmoi7pDcKJd1b6lASrigSCDFEq/KLY1bKSU0sYfvX8+uZ8czs7bjV1b6NLNee/f/Zr7/MbNmzHIIJnxgDciws3g0OQhwIUDgAP2VM0fgaVr4gDhORdjBee7MEDh8VviAIDx5poV/ZwJ8ByxOip3cdqE+K3w0wYhzIb8s2sBRBErPH/L6RBKozQsfhloJSFSJyOxjvj5xBP58GQRoBuzGq6jgu8VzB7wyMQSqF1sS4l0QmGpG/XUiMvcPr46dwK+XeidALVDHTcvz9/nnYyXwy+txJ04ioJEgVHFeWLjPN8dC4Oc3DARaKZpNUT80WPYNBJjXDK5ViMj5Gt8dKYEfr8ajkColG4EA9FlDI9KQ/tEiMtCwO2W7cJyO4+m00levpbTvaucn8rzhsZXmFNu/d17cHMkM3Hmr7cQmR1Ylo0tHHflQSjQrXiP6rIqZWl2onT7splwz4EpiMGIXbQbtookAGYoRD/pNMpy3zgm4tgj4tQVRgfxW5x/0H3ZPQ+AR2i0iAOQlmWi0Q+OJiOCR4UE/QoPlPMb2H8yJEn5fnHvYu3907QMOVIEytH4bOFJ94ETr29BIsZsgtP/wXO9lu5VA6LSq8yZgWxLZVo2PGZtWoH7uBW2m6bFbILHz6MXuy3anhFxZ2JK8jiCV22jvYkTzJC+haJ98oRnKh7WlxFi7BZcs7uk/fkFsQo6F5w7d/jFQAorj1tFu4Pwy2iW0mZj2HQSU0LiMey9jHVLE/Uoz/5n9oysn7jBeT14SquPKUd9Dfw/G5YBrwIzNaMOiPHzeZ3jejeO0KKaPO8v2/ggkxH5DxKkA28ASkNUJxEZfGJMThepbJ564gfsWOWvnj4ES6DBeap9anB8BW8Bd3HaJjIplVGGdARU5PCcnhKCZICLVgRBQY39gvOK0nq59zu7hthtoLwdEOMvoRKxVr5QunrFC/iGYKKdcYVQtI2IFnK59aqcU7RsMl4Nq1r1S7UawDZzX6tOzeopCLgIO7ZsizQWThFQyLHnhtA57EJ0SJNQtgSTjpeEUgcjwbEdFaZNNM248KtoKGT79pA8njkKnFkI9k/bbjhuCMus1ItGL3httAnU8i1Z3/YfRmOFtZ4qM17XPWsmKMnEeZ52OytyaD/VOI94QrPTq76dMZKbkpRqvyYcyb14ScCarSPPNDgKbeGbh0l6/pcR03JlMBAzGk87fBq5bV1DJWzO7ZHj2p+4WO/Ywmk5OXqrx6F+Xxs9YM6xb73VaE+Tu8NLAFjQ2AuFSURq+COSpYGNagWbMsGa9l2BJcemH3hc0PRHQirZZtO9RxdlNgWaRC4XF1Xe/HdKS0kZAzkLWkayS9E6bXqsffDPkRb2NgClhWcNk3HiK5+UPvx7ctmNPBMKiLaw01WRl3KSJO+o64nmhUBrsxlZXBPSiLUxa4crKVQbjdxU8pfDpF8PZWnTmAW6J/XqFadF8sLn75SfD3dxNngGH9k3bfKRzoAyU1j4e/usnNwEt9juNl2UuRzz/6qPRveBwZmJVOkanZbF4Xtx4f/SvmJwzEEtemtOGYZF0vrU8vpd8XREI5aM6LZevWb9/Z7yvWa0ETtLtml+TTVDm+m+OTuf9zUCqXbBJ42k7vbBzZfQ67+t46S/hv/KHEBer4uC13wb7VmUkx3xN+Bf+FmsgMdF/t/lfgAEAeu5nx8gUITUAAAAASUVORK5CYII=
// @run-at document-start
// ==/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 = "Iridium for YouTube";
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 = {
"html/options.html": "\r\n\r\n
\r\n \r\n Iridium \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n\r\n\r\n\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n\r\n \r\n
\r\n
\r\n
General
\r\n
Various settings that control the extension and website
\r\n
\r\n
Extension settings
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Display extension button on YouTube
\r\n
Show an extension shortcut button on YouTube pages
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Sync extension settings
\r\n
Extension settings will be synced across browsers with the same user profile logged in
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Manage settings
\r\n
\r\n
\r\n
\r\n
Export settings into a file
\r\n
Extension settings are exported in a json file format
\r\n
\r\n
\r\n
\r\n
\r\n
Import settings from backup
\r\n
Restore settings from an existing backup json file format
\r\n
\r\n
\r\n
\r\n
\r\n
Reset settings
\r\n
Resetting the extension settings may help resolve certain issues, but remember to back up the current settings first
\r\n
\r\n
\r\n
\r\n
\r\n
Donate
\r\n
Contribute to help keep the extension alive
\r\n
\r\n
Donation options
\r\n
\r\n
\r\n
\r\n
\r\n
You may donate one time only or set up a monthly donation
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Join my patreon and follow up with less technical summarized updates
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Ad manager
\r\n
Manage how ads are displayed in video and other web pages
\r\n
\r\n
General
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Opt out of all ads
\r\n
No ads will be displayed on the website, but other settings may take precedence
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Home page
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Masthead ad
\r\n
Allow a large banner ad to show on the top of the page
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
In-feed ads
\r\n
Allow ads to show in the home page video list
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Search page
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
In-feed ads
\r\n
Allow ads to show in the search page results
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Video page
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Subscribed ads
\r\n
Only show ads on videos from subscribed channels
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
In-feed ads
\r\n
Allow ads to be displayed in the suggested videos sidebar
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Video ads
\r\n
Allow ads to be displayed in the video
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Tagged product ads
\r\n
Allow small banners of tagged affiliate products in the video
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Website
\r\n
Personalize the aspect and behavior of YouTube
\r\n
\r\n
Visual
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Show full titles in thumbnails
\r\n
Makes the titles in the thumbnails display in full
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Logo redirect
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Redirect to subscriptions page
\r\n
Clicking on the YouTube logo will redirect to the subscriptions page instead of the homepage
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Channel
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Autoplay channel trailer
\r\n
Channel trailers will play automatically. Does not override the browser settings
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Channel tab
\r\n
\r\n
\r\n
\r\n
\r\n \r\n Home \r\n Videos \r\n Shorts \r\n Live \r\n Podcasts \r\n Playlists \r\n Community \r\n Channels \r\n Store \r\n \r\n \r\n
Clicking on a channel link will automatically open the specified tab
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Shorts
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Home page
\r\n
Short videos will show on the home page
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Subscriptions
\r\n
Short videos will show in the subscriptions
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Video Page
\r\n
Short videos will show in video page sidebar
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Search results
\r\n
Short videos will show in search results
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Video page
\r\n
Customize the video page and player
\r\n
\r\n
Page
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Video focus
\r\n
Fade out the webpage while the video is playing
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Creator merch
\r\n
Allows the creator merch to be shown below the video details
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Linked video count
\r\n
Creates a link that shows the number of videos uploaded by the channel next to the subscriber count
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Ambient Mode
\r\n
Allows the lighting effect around the video player to be displayed if it is enabled in the player settings
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Playlist
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Reverse playlist control
\r\n
Show a button on playlists to reverse the playlist order
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Video quality
\r\n
\r\n
\r\n
\r\n
\r\n \r\n 4320p (8k) \r\n 2880p (5k) \r\n 2160p (4k) \r\n 1440p \r\n 1080p \r\n 720p \r\n 480p \r\n 360p \r\n 240p \r\n 144p \r\n Auto \r\n \r\n \r\n
Enforces the preferred or closest quality option
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Playback speed
\r\n
\r\n
\r\n
\r\n
\r\n \r\n 0.25 \r\n 0.5 \r\n 0.75 \r\n Normal \r\n 1.25 \r\n 1.5 \r\n 1.75 \r\n 2 \r\n Auto \r\n \r\n \r\n
Enforces the preferred or closest playback speed option
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Player
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Super theater
\r\n
Switching to theater mode will make the player cover the full browser while allowing the page to be scrolled
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Show super theater scrollbar
\r\n
Controls the vertical scrollbar visibility while in super theater mode
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Always visible
\r\n
Changes into an always visible mini player when less than half of the video is visible. The mini player can be moved using the right mouse button
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
HFR allowed
\r\n
Allows HFR (High Frame Rate) streams to be played. YouTube no longer provides normal framerate options above 720p when the respective HFR exists
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Autoplay
\r\n
Videos will play automatically. Does not override the browser settings
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Loudness normalization
\r\n
Enables YouTube's audio loudness normalization
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Info cards
\r\n
Allows info cards to show on the video
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Annotations
\r\n
Allow annotations to show on the video
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
End screen hidden
\r\n
Hide end screen when the cursor is over the video
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Volume
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Scroll volume
\r\n
Change video volume by scrolling on the video player, or by holding shift and scrolling anywhere in the page if the option below is enabled
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Shift + scroll volume
\r\n
Changing volume using the scroll wheel requires the shift key to be pressed, and can be done from anywhere in the page
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Volume step
\r\n
\r\n
\r\n
\r\n
\r\n \r\n 10 \r\n 9 \r\n 8 \r\n 7 \r\n 6 \r\n 5 \r\n 4 \r\n 3 \r\n 2 \r\n 1 \r\n \r\n \r\n
Number of volume steps each scroll will change at once
\r\n
\r\n
\r\n
\r\n
\r\n
Utils and shortcuts
\r\n
Show utils and shortcuts below the player. Shortcuts allow features to be controlled directly
\r\n
\r\n
Utils
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Video screenshot
\r\n
Allows a screenshot to be taken of a playing video at the current native resolution
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Video thumbnail
\r\n
Opens the highest quality thumbnail available
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Monetization info
\r\n
Shows if a video is monetized and how many ads are configured
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Shortcuts
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Autoplay shortcut
\r\n
Shortcut that allows autoplay to be controlled from the video page
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Video focus shortcut
\r\n
Shortcut that allows video focus to be controlled from the video page
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Blacklist
\r\n
Videos from blocked channels will no longer be displayed
\r\n
\r\n
Blacklist
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Blacklist enabled
\r\n
Videos from channels in the blacklist will not be displayed except in its own channel page
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
Blacklist button
\r\n
Displays a button in the three vertical dots context menu of video thumbnails to add that channel to the blacklist. This option is not available on vertical shorts thumbnails
\r\n
\r\n
\r\n
\r\n
\r\n
Blacklisted channels
\r\n
Remove blacklisted channels, add channels manually, import from an existing backup, or export the current list
\r\n
\r\n
\r\n \r\n
\r\n
\r\n
Add
\r\n
Import
\r\n
Export
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
Help
\r\n
Troubleshoot or report problems with the extension
\r\n
\r\n
\r\n
\r\n
\r\n
Report a problem
\r\n
If you found an issue or a bug with the extension please follow the instructions for reporting a bug below
\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(
"iridium-for-youtube",
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 = "Iridium for YouTube";
const debug = "[Iridium for YouTube]";
_log(debug + ' Executing background scripts...');
function executeBackgroundScripts(){
with(backgroundPolyfill){
// BG: js/setting-data.js
const SettingData = {
extensionButton: {
id: "extensionButton",
default: true,
},
syncSettings: {
id: "syncSettings",
default: false,
},
fullTitles: {
id: "fullTitles",
default: true,
},
theme: {
id: "theme",
default: "deviceTheme",
},
logoSubscriptions: {
id: "logoSubscriptions",
default: false,
},
autoplayChannelTrailer: {
id: "autoplayChannelTrailer",
default: false,
},
channelTab: {
id: "channelTab",
default: "featured",
},
homeShorts: {
id: "homeShorts",
default: true,
},
subscriptionsShorts: {
id: "subscriptionsShorts",
default: true,
},
videoPageShorts: {
id: "videoPageShorts",
default: true,
},
searchShorts: {
id: "searchShorts",
default: true,
},
adOptOutAll: {
id: "adOptOutAll",
default: false,
},
adSubscribed: {
id: "adSubscribed",
default: false,
},
adVideoFeed: {
id: "adVideoFeed",
default: false,
},
adInVideo: {
id: "adInVideo",
default: false,
},
adTaggedProducts: {
id: "adTaggedProducts",
default: false,
},
adMasthead: {
id: "adMasthead",
default: false,
},
adHomeFeed: {
id: "adHomeFeed",
default: false,
},
adSearchFeed: {
id: "adSearchFeed",
default: false,
},
videoFocus: {
id: "videoFocus",
default: true,
},
creatorMerch: {
id: "creatorMerch",
default: true,
},
videoCount: {
id: "videoCount",
default: true,
},
ambientMode: {
id: "ambientMode",
default: true,
},
reversePlaylist: {
id: "reversePlaylist",
default: true,
},
reversePlaylistToggled: {
id: "reversePlaylistToggled",
default: false,
},
superTheater: {
id: "superTheater",
default: true,
},
superTheaterScrollbar: {
id: "superTheaterScrollbar",
default: true,
},
defaultQuality: {
id: "defaultQuality",
default: "auto",
},
defaultSpeed: {
id: "defaultSpeed",
default: "-1",
},
alwaysVisible: {
id: "alwaysVisible",
default: true,
},
alwaysVisiblePosition: {
id: "alwaysVisiblePosition",
default: {},
},
hfrAllowed: {
id: "hfrAllowed",
default: true,
},
autoplay: {
id: "autoplay",
default: false,
},
loudness: {
id: "loudness",
default: false,
},
scrollVolume: {
id: "scrollVolume",
default: true,
},
scrollVolumeShift: {
id: "scrollVolumeShift",
default: true,
},
scrollVolumeStep: {
id: "scrollVolumeStep",
default: 5,
},
infoCards: {
id: "infoCards",
default: false,
},
annotations: {
id: "annotations",
default: true,
},
endScreen: {
id: "endScreen",
default: false,
},
autoplayShortcut: {
id: "autoplayShortcut",
default: true,
},
videoFocusToggle: {
id: "videoFocusToggle",
default: true,
},
videoScreenshot: {
id: "videoScreenshot",
default: true,
},
videoThumbnail: {
id: "videoThumbnail",
default: true,
},
monetizationInfo: {
id: "monetizationInfo",
default: true,
},
blacklistEnabled: {
id: "blacklistEnabled",
default: true,
},
blacklistButton: {
id: "blacklistButton",
default: true,
},
blacklist: {
id: "blacklist",
default: {},
},
};
const getDefaultSettings = () => Object.keys(SettingData).reduce((previousValue, currentValue) => ({
...previousValue,
[currentValue]: SettingData[currentValue].default
}), {});
// BG: js/background.js
globalThis.browser ??= chrome;
const openOptions = () => {
browser.runtime.openOptionsPage()?.then?.();
}
const Api = {
debounce: false,
onMessageListener: (data) => {
if (!data.type || !data.payload || Api.debounce) {
return;
}
// prevents the options page from opening in duplicate
Api.debounce = true;
setTimeout(() => Api.debounce = false, 1000);
if (data.payload === SettingData.extensionButton.id) {
openOptions();
}
}
}
const onConnect = (port) => {
port.onMessage.addListener(Api.onMessageListener);
}
browser.runtime.onConnect.addListener(onConnect);
// firefox mv2
browser.browserAction.onClicked.addListener(openOptions);
}
}
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":2,"name":"Iridium for YouTube","version":"2.1.2","description":"Take control of YouTube and boost your user experience with Iridium","permissions":["storage","*://www.youtube.com/*"],"optional_permissions":[],"content_scripts":[{"matches":["*://www.youtube.com/*"],"css":["css/content-script.css"],"js":["js/setting-data.js","js/background-inject.js","js/content-script.js"],"run_at":"document_start"}],"options_ui":{"page":"html/options.html","open_in_tab":true},"browser_action":{"default_icon":{"16":"icons/16.png","32":"icons/32.png","48":"icons/48.png","64":"icons/64.png","128":"icons/128.png"}},"page_action":{},"action":{},"icons":{"16":"icons/16.png","32":"icons/32.png","48":"icons/48.png","64":"icons/64.png","128":"icons/128.png"},"web_accessible_resources":[],"background":{"scripts":["js/setting-data.js","js/background.js"],"persistent":false},"_id":"iridium-for-youtube"};
const CONTENT_SCRIPT_CONFIGS_FOR_MATCHING = [
{
"matches": [
"*://www.youtube.com/*"
]
}
];
const OPTIONS_PAGE_PATH = "html/options.html";
const POPUP_PAGE_PATH = null;
const EXTENSION_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABSBJREFUeNrUWktsG0UYnsnaUXjGVZS0gEQj0UO5IKuq1F4A3yIuODcOSDQ5IK41NyQk8AGJW+wrQmoi7pDcKJd1b6lASrigSCDFEq/KLY1bKSU0sYfvX8+uZ8czs7bjV1b6NLNee/f/Zr7/MbNmzHIIJnxgDciws3g0OQhwIUDgAP2VM0fgaVr4gDhORdjBee7MEDh8VviAIDx5poV/ZwJ8ByxOip3cdqE+K3w0wYhzIb8s2sBRBErPH/L6RBKozQsfhloJSFSJyOxjvj5xBP58GQRoBuzGq6jgu8VzB7wyMQSqF1sS4l0QmGpG/XUiMvcPr46dwK+XeidALVDHTcvz9/nnYyXwy+txJ04ioJEgVHFeWLjPN8dC4Oc3DARaKZpNUT80WPYNBJjXDK5ViMj5Gt8dKYEfr8ajkColG4EA9FlDI9KQ/tEiMtCwO2W7cJyO4+m00levpbTvaucn8rzhsZXmFNu/d17cHMkM3Hmr7cQmR1Ylo0tHHflQSjQrXiP6rIqZWl2onT7splwz4EpiMGIXbQbtookAGYoRD/pNMpy3zgm4tgj4tQVRgfxW5x/0H3ZPQ+AR2i0iAOQlmWi0Q+OJiOCR4UE/QoPlPMb2H8yJEn5fnHvYu3907QMOVIEytH4bOFJ94ETr29BIsZsgtP/wXO9lu5VA6LSq8yZgWxLZVo2PGZtWoH7uBW2m6bFbILHz6MXuy3anhFxZ2JK8jiCV22jvYkTzJC+haJ98oRnKh7WlxFi7BZcs7uk/fkFsQo6F5w7d/jFQAorj1tFu4Pwy2iW0mZj2HQSU0LiMey9jHVLE/Uoz/5n9oysn7jBeT14SquPKUd9Dfw/G5YBrwIzNaMOiPHzeZ3jejeO0KKaPO8v2/ggkxH5DxKkA28ASkNUJxEZfGJMThepbJ564gfsWOWvnj4ES6DBeap9anB8BW8Bd3HaJjIplVGGdARU5PCcnhKCZICLVgRBQY39gvOK0nq59zu7hthtoLwdEOMvoRKxVr5QunrFC/iGYKKdcYVQtI2IFnK59aqcU7RsMl4Nq1r1S7UawDZzX6tOzeopCLgIO7ZsizQWThFQyLHnhtA57EJ0SJNQtgSTjpeEUgcjwbEdFaZNNM248KtoKGT79pA8njkKnFkI9k/bbjhuCMus1ItGL3httAnU8i1Z3/YfRmOFtZ4qM17XPWsmKMnEeZ52OytyaD/VOI94QrPTq76dMZKbkpRqvyYcyb14ScCarSPPNDgKbeGbh0l6/pcR03JlMBAzGk87fBq5bV1DJWzO7ZHj2p+4WO/Ywmk5OXqrx6F+Xxs9YM6xb73VaE+Tu8NLAFjQ2AuFSURq+COSpYGNagWbMsGa9l2BJcemH3hc0PRHQirZZtO9RxdlNgWaRC4XF1Xe/HdKS0kZAzkLWkayS9E6bXqsffDPkRb2NgClhWcNk3HiK5+UPvx7ctmNPBMKiLaw01WRl3KSJO+o64nmhUBrsxlZXBPSiLUxa4crKVQbjdxU8pfDpF8PZWnTmAW6J/XqFadF8sLn75SfD3dxNngGH9k3bfKRzoAyU1j4e/usnNwEt9juNl2UuRzz/6qPRveBwZmJVOkanZbF4Xtx4f/SvmJwzEEtemtOGYZF0vrU8vpd8XREI5aM6LZevWb9/Z7yvWa0ETtLtml+TTVDm+m+OTuf9zUCqXbBJ42k7vbBzZfQ67+t46S/hv/KHEBer4uC13wb7VmUkx3xN+Bf+FmsgMdF/t/lfgAEAeu5nx8gUITUAAAAASUVORK5CYII=";
const extensionCssData = { "css/content-script.css": "/* ini | autoplay */\r\n#movie_player.unstarted-mode:is(.playing-mode, .paused-mode):not(.buffering-mode, .ytp-player-minimized) > *:not(.ytp-cued-thumbnail-overlay) {\r\n display: none;\r\n}\r\n/* end | autoplay */\r\n\r\n/* ini | autoplay channel trailer */\r\n#c4-player.unstarted-mode:is(.playing-mode, .paused-mode):not(.buffering-mode) > *:not(.ytp-cued-thumbnail-overlay) {\r\n display: none;\r\n}\r\n/* end | autoplay channel trailer */\r\n\r\n/* ini | video focus */\r\nhtml #masthead-container,\r\nhtml #secondary,\r\nhtml #below {\r\n transition-property: opacity !important;\r\n transition-duration: 0.8s !important;;\r\n transition-timing-function: ease !important;;\r\n transition-delay: 0.25s !important;;\r\n}\r\nhtml[dim] #masthead-container:not(:hover),\r\nhtml[dim] #secondary:not(:hover),\r\nhtml[dim] #below:not(:hover) {\r\n opacity: 0.1;\r\n}\r\n/* end | video focus */\r\n\r\n/* ini | iridium icon */\r\nhtml[dark] .iridium-options yt-icon {\r\n background: #fff;\r\n}\r\nhtml .iridium-options:hover yt-icon {\r\n background: radial-gradient(circle at 0% 150%, #0ff 35%, #f0f 75%);\r\n}\r\n.iridium-options yt-icon {\r\n mask-image: url('data:image/svg+xml, ');\r\n background: #030303;\r\n}\r\n/* end | iridium icon */\r\n\r\n/* ini | reverse playlist */\r\nytd-toggle-button-renderer:has([aria-label=\"reversePlaylistButton\"]) {\r\n margin-left: 0 !important;\r\n}\r\nhtml[dark] [aria-label=\"reversePlaylistButton\"] yt-icon {\r\n background: #f1f1f1;\r\n}\r\n[aria-label=\"reversePlaylistButton\"] yt-icon {\r\n background: #0f0f0f;\r\n}\r\n[aria-label=\"reversePlaylistButton\"] yt-icon {\r\n mask-image: url('data:image/svg+xml, ');\r\n}\r\n[aria-label=\"reversePlaylistButton\"][aria-pressed=\"true\"] yt-icon {\r\n mask-image: url('data:image/svg+xml, ');\r\n}\r\n/* end | reverse playlist */\r\n\r\n/* ini | full titles */\r\nhtml.iridium-full-titles #video-title.ytd-video-renderer,\r\nhtml.iridium-full-titles #video-title.ytd-compact-video-renderer,\r\nhtml.iridium-full-titles #video-title.ytd-reel-item-renderer,\r\nhtml.iridium-full-titles #video-title.ytd-rich-grid-media {\r\n max-height: unset;\r\n -webkit-line-clamp: unset;\r\n}\r\n/* end | full titles */\r\n\r\n/* ini | super theater */\r\nhtml[super-theater] #masthead-container.ytd-app:has( > [theater][is-watch-page]) #masthead {\r\n transform: translateY(-100%);\r\n transition: transform .5s ease-out !important;\r\n}\r\nhtml[super-theater] #masthead-container.ytd-app:has( > [theater][is-watch-page]):hover #masthead {\r\n transform: translateY(0);\r\n}\r\nhtml[super-theater] ytd-watch-flexy[full-bleed-player]:not([hidden]) #full-bleed-container.ytd-watch-flexy,\r\nhtml[super-theater] ytd-watch-grid[full-bleed-player]:not([hidden]) #player-full-bleed-container.ytd-watch-grid {\r\n max-height: calc(100vh);\r\n height: calc(100vh);\r\n min-height: unset;\r\n}\r\nhtml[super-theater] #page-manager.ytd-app:has( > [theater]:not([hidden])) {\r\n margin-top: 0;\r\n}\r\nhtml[super-theater] #page-manager.ytd-app:has( > [theater]:not([hidden])) ytd-live-chat-frame {\r\n top: 0 !important;\r\n}\r\nhtml[super-theater] #player-full-bleed-container {\r\n display: flex;\r\n flex-direction: row;\r\n}\r\nhtml[super-theater] #player-full-bleed-container #player-container {\r\n position: relative;\r\n flex: 1;\r\n}\r\nhtml[super-theater]:not([super-theater]) #masthead-container.ytd-app,\r\nhtml[super-theater][super-theater] #masthead-container.ytd-app:not(:has( > [is-watch-page])) {\r\n width: 100% !important;\r\n}\r\nhtml[super-theater] #page-manager.ytd-app:has( > [theater]:not([hidden])) #chat-container {\r\n position: relative;\r\n}\r\nhtml[super-theater] #page-manager.ytd-app:has( > [theater]:not([hidden])) ytd-live-chat-frame {\r\n height: 100% !important;\r\n min-height: unset !important;\r\n top: unset !important;\r\n}\r\n/* end | super theater */\r\n\r\n/* ini | super theater scrollbar */\r\nhtml[super-theater][super-theater-scrollbar]:has( ytd-watch-flexy[theater]:not([hidden])) {\r\n scrollbar-width: none;\r\n}\r\n/* end | super theater scrollbar */\r\n\r\n/* ini | hide end screen cards */\r\n.iridium-hide-end-screen-cards #movie_player:hover .ytp-ce-element:not(:hover) {\r\n opacity: 0;\r\n}\r\n/* end | hide end screen cards */\r\n\r\n/* ini | scroll volume */\r\n#iridium-scroll-volume-level-container {\r\n text-align: center;\r\n position: absolute;\r\n left: 0;\r\n right: 0;\r\n top: 10%;\r\n z-index: 19;\r\n}\r\n#iridium-scroll-volume-level {\r\n display: inline-block;\r\n padding: 10px 20px;\r\n font-size: 175%;\r\n background: rgba(0, 0, 0, .5);\r\n pointer-events: none;\r\n border-radius: 3px;\r\n}\r\n/* end | scroll volume */\r\n\r\n/* ini | player tools */\r\n#iridium-player-tools {\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 0;\r\n position: absolute;\r\n top: 0;\r\n right: 0;\r\n bottom: 0;\r\n}\r\n#bottom-grid #iridium-player-tools {\r\n position: unset;\r\n margin-left: auto;\r\n}\r\n#iridium-player-tools > div {\r\n cursor: pointer;\r\n width: 36px;\r\n height: 36px;\r\n justify-content: center;\r\n display: flex;\r\n align-items: center;\r\n fill: var(--yt-spec-text-primary);\r\n position: relative;\r\n}\r\n#iridium-player-tools > div:hover {\r\n background-color: var(--yt-spec-10-percent-layer);\r\n border-radius: 18px;\r\n}\r\n#iridium-player-tools > div > svg {\r\n pointer-events: none;\r\n user-select: none;\r\n}\r\n#iridium-autoplay-shortcut:not([iridium-enabled]) {\r\n opacity: 0.5;\r\n}\r\n#iridium-video-focus:not([iridium-enabled]) #iridium-on,\r\n#iridium-video-focus[iridium-enabled] #iridium-off {\r\n display: none;\r\n}\r\n#iridium-monetization:not(.monetized) .iridium-on,\r\n#iridium-monetization.monetized .iridium-off {\r\n display: none;\r\n}\r\nhtml[dark] #iridium-player-tools .monetized {\r\n fill: #3fd20a;\r\n}\r\n#iridium-player-tools .monetized {\r\n fill: #3ad406;\r\n}\r\nhtml[dark] #iridium-player-tools .sponsored {\r\n fill: #3e8bff;\r\n}\r\n#iridium-player-tools .sponsored {\r\n fill: #0677d4;\r\n}\r\n#iridium-monetization-count {\r\n font-size: 10px;\r\n position: absolute;\r\n color: var(--yt-spec-text-primary);\r\n bottom: 0;\r\n right: 0;\r\n background: var(--yt-spec-base-background);\r\n border-radius: 12px;\r\n padding: 0 3px;\r\n}\r\n#iridium-monetization-count {\r\n font-size: 10px;\r\n position: absolute;\r\n color: var(--yt-spec-text-primary);\r\n bottom: 0;\r\n right: 0;\r\n background: var(--yt-spec-base-background);\r\n border-radius: 12px;\r\n padding: 0 3px;\r\n}\r\n#iridium-player-tools > div:hover #iridium-monetization-count {\r\n display: none;\r\n}\r\n/* end | player tools */\r\n\r\n/* ini | always visible */\r\nhtml[always-visible-player] .ytp-miniplayer-button,\r\nhtml[always-visible-player] .ytp-size-button {\r\n display: none !important;\r\n}\r\nhtml[always-visible-player]:not(:has(ytd-watch-flexy[fullscreen])) #movie_player {\r\n position: fixed;\r\n z-index: 9999;\r\n height: 225px;\r\n width: 400px;\r\n bottom: 10px;\r\n right: 10px;\r\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, .14), 0 1px 10px 0 rgba(0, 0, 0, .12), 0 2px 4px -1px rgba(0, 0, 0, .4);\r\n}\r\nhtml[always-visible-player] #movie_player::before {\r\n background-color: #000;\r\n bottom: 0;\r\n content: \"\";\r\n display: block;\r\n left: 0;\r\n position: absolute;\r\n right: 0;\r\n top: 0;\r\n}\r\nhtml[always-visible-player][moving] #movie_player {\r\n opacity: 0.9;\r\n}\r\nhtml[always-visible-player][moving] #movie_player::after {\r\n background-color: #fff;\r\n bottom: 0;\r\n content: \"\";\r\n display: block;\r\n left: 0;\r\n opacity: .4;\r\n position: absolute;\r\n right: 0;\r\n top: 0;\r\n z-index: 99;\r\n}\r\nhtml[dark][always-visible-player][moving] #movie_player::after {\r\n background-color: #000;\r\n}\r\n/* end | always visible */\r\n\r\n/* ini | ambient mode disabled */\r\nhtml[ambient-mode-disabled] #cinematics-container {\r\n display: none;\r\n}\r\n/* end | ambient mode disabled */"};
const LOCALE_KEYS = {"locale_id":{"message":"English (US)"},"page_title":{"message":"Iridium"}};
const USED_LOCALE = "en_US";
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 = "Iridium for YouTube";
_log(`Starting execution phases...`);
// #region Document Start
if (typeof document !== 'undefined') {
_log(`Executing document-start phase...`);
const cssKey_0 = "css/content-script.css";
try {
if (extensionCssData[cssKey_0]) {
_log(` Injecting CSS (start): ${cssKey_0}`);
const style = document.createElement('style');
style.textContent = extensionCssData[cssKey_0];
(document.head || document.documentElement).appendChild(style);
} else { console.warn(` CSS not found (start): ${cssKey_0}`); }
} catch(e) { _error(` Failed injecting CSS (${cssKey_0}) in phase start`, e, extensionCssData); }
const scriptPaths = ["js/setting-data.js","js/background-inject.js","js/content-script.js"];
_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){;
// START: js/setting-data.js
const SettingData = {
extensionButton: {
id: "extensionButton",
default: true,
},
syncSettings: {
id: "syncSettings",
default: false,
},
fullTitles: {
id: "fullTitles",
default: true,
},
theme: {
id: "theme",
default: "deviceTheme",
},
logoSubscriptions: {
id: "logoSubscriptions",
default: false,
},
autoplayChannelTrailer: {
id: "autoplayChannelTrailer",
default: false,
},
channelTab: {
id: "channelTab",
default: "featured",
},
homeShorts: {
id: "homeShorts",
default: true,
},
subscriptionsShorts: {
id: "subscriptionsShorts",
default: true,
},
videoPageShorts: {
id: "videoPageShorts",
default: true,
},
searchShorts: {
id: "searchShorts",
default: true,
},
adOptOutAll: {
id: "adOptOutAll",
default: false,
},
adSubscribed: {
id: "adSubscribed",
default: false,
},
adVideoFeed: {
id: "adVideoFeed",
default: false,
},
adInVideo: {
id: "adInVideo",
default: false,
},
adTaggedProducts: {
id: "adTaggedProducts",
default: false,
},
adMasthead: {
id: "adMasthead",
default: false,
},
adHomeFeed: {
id: "adHomeFeed",
default: false,
},
adSearchFeed: {
id: "adSearchFeed",
default: false,
},
videoFocus: {
id: "videoFocus",
default: true,
},
creatorMerch: {
id: "creatorMerch",
default: true,
},
videoCount: {
id: "videoCount",
default: true,
},
ambientMode: {
id: "ambientMode",
default: true,
},
reversePlaylist: {
id: "reversePlaylist",
default: true,
},
reversePlaylistToggled: {
id: "reversePlaylistToggled",
default: false,
},
superTheater: {
id: "superTheater",
default: true,
},
superTheaterScrollbar: {
id: "superTheaterScrollbar",
default: true,
},
defaultQuality: {
id: "defaultQuality",
default: "auto",
},
defaultSpeed: {
id: "defaultSpeed",
default: "-1",
},
alwaysVisible: {
id: "alwaysVisible",
default: true,
},
alwaysVisiblePosition: {
id: "alwaysVisiblePosition",
default: {},
},
hfrAllowed: {
id: "hfrAllowed",
default: true,
},
autoplay: {
id: "autoplay",
default: false,
},
loudness: {
id: "loudness",
default: false,
},
scrollVolume: {
id: "scrollVolume",
default: true,
},
scrollVolumeShift: {
id: "scrollVolumeShift",
default: true,
},
scrollVolumeStep: {
id: "scrollVolumeStep",
default: 5,
},
infoCards: {
id: "infoCards",
default: false,
},
annotations: {
id: "annotations",
default: true,
},
endScreen: {
id: "endScreen",
default: false,
},
autoplayShortcut: {
id: "autoplayShortcut",
default: true,
},
videoFocusToggle: {
id: "videoFocusToggle",
default: true,
},
videoScreenshot: {
id: "videoScreenshot",
default: true,
},
videoThumbnail: {
id: "videoThumbnail",
default: true,
},
monetizationInfo: {
id: "monetizationInfo",
default: true,
},
blacklistEnabled: {
id: "blacklistEnabled",
default: true,
},
blacklistButton: {
id: "blacklistButton",
default: true,
},
blacklist: {
id: "blacklist",
default: {},
},
};
const getDefaultSettings = () => Object.keys(SettingData).reduce((previousValue, currentValue) => ({
...previousValue,
[currentValue]: SettingData[currentValue].default
}), {});
// END: js/setting-data.js
// START: js/background-inject.js
function mainScript(extensionId, SettingData, defaultSettings) {
const iridiumSettings = defaultSettings;
// ini listeners
const OnPageDataChanged = (() => {
const listeners = [];
const onEvent = (event) =>
listeners.forEach((listener) => listener?.(event));
window.addEventListener("yt-page-data-updated", onEvent, true);
window.addEventListener("yt-navigate-start", onEvent, false);
window.addEventListener("yt-navigate-finish", onEvent, false);
window.addEventListener("yt-next-continuation-data-updated", onEvent, true);
window.addEventListener("popstate", onEvent, true);
return {
addListener: (listener) => listeners.push(listener),
};
})();
const OnYtPageDataFetched = (() => {
const listeners = [];
const onEvent = (data) => {
const response = data?.["detail"]?.["pageData"]?.["response"];
if (response) {
listeners.forEach((listener) => listener?.(response));
}
};
window.addEventListener("yt-page-data-fetched", onEvent, true);
return {
addListener: (listener) => listeners.push(listener),
};
})();
// end listeners
// ini utils
const j2d = (() => {
const processNode = (element, attributes, children) => {
if (
Array.isArray(attributes) ||
typeof attributes === "string" ||
attributes instanceof String
) {
children = attributes;
attributes = {};
}
for (let name in attributes) {
element.setAttribute(name, attributes[name]);
}
if (children) {
if (Array.isArray(children)) {
for (let child of children) {
if (child.constructor === String) {
const textNode = document.createTextNode(child);
element.appendChild(textNode);
} else {
element.appendChild(child);
}
}
} else if (children.constructor === String) {
element.textContent = children;
}
}
return element;
};
return {
make: (node, attributes, children) =>
processNode(document.createElement(node), attributes, children),
makeSVG: (node, attributes, children) =>
processNode(
document.createElementNS("http://www.w3.org/2000/svg", node),
attributes,
children,
),
};
})();
const Util = {
getSingleObjectByKey: (obj, keys, match) => {
if (!obj) {
return null;
}
for (let property in obj) {
if (
!obj.hasOwnProperty(property) ||
obj[property] === null ||
obj[property] === undefined
) {
continue;
}
if (
(keys.constructor.name === "String"
? keys === property
: keys.indexOf(property) > -1) &&
(!match ||
(obj[property].constructor.name !== "Object" &&
match(obj[property], obj)))
) {
return obj[property];
}
if (obj[property].constructor.name === "Object") {
let result = Util.getSingleObjectByKey(obj[property], keys, match);
if (result) {
return result;
}
} else if (obj[property].constructor.name === "Array") {
for (let i = 0; i < obj[property].length; i++) {
let result = Util.getSingleObjectByKey(
obj[property][i],
keys,
match,
);
if (result) {
return result;
}
}
}
}
},
getSingleObjectAndParentByKey: (obj, keys, match) => {
for (let property in obj) {
if (
!obj.hasOwnProperty(property) ||
obj[property] === null ||
obj[property] === undefined
) {
continue;
}
if (
(keys.constructor.name === "String"
? keys === property
: keys.indexOf(property) > -1) &&
(!match || match(obj[property], obj))
) {
return {
parent: obj,
object: obj[property],
};
}
if (obj[property].constructor.name === "Object") {
let result = Util.getSingleObjectAndParentByKey(
obj[property],
keys,
match,
);
if (result) {
return result;
}
} else if (obj[property].constructor.name === "Array") {
for (let i = 0; i < obj[property].length; i++) {
let result = Util.getSingleObjectAndParentByKey(
obj[property][i],
keys,
match,
);
if (result) {
return result;
}
}
}
}
},
isWatchPage: () =>
window.location.pathname === "/watch" ||
window.location.pathname.startsWith("/clip"),
};
const FeatureUpdater = (() => {
const features = {};
const register = (id, feature) => (features[id] ??= []).push(feature);
const updateAll = () =>
Object.keys(features).forEach((id) =>
features[id]?.forEach((feature) => feature?.()),
);
const update = (id) => features[id]?.forEach((feature) => feature?.());
OnPageDataChanged.addListener(updateAll);
return {
update: update,
register: register,
};
})();
const Broadcaster = (() => {
const channel = new BroadcastChannel(extensionId);
const onMessageListener = (event) => {
if (event?.data) {
// first update setting(s)
for (let key in event.data) {
iridiumSettings[key] = event.data?.[key];
}
// then update feature(s)
for (let key in event.data) {
FeatureUpdater.update(key);
}
}
};
channel.addEventListener("message", onMessageListener);
return {
postMessage: (message) => channel.postMessage(message),
doAction: (action) =>
channel.postMessage({
type: "action",
payload: action,
}),
saveSetting: (settingId) =>
channel.postMessage({
type: "setting",
payload: { [settingId]: iridiumSettings[settingId] },
}),
};
})();
// end utils
// ini overrides
const OverrideHandleResponse = (() => {
const listeners = [];
const handleResponseKey = crypto.randomUUID();
Object.defineProperty(Object.prototype, "handleResponse", {
set(data) {
this[handleResponseKey] = data;
},
get() {
const original = this[handleResponseKey];
return function (url, code, response, callback) {
if (
response?.constructor === String &&
original?.toString()?.indexOf('")]}\'"') !== -1
) {
try {
const parsed = JSON.parse(response);
listeners?.forEach((listener) => listener?.(parsed));
arguments[2] = JSON.stringify(parsed);
} catch (e) {}
}
return original?.apply(this, arguments);
};
},
});
return {
onHandleResponseListener: (listener) => listeners.push(listener),
};
})();
const OverrideFetch = (() => {
const listeners = [];
const canProceed = (data) => {
const endpoints = data?.["onResponseReceivedEndpoints"];
if (
endpoints != null &&
endpoints?.constructor === Array &&
endpoints.length > 0
) {
for (let i = endpoints.length - 1; i >= 0; i--) {
if (
endpoints[i]?.["reloadContinuationItemsCommand"]?.["targetId"] ===
"comments-section"
) {
return false;
}
}
}
return (
data?.["contents"] ||
data?.["videoDetails"] ||
data?.["items"] ||
data?.["onResponseReceivedActions"] ||
data?.["onResponseReceivedEndpoints"] ||
data?.["onResponseReceivedCommands"]
);
};
const override = function (target, thisArg, argArray) {
if (
!argArray?.[0]?.url ||
Object.getOwnPropertyDescriptor(argArray[0], "url") !== undefined
) {
return target.apply(thisArg, argArray);
} else {
return target.apply(thisArg, argArray).then((response) => {
return response
.clone()
.text()
.then((text) => {
const data = JSON.parse(text.replace(")]}'\n", ""));
if (canProceed(data)) {
listeners?.forEach((listener) => listener?.(data));
return new Response(JSON.stringify(data));
} else {
return response;
}
})
.catch(() => response);
});
}
};
const original = window.fetch?.["original"] || window.fetch;
window.fetch = new Proxy(window.fetch, { apply: override });
window.fetch.original = original;
let ytInitialData = undefined;
Object.defineProperty(window, "ytInitialData", {
set(data) {
ytInitialData = data;
listeners?.forEach((listener) => listener?.(ytInitialData));
},
get() {
return ytInitialData;
},
});
let ytInitialPlayerResponse = undefined;
Object.defineProperty(window, "ytInitialPlayerResponse", {
set(data) {
ytInitialPlayerResponse = data;
listeners?.forEach((listener) => listener?.(ytInitialPlayerResponse));
},
get() {
return ytInitialPlayerResponse;
},
});
return {
onFetchListener: (listener) => listeners.push(listener),
};
})();
const OverrideVideoPlay = (() => {
let canPlay = false;
let timer = null;
let previousVideoId = null;
let lastKey = -1;
const spacebar = 32;
const keyK = 75;
const moviePlayerContainers = [
"#player:has(#movie_player)",
"#player-container:has(#movie_player)",
"#ytd-player:has(#movie_player)",
"#movie_player",
].join(",");
const c4PlayerContainers = [
"#player:has(#c4-player)",
"#player-container:has(#c4-player)",
"#ytd-player:has(#c4-player)",
"#c4-player",
].join(",");
const onClick = (event) => {
lastKey = -1;
clearTimeout(timer);
canPlay =
event.target.id !== "player-wrap" &&
(document
.querySelector(moviePlayerContainers)
?.contains(event.target) === true ||
document.querySelector(c4PlayerContainers)?.contains(event.target) ===
true);
if (canPlay) {
previousVideoId =
document.getElementById("movie_player")?.["getVideoData"]()?.[
"video_id"
] || null;
timer = setTimeout(() => (canPlay = false), 300);
}
};
const isAllowed = (target) => {
const currentKey = lastKey;
lastKey = -1;
const isMoviePlayer =
document.getElementById("movie_player")?.contains(target) === true;
if (isMoviePlayer) {
return (
iridiumSettings.autoplay ||
canPlay ||
previousVideoId ===
(document.getElementById("movie_player")?.["getVideoData"]()?.[
"video_id"
] || "") ||
lastKey === keyK ||
lastKey === spacebar
);
}
const isC4Player =
document.getElementById("c4-player")?.contains(target) === true;
if (isC4Player) {
return (
iridiumSettings.autoplayChannelTrailer ||
canPlay ||
lastKey === keyK ||
lastKey === spacebar
);
}
return true;
};
const override = () => {
const original =
HTMLVideoElement.prototype.play?.["original"] ||
HTMLVideoElement.prototype.play;
HTMLVideoElement.prototype.play = function () {
const moviePlayer = document.getElementById("movie_player");
const isMoviePlayer = moviePlayer?.contains(this) === true;
const currentVideoId =
moviePlayer?.["getVideoData"]()?.["video_id"] || "";
if (isAllowed(this)) {
if (isMoviePlayer) {
previousVideoId = currentVideoId;
} else {
previousVideoId = null;
}
return original.apply(this, arguments);
} else {
previousVideoId = null;
return new Promise((resolve, reject) =>
reject(new DOMException("rejected")),
);
}
};
HTMLVideoElement.prototype.play.original = original;
};
const reset = () => {
HTMLVideoElement.prototype.play =
HTMLVideoElement.prototype.play?.["original"] ||
HTMLVideoElement.prototype.play;
delete HTMLVideoElement.prototype.play?.["original"];
canPlay = false;
timer = null;
previousVideoId = null;
};
const onNavigate = () => {
clearTimeout(timer);
canPlay = false;
previousVideoId = null;
};
const onKeyEvent = (event) => {
lastKey = event.keyCode;
};
let listenersActive = false;
const update = () => {
if (iridiumSettings.autoplay && iridiumSettings.autoplayChannelTrailer) {
listenersActive = false;
window.removeEventListener("yt-navigate-start", onNavigate, false);
window.removeEventListener("popstate", onNavigate, true);
document.removeEventListener("keydown", onKeyEvent, true);
document.removeEventListener("keyup", onKeyEvent, true);
document.removeEventListener("click", onClick, true);
reset();
} else if (!listenersActive) {
listenersActive = true;
window.addEventListener("yt-navigate-start", onNavigate, false);
window.addEventListener("popstate", onNavigate, true);
document.addEventListener("keydown", onKeyEvent, true);
document.addEventListener("keyup", onKeyEvent, true);
document.addEventListener("click", onClick, true);
override();
}
};
update();
FeatureUpdater.register(SettingData.autoplay.id, update);
FeatureUpdater.register(SettingData.autoplayChannelTrailer.id, update);
return {};
})();
const OverrideOnPlayerReady = (() => {
const listeners = [];
const onPlayerReadyEventKey = crypto.randomUUID();
let recentApi = null;
Object.defineProperty(Object.prototype, "onPlayerReadyEvent_", {
set(data) {
this[onPlayerReadyEventKey] = data;
},
get() {
const original = this[onPlayerReadyEventKey];
if (original.constructor === Boolean) {
return original;
}
return function (api) {
recentApi = api;
listeners?.forEach((listener) => listener?.(api));
return original?.apply(this, arguments);
};
},
});
function addListener(listener) {
if (recentApi) {
listener(recentApi);
}
listeners.push(listener);
}
return {
onReadyListener: (listener) => addListener(listener),
};
})();
const OverrideSetInternalSize = (() => {
const setInternalSizeKey = crypto.randomUUID();
Object.defineProperty(Object.prototype, "setInternalSize", {
set(data) {
this[setInternalSizeKey] = data;
},
get() {
const original = this[setInternalSizeKey];
if (original?.toString()?.startsWith("function(a)")) {
return function (size) {
if (
iridiumSettings.alwaysVisible &&
Object.hasOwn(size, "width") &&
Object.hasOwn(size, "height")
) {
arguments[0].width = NaN;
arguments[0].height = NaN;
}
return original?.apply(this, arguments);
};
} else {
return original;
}
},
});
return {};
})();
// end overrides
// ini features
const FeatureFullTitles = (() => {
const update = () => {
if (iridiumSettings.fullTitles) {
document.documentElement.classList.add("iridium-full-titles");
} else {
document.documentElement.classList.remove("iridium-full-titles");
}
};
FeatureUpdater.register(SettingData.fullTitles.id, update);
return {
update: update,
};
})();
const FeatureTheme = (() => {
const update = () => {
switch (iridiumSettings.theme) {
case "deviceTheme":
document
.querySelector("ytd-app")
?.["handleSignalActionToggleDarkThemeDevice"]?.();
break;
case "darkTheme":
document
.querySelector("ytd-app")
?.["handleSignalActionToggleDarkThemeOn"]?.();
break;
case "lightTheme":
document
.querySelector("ytd-app")
?.["handleSignalActionToggleDarkThemeOff"]?.();
break;
}
};
FeatureUpdater.register(SettingData.theme.id, update);
return {
update: update,
};
})();
const FeatureSettingsButton = (() => {
const listener = (data) => {
if (!iridiumSettings.extensionButton) {
return;
}
const topBarButtons =
data?.["topbar"]?.["desktopTopbarRenderer"]?.["topbarButtons"];
if (!topBarButtons) {
return;
}
let alreadySet;
topBarButtons.forEach(function (value) {
if (
value.hasOwnProperty("topbarMenuButtonRenderer") &&
value.topbarMenuButtonRenderer.icon &&
value.topbarMenuButtonRenderer.icon.iconType === "IRIDIUM_LOGO"
) {
return (alreadySet = true);
}
});
if (alreadySet) {
return;
}
topBarButtons.unshift({
topbarMenuButtonRenderer: {
icon: { iconType: "IRIDIUM_LOGO" },
tooltip: "Iridium",
style: "IRIDIUM_OPTIONS",
isDisabled: false,
},
});
};
const onDocumentClick = (event) => {
if (!iridiumSettings.extensionButton) {
return;
}
const optionsButton =
document.documentElement.querySelector(".iridium-options");
const buttonClicked =
optionsButton === event.target || optionsButton?.contains(event.target);
if (!buttonClicked) {
return;
}
if (document.activeElement && document.activeElement.blur) {
document.activeElement.blur();
}
Broadcaster.doAction("extensionButton");
};
document.documentElement.addEventListener("click", onDocumentClick, false);
OnYtPageDataFetched.addListener(listener);
OverrideFetch.onFetchListener(listener);
return {};
})();
const FeatureDefaultTab = (() => {
const iniDefaultTab = (event) => {
if (iridiumSettings.channelTab === "featured") {
return;
}
let link = event.target;
while (link && link?.tagName !== "A") {
link = link.parentNode;
}
if (!link) {
return;
}
const browseEndpoint = link?.["data"]?.["browseEndpoint"];
const metadata =
link?.["data"]?.["commandMetadata"]?.["webCommandMetadata"];
if (
!browseEndpoint ||
metadata["webPageType"] !== "WEB_PAGE_TYPE_CHANNEL"
) {
return;
}
let param = "";
switch (iridiumSettings.channelTab) {
case "featured":
param = "EghmZWF0dXJlZPIGBAoCMgA%3D";
break;
case "videos":
param = "EgZ2aWRlb3PyBgQKAjoA";
break;
case "shorts":
param = "EgZzaG9ydHPyBgUKA5oBAA%3D%3D";
break;
case "streams":
param = "EgdzdHJlYW1z8gYECgJ6AA%3D%3D";
break;
case "podcasts":
param = "Eghwb2RjYXN0c_IGBQoDugEA";
break;
case "playlists":
param = "EglwbGF5bGlzdHPyBgQKAkIA";
break;
case "community":
param = "Egljb21tdW5pdHnyBgQKAkoA";
break;
case "channels":
param = "EghjaGFubmVscw%3D%3D";
break;
case "store":
param = "EgVzdG9yZfIGBAoCGgA%3D";
break;
}
if (param !== "") {
browseEndpoint.params = param;
}
const url =
browseEndpoint["canonicalBaseUrl"] ||
(browseEndpoint["browseId"]
? `/channel/${browseEndpoint["browseId"]}`
: undefined);
if (url) {
metadata.url = link.href = `${url}/${iridiumSettings.channelTab}`;
}
};
const update = () => {
if (iridiumSettings.channelTab) {
window.addEventListener("mouseup", iniDefaultTab, true);
} else {
window.removeEventListener("mouseup", iniDefaultTab, true);
}
};
FeatureUpdater.register(SettingData.channelTab.id, update);
return {
update: update,
};
})();
const FeatureLogoSubscriptions = (() => {
const iniLogoShortcut = (event) => {
let link = event.target;
while (link && (link?.tagName !== "A" || link?.id !== "logo")) {
link = link.parentNode;
}
if (!link) {
return;
}
const browseEndpoint = link?.["data"]?.["browseEndpoint"];
const metadata =
link?.["data"]?.["commandMetadata"]?.["webCommandMetadata"];
if (browseEndpoint && metadata) {
browseEndpoint.browseId = "FEsubscriptions";
metadata.url = "/feed/subscriptions";
}
};
const update = () => {
if (iridiumSettings.logoSubscriptions) {
window.addEventListener("mouseup", iniLogoShortcut, true);
} else {
window.removeEventListener("mouseup", iniLogoShortcut, true);
}
};
FeatureUpdater.register(SettingData.logoSubscriptions.id, update);
return {
update: update,
};
})();
const FeaturePlaylistReverse = (() => {
const buttonId = "reversePlaylistButton";
const reversePlaylist = (data) => {
if (!data) {
return;
}
const watchNextResults =
data?.["contents"]?.["twoColumnWatchNextResults"];
const playlist = watchNextResults?.["playlist"]?.["playlist"];
if (
!!playlist?.["isReversed"] === iridiumSettings.reversePlaylistToggled
) {
return;
}
if (playlist) {
playlist["isReversed"] = iridiumSettings.reversePlaylistToggled;
}
const topLevelButtons =
playlist?.["playlistButtons"]?.["menuRenderer"]?.["topLevelButtons"];
if (topLevelButtons?.constructor === Array) {
const length = topLevelButtons.length;
for (let i = 0; i < length; i++) {
const button = topLevelButtons[i]?.["toggleButtonRenderer"];
if (button?.id === "reversePlaylist") {
button.isToggled = iridiumSettings.reversePlaylistToggled;
break;
}
}
}
const autoplay = watchNextResults?.["autoplay"]?.["autoplay"];
const autoplaySets = autoplay?.["sets"];
if (autoplaySets?.constructor === Array) {
for (let i = 0; i < autoplaySets.length; i++) {
const item = autoplaySets[i];
if (!item) {
continue;
}
if (item["previousButtonVideo"] && item["nextButtonVideo"]) {
item["autoplayVideo"] = item["previousButtonVideo"];
item["previousButtonVideo"] = item["nextButtonVideo"];
item["nextButtonVideo"] = item["autoplayVideo"];
}
}
}
const contents = playlist?.["contents"];
if (contents?.constructor === Array) {
contents.reverse();
const count = contents.length;
for (let i = 0; i < count; i++) {
const item = contents[i];
if (
item?.["playlistPanelVideoRenderer"]?.["indexText"]?.["simpleText"]
) {
item["playlistPanelVideoRenderer"]["indexText"]["simpleText"] =
`${i + 1}`;
}
}
if (playlist?.["currentIndex"]?.constructor === Number) {
playlist["currentIndex"] =
contents.length - playlist["currentIndex"] - 1;
}
if (playlist?.["localCurrentIndex"]?.constructor === Number) {
playlist["localCurrentIndex"] =
contents.length - playlist["localCurrentIndex"] - 1;
}
}
};
const listener = (data) => {
const topLevelButtons =
data?.["contents"]?.["twoColumnWatchNextResults"]?.["playlist"]?.[
"playlist"
]?.["playlistButtons"]?.["menuRenderer"]?.["topLevelButtons"];
if (topLevelButtons?.constructor !== Array) {
return;
}
const length = topLevelButtons.length;
for (let i = 0; i < length; i++) {
if (
topLevelButtons[i]?.["toggleButtonRenderer"]?.id === "reversePlaylist"
) {
if (iridiumSettings.reversePlaylist !== true) {
topLevelButtons.splice(i, 1);
}
break;
}
if (i === length - 1 && iridiumSettings.reversePlaylist === true) {
topLevelButtons.push({
toggleButtonRenderer: {
id: "reversePlaylist",
style: { styleType: "STYLE_GREY_TEXT" },
size: { sizeType: "SIZE_DEFAULT" },
isToggled: iridiumSettings.reversePlaylistToggled,
isDisabled: false,
defaultIcon: { iconType: "IRIDIUM_PLAYLIST_SHUFFLE" },
accessibility: { label: "reversePlaylistButton" },
defaultTooltip: "Reverse playlist",
toggledTooltip: "Reverse playlist",
toggledStyle: { styleType: "STYLE_DEFAULT_ACTIVE" },
},
});
}
}
reversePlaylist(data);
};
const updateUI = (data) => {
if (!data) {
return;
}
const ytPlaylistManager = document.querySelector("yt-playlist-manager");
if (!ytPlaylistManager) {
return;
}
const watchNextResults =
data?.["contents"]?.["twoColumnWatchNextResults"];
const autoplay = watchNextResults?.["autoplay"]?.["autoplay"];
const playlist = structuredClone(
watchNextResults?.["playlist"]?.["playlist"],
);
ytPlaylistManager?.["setAutoplayRenderer"]?.(autoplay);
ytPlaylistManager?.["setPlaylistData"]?.(playlist);
ytPlaylistManager?.["setPlayerPlaybackControlData"]?.({
playlistPanelRenderer: playlist,
});
};
const onDocumentClick = (event) => {
const ytdWatchData = document.querySelector("ytd-watch-flexy")?.["data"];
const reverseButton = document.querySelector(
`ytd-toggle-button-renderer:has([aria-label='${buttonId}'])`,
);
const isReverseButton = reverseButton?.contains(event.target);
if (isReverseButton && ytdWatchData) {
iridiumSettings.reversePlaylistToggled =
!iridiumSettings.reversePlaylistToggled;
Broadcaster.saveSetting(SettingData.reversePlaylistToggled.id);
reversePlaylist(ytdWatchData);
updateUI(ytdWatchData);
document
.querySelector("ytd-playlist-panel-video-renderer[selected]")
?.scrollIntoView({ block: "nearest" });
}
};
const update = () => {
const ytdWatchData = document.querySelector("ytd-watch-flexy")?.["data"];
if (iridiumSettings.reversePlaylist) {
document.documentElement.addEventListener(
"click",
onDocumentClick,
false,
);
if (ytdWatchData) {
listener(ytdWatchData);
updateUI(ytdWatchData);
}
} else {
iridiumSettings.reversePlaylistToggled = false;
Broadcaster.saveSetting(SettingData.reversePlaylistToggled.id);
if (ytdWatchData) {
listener(ytdWatchData);
updateUI(ytdWatchData);
document
.querySelector("ytd-playlist-panel-video-renderer[selected]")
?.scrollIntoView({ block: "nearest" });
}
document.documentElement.removeEventListener(
"click",
onDocumentClick,
false,
);
}
};
FeatureUpdater.register(SettingData.reversePlaylist.id, update);
OnYtPageDataFetched.addListener(listener);
OverrideFetch.onFetchListener(listener);
OverrideHandleResponse.onHandleResponseListener(listener);
return {};
})();
const FeatureSuperTheater = (() => {
const onResize = () => {
const ytdApp = document.querySelector("ytd-app");
if (
iridiumSettings.superTheater &&
ytdApp?.["fullscreen"] !== true &&
ytdApp?.["isWatchPage"] === true &&
ytdApp?.["isTheaterMode"]?.() === true &&
document.getElementById("chat")?.["isHiddenByUser"] !== true
) {
const chat = document.getElementById("chat-container");
if (chat && !chat.firstElementChild.hasAttribute("collapsed")) {
const videoContainer = document.getElementById(
"player-full-bleed-container",
);
if (videoContainer && !videoContainer.contains(chat)) {
videoContainer.appendChild(chat);
}
}
const masthead = document.getElementById("masthead-container");
const moviePlayerParent =
document.getElementById("movie_player")?.parentElement;
if (masthead && moviePlayerParent?.offsetWidth > 0) {
const width = `${moviePlayerParent.offsetWidth}px`;
if (masthead.style.width !== width) {
masthead.style.width = width;
}
}
} else {
const masthead = document.getElementById("masthead-container");
const moviePlayerParent =
document.getElementById("movie_player")?.parentElement;
if (masthead) {
masthead.style.width = "";
}
const chat = document.getElementById("chat-container");
if (chat && document.getElementById("full-bleed-container")) {
const sidebar = document.getElementById("secondary-inner");
if (sidebar && !sidebar.contains(chat)) {
const donationShelf = document.getElementById("donation-shelf");
if (donationShelf && sidebar === donationShelf.parentElement) {
sidebar.insertBefore(chat, donationShelf);
} else {
sidebar.prepend(chat);
}
}
}
}
};
const onYTAction = (event) => {
if (
iridiumSettings.superTheater &&
event?.detail?.["actionName"] === "yt-set-live-chat-collapsed"
) {
window.dispatchEvent(new CustomEvent("resize"));
}
};
const created = (api) => {
api.addEventListener("resize", onResize, false);
document.documentElement.addEventListener("yt-action", onYTAction, false);
};
const update = () => {
if (iridiumSettings.superTheater) {
document.documentElement.setAttribute("super-theater", "");
} else {
document.documentElement.removeAttribute("super-theater");
}
window.dispatchEvent(new CustomEvent("resize"));
};
FeatureUpdater.register(SettingData.superTheater.id, update);
OverrideOnPlayerReady.onReadyListener(created);
return {};
})();
const FeatureSuperTheaterScrollbar = (() => {
const update = () => {
if (iridiumSettings.superTheaterScrollbar) {
document.documentElement.removeAttribute("super-theater-scrollbar");
} else {
document.documentElement.setAttribute("super-theater-scrollbar", "");
}
};
FeatureUpdater.register(SettingData.superTheaterScrollbar.id, update);
return {};
})();
const FeatureAlwaysVisible = (() => {
let moviePlayer = null;
let maxX = null;
let maxY = null;
let pos = { x1: 0, x2: 0, y1: 0, y2: 0 };
const offset = 10;
const isPlayer = (target) =>
target.id === "movie_player" ||
document.getElementById("movie_player")?.contains(target);
const isAlwaysVisible = () =>
document.documentElement.hasAttribute("always-visible-player");
const updatePosition = () => {
if (
!iridiumSettings.alwaysVisible ||
!isAlwaysVisible() ||
!moviePlayer
) {
return;
}
const newX = (iridiumSettings.alwaysVisiblePosition.x ??= offset);
const newY = (iridiumSettings.alwaysVisiblePosition.y ??= offset);
const snapRight =
(iridiumSettings.alwaysVisiblePosition.snapRight ??= true);
const snapBottom =
(iridiumSettings.alwaysVisiblePosition.snapBottom ??= true);
if (newX < offset) {
moviePlayer.style.left = `${offset}px`;
moviePlayer.style.right = "unset";
} else if (snapRight || newX > maxX) {
moviePlayer.style.left = "unset";
moviePlayer.style.right = `${offset}px`;
} else {
moviePlayer.style.left = `${newX}px`;
moviePlayer.style.right = "unset";
}
if (newY < offset) {
moviePlayer.style.top = `${offset}px`;
moviePlayer.style.bottom = "unset";
} else if (snapBottom || newY > maxY) {
moviePlayer.style.top = "unset";
moviePlayer.style.bottom = `${offset}px`;
} else {
moviePlayer.style.top = `${newY}px`;
moviePlayer.style.bottom = "unset";
}
};
const onMouseMove = (event) => {
if (document.fullscreenElement || !moviePlayer) {
return false;
}
pos.x1 = pos.x2 - event.clientX;
pos.y1 = pos.y2 - event.clientY;
const newX = moviePlayer.offsetLeft - pos.x1;
const newY = moviePlayer.offsetTop - pos.y1;
if (newX > offset && newX < maxX) {
pos.x2 = event.clientX;
iridiumSettings.alwaysVisiblePosition.x = newX;
iridiumSettings.alwaysVisiblePosition.snapRight = false;
}
if (newY > offset && newY < maxY) {
pos.y2 = event.clientY;
iridiumSettings.alwaysVisiblePosition.y = newY;
iridiumSettings.alwaysVisiblePosition.snapBottom = false;
}
iridiumSettings.alwaysVisiblePosition.snapRight = newX >= maxX;
iridiumSettings.alwaysVisiblePosition.snapBottom = newY >= maxY;
updatePosition();
event.preventDefault();
event.stopPropagation();
return true;
};
const onMouseDown = (event) => {
if (
document.fullscreenElement ||
event.buttons !== 2 ||
!isPlayer(event.target)
) {
return true;
}
document.documentElement.setAttribute("moving", "");
pos.x2 = event.clientX;
pos.y2 = event.clientY;
maxX =
document.documentElement.clientWidth - moviePlayer.offsetWidth - offset;
maxY =
document.documentElement.clientHeight -
moviePlayer.offsetHeight -
offset;
const onMouseUp = function () {
document.documentElement.removeAttribute("moving");
window.removeEventListener("mouseup", onMouseUp, true);
window.removeEventListener("mousemove", onMouseMove, true);
Broadcaster.saveSetting(SettingData.alwaysVisiblePosition.id);
};
window.addEventListener("mousemove", onMouseMove, true);
window.addEventListener("mouseup", onMouseUp, true);
event.preventDefault();
event.stopPropagation();
return true;
};
const onContextMenu = function (event) {
if (
document.fullscreenElement ||
!isAlwaysVisible() ||
!isPlayer(event.target)
) {
return false;
}
event.preventDefault();
event.stopPropagation();
return true;
};
const preventScrollTop = (() => {
let original = null;
const override = () => {
const watchFlexy = document.querySelector("ytd-watch-flexy");
if (watchFlexy) {
original = watchFlexy.setScrollTop;
watchFlexy.setScrollTop = () => {};
}
};
const restore = () => {
const watchFlexy = document.querySelector("ytd-watch-flexy");
if (watchFlexy) {
watchFlexy.setScrollTop = original;
original = null;
}
};
return {
override: override,
restore: restore,
};
})();
const updateState = (event) => {
moviePlayer ??= document.getElementById("movie_player");
const parentRects = moviePlayer?.parentElement?.getBoundingClientRect();
if (!parentRects) {
return;
}
maxX =
document.documentElement.clientWidth - moviePlayer.offsetWidth - offset;
maxY =
document.documentElement.clientHeight -
moviePlayer.offsetHeight -
offset;
if (event?.type === "resize") {
updatePosition();
}
if (
iridiumSettings.alwaysVisible &&
!document.fullscreenElement &&
Util.isWatchPage() &&
parentRects.bottom < parentRects.height * 0.5
) {
if (!isAlwaysVisible()) {
preventScrollTop.override();
document.documentElement.setAttribute("always-visible-player", "");
window.addEventListener("mousedown", onMouseDown, true);
window.addEventListener("contextmenu", onContextMenu, true);
updatePosition();
window.dispatchEvent(new CustomEvent("resize"));
}
} else if (isAlwaysVisible()) {
preventScrollTop.restore();
document.documentElement.removeAttribute("always-visible-player");
window.removeEventListener("mousedown", onMouseDown, true);
window.removeEventListener("contextmenu", onContextMenu, true);
moviePlayer.removeAttribute("style");
window.dispatchEvent(new CustomEvent("resize"));
}
};
const update = () => {
if (iridiumSettings.alwaysVisible) {
window.addEventListener("yt-navigate-finish", updateState, false);
window.addEventListener("scroll", updateState, false);
window.addEventListener("resize", updateState, false);
} else {
window.removeEventListener("yt-navigate-finish", updateState, false);
window.removeEventListener("scroll", updateState, false);
window.removeEventListener("resize", updateState, false);
}
updateState();
};
FeatureUpdater.register(SettingData.alwaysVisible.id, update);
return {};
})();
const FeaturePlayerTools = (() => {
const getPlayerTools = () => {
const titleSection =
document.querySelector("#below ytd-watch-metadata #title") ||
document.querySelector("#bottom-grid");
if (!titleSection) {
return null;
}
let playerTools = document.getElementById("iridium-player-tools");
if (!playerTools) {
playerTools = document.createElement("div");
playerTools.id = "iridium-player-tools";
titleSection.prepend(playerTools);
titleSection.style.position = "relative";
} else {
playerTools.replaceChildren();
}
return playerTools;
};
const checkPlayerToolsSpacing = (playerTools) => {
if (!playerTools) {
playerTools = document.getElementById("iridium-player-tools");
if (!playerTools || playerTools.childElementCount === 0) {
playerTools?.remove();
return;
}
}
const titleSection = document.querySelector(
"#below ytd-watch-metadata #title",
);
if (titleSection) {
titleSection.style.paddingRight = (playerTools.offsetWidth || 0) + "px";
}
};
const update = () => {
const playerTools = getPlayerTools();
if (!playerTools) {
return;
}
FeatureAutoplayShortcut.check(playerTools);
FeatureVideoFocus.check(playerTools);
FeatureScreenshot.update(playerTools);
FeatureThumbnail.update(playerTools);
FeatureMonetizationInfo.update(playerTools);
checkPlayerToolsSpacing(playerTools);
};
OnPageDataChanged.addListener(update);
FeatureUpdater.register(SettingData.videoFocusToggle.id, update);
FeatureUpdater.register(SettingData.videoScreenshot.id, update);
FeatureUpdater.register(SettingData.videoThumbnail.id, update);
FeatureUpdater.register(SettingData.monetizationInfo.id, update);
return {};
})();
const FeatureVideoFocus = (() => {
const onCreated = (api) => {
api.addEventListener("onStateChange", (state) => {
if (iridiumSettings.videoFocus) {
switch (state) {
case 1:
case 3:
document.documentElement.setAttribute("dim", "");
break;
default:
document.documentElement.removeAttribute("dim");
}
}
});
};
const update = () => {
if (iridiumSettings.videoFocus) {
document
.getElementById("iridium-video-focus")
?.setAttribute("iridium-enabled", "");
switch (
document.getElementById("movie_player")?.["getPlayerState"]?.()
) {
case 1:
case 3:
document.documentElement.setAttribute("dim", "");
break;
}
} else {
document
.getElementById("iridium-video-focus")
?.removeAttribute("iridium-enabled");
document.documentElement.removeAttribute("dim");
}
};
const check = (playerTools) => {
if (!iridiumSettings.videoFocusToggle) {
document.getElementById("iridium-focus")?.remove();
} else {
let videoFocusButton = document.getElementById("iridium-focus");
if (!videoFocusButton) {
videoFocusButton = document.createElement("div");
videoFocusButton.id = "iridium-focus";
videoFocusButton.title = "Video focus";
videoFocusButton.appendChild(
j2d.makeSVG(
"svg",
{
id: "iridium-video-focus",
"iridium-enabled": "",
viewBox: "0 -960 960 960",
height: "24",
width: "24",
},
[
j2d.makeSVG("path", {
id: "iridium-on",
d: "M112.857-378Q104-378 97.5-384.643t-6.5-15.5Q91-409 97.643-415.5t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5Zm0-160Q104-538 97.5-544.643t-6.5-15.5Q91-569 97.643-575.5t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5ZM235-196.154q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731Zm0-164q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731Zm0-162q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731Zm0-164q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731Zm164.299 343.846q-23.914 0-40.953-16.739-17.038-16.74-17.038-40.654 0-23.914 16.739-40.953 16.74-17.038 40.654-17.038 23.914 0 40.953 16.739 17.038 16.74 17.038 40.654 0 23.914-16.739 40.953-16.74 17.038-40.654 17.038Zm0-162q-23.914 0-40.953-16.739-17.038-16.74-17.038-40.654 0-23.914 16.739-40.953 16.74-17.038 40.654-17.038 23.914 0 40.953 16.739 17.038 16.74 17.038 40.654 0 23.914-16.739 40.953-16.74 17.038-40.654 17.038ZM397-196.154q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731Zm0-490q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731ZM396.857-92Q388-92 381.5-98.643t-6.5-15.5q0-8.857 6.643-15.357t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5Zm0-734q-8.857 0-15.357-6.643t-6.5-15.5q0-8.857 6.643-15.357t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5Zm164.442 483.692q-23.914 0-40.953-16.739-17.038-16.74-17.038-40.654 0-23.914 16.739-40.953 16.74-17.038 40.654-17.038 23.914 0 40.953 16.739 17.038 16.74 17.038 40.654 0 23.914-16.739 40.953-16.74 17.038-40.654 17.038Zm0-162q-23.914 0-40.953-16.739-17.038-16.74-17.038-40.654 0-23.914 16.739-40.953 16.74-17.038 40.654-17.038 23.914 0 40.953 16.739 17.038 16.74 17.038 40.654 0 23.914-16.739 40.953-16.74 17.038-40.654 17.038ZM561-196.154q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731Zm0-490q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731ZM560.857-92Q552-92 545.5-98.643t-6.5-15.5q0-8.857 6.643-15.357t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5Zm0-734q-8.857 0-15.357-6.643t-6.5-15.5q0-8.857 6.643-15.357t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5ZM725-196.154q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731Zm0-164q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731Zm0-162q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731Zm0-164q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731ZM846.857-378q-8.857 0-15.357-6.643t-6.5-15.5q0-8.857 6.643-15.357t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5Zm0-160q-8.857 0-15.357-6.643t-6.5-15.5q0-8.857 6.643-15.357t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5Z",
}),
j2d.makeSVG("path", {
id: "iridium-off",
d: "M798.923-96.23 95.231-798.924q-10.616-10.615-11-23.269-.385-12.654 11-24.039 11.384-11.384 23.654-11.384 12.269 0 23.654 11.384l703.692 703.692q10.615 10.616 11 22.77.384 12.154-11 23.538-11.385 11.385-23.654 11.385-12.27 0-23.654-11.385ZM396.857-92Q388-92 381.5-98.643t-6.5-15.5q0-8.857 6.643-15.357t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5Zm164 0Q552-92 545.5-98.643t-6.5-15.5q0-8.857 6.643-15.357t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5ZM235-196.154q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731Zm162 0q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731Zm164 0q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731ZM399-342.308q-22.846 0-40.269-17.423T341.308-400q0-22.846 17.423-40.269T399-457.692q22.846 0 40.269 17.423T456.692-400q0 22.846-17.423 40.269T399-342.308Zm-164-17.846q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731Zm507.154-.231-56.769-56.769q6.076-10.384 15.884-16.538t23.943-6.154q16.295 0 27.964 11.67 11.67 11.669 11.67 27.964 0 14.135-6.154 23.943-6.154 9.808-16.538 15.884ZM112.857-378Q104-378 97.5-384.643t-6.5-15.5Q91-409 97.643-415.5t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5Zm734 0q-8.857 0-15.357-6.643t-6.5-15.5q0-8.857 6.643-15.357t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5ZM591.231-510.538l-81.693-81.693q8.231-12.952 22.423-20.207 14.193-7.254 29.416-7.254 22.469 0 39.892 17.423 17.423 17.423 17.423 39.892 0 15.223-7.038 29.031-7.039 13.808-20.423 22.808ZM235-522.154q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731Zm490 0q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731ZM112.857-538Q104-538 97.5-544.643t-6.5-15.5Q91-569 97.643-575.5t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5Zm734 0q-8.857 0-15.357-6.643t-6.5-15.5q0-8.857 6.643-15.357t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5ZM561-686.154q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731Zm-146.615-1.231-56-56q5.307-9.384 15.109-15.923 9.803-6.538 23.506-6.538 16.385 0 28.115 11.67 11.731 11.669 11.731 27.964 0 14.135-6.538 23.827-6.539 9.693-15.923 15ZM725-686.154q-16.385 0-28.115-11.731-11.731-11.73-11.731-28.115 0-16.385 11.731-28.115 11.73-11.731 28.115-11.731 16.385 0 28.115 11.731 11.731 11.73 11.731 28.115 0 16.385-11.731 28.115-11.73 11.731-28.115 11.731ZM396.857-826q-8.857 0-15.357-6.643t-6.5-15.5q0-8.857 6.643-15.357t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5Zm164 0q-8.857 0-15.357-6.643t-6.5-15.5q0-8.857 6.643-15.357t15.5-6.5q8.857 0 15.357 6.643t6.5 15.5q0 8.857-6.643 15.357t-15.5 6.5Z",
}),
],
),
);
playerTools?.appendChild(videoFocusButton);
}
if (videoFocusButton) {
update();
videoFocusButton.onclick = () => {
iridiumSettings.videoFocus = !iridiumSettings.videoFocus;
Broadcaster.saveSetting(SettingData.videoFocus.id);
update();
};
}
}
};
FeatureUpdater.register(SettingData.videoFocusToggle.id, update);
FeatureUpdater.register(SettingData.videoFocus.id, update);
OverrideOnPlayerReady.onReadyListener(onCreated);
return {
check: check,
};
})();
const FeatureScreenshot = (() => {
const update = (playerTools) => {
if (!iridiumSettings.videoScreenshot) {
document.getElementById("iridium-screenshot")?.remove();
} else if (playerTools) {
let screenshotButton = document.getElementById("iridium-screenshot");
if (!screenshotButton) {
screenshotButton = document.createElement("div");
screenshotButton.id = "iridium-screenshot";
screenshotButton.title = "Screenshot";
screenshotButton.appendChild(
j2d.makeSVG(
"svg",
{ viewBox: "0 -960 960 960", height: "24", width: "24" },
[
j2d.makeSVG("path", {
d: "M686.308-395.692h-61.616q-12.615 0-21.654 9.038Q594-377.615 594-365t9.038 21.654q9.039 9.038 21.654 9.038h77.693q18.923 0 32.115-13.192 13.192-13.192 13.192-32.115v-77.693q0-12.615-9.038-21.654Q729.615-488 717-488t-21.654 9.038q-9.038 9.039-9.038 21.654v61.616ZM273.692-652.308h61.616q12.615 0 21.654-9.038Q366-670.385 366-683t-9.038-21.654q-9.039-9.038-21.654-9.038h-77.693q-18.923 0-32.115 13.192-13.192 13.192-13.192 32.115v77.693q0 12.615 9.038 21.654Q230.385-560 243-560t21.654-9.038q9.038-9.039 9.038-21.654v-61.616ZM184.615-216q-38.34 0-64.478-26.137Q94-268.275 94-306.615v-434.77q0-38.34 26.137-64.478Q146.275-832 184.615-832h590.77q38.34 0 64.478 26.137Q866-779.725 866-741.385v434.77q0 38.34-26.137 64.478Q813.725-216 775.385-216H628v42.693q0 18.923-13.192 32.115Q601.616-128 582.693-128H377.307q-18.923 0-32.115-13.192Q332-154.384 332-173.307V-216H184.615Zm0-66h590.77q9.23 0 16.923-7.692Q800-297.385 800-306.615v-434.77q0-9.23-7.692-16.923Q784.615-766 775.385-766h-590.77q-9.23 0-16.923 7.692Q160-750.615 160-741.385v434.77q0 9.23 7.692 16.923Q175.385-282 184.615-282ZM160-282v-484 484Z",
}),
],
),
);
playerTools.appendChild(screenshotButton);
}
if (screenshotButton) {
screenshotButton.onclick = function () {
const video = document.querySelector("video");
if (!video || !video.src) {
return;
}
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const aspectRatio = video.videoWidth / video.videoHeight;
const canvasWidth = video.videoWidth;
const canvasHeight = parseInt(`${canvasWidth / aspectRatio}`, 10);
canvas.width = canvasWidth;
canvas.height = canvasHeight;
context.drawImage(video, 0, 0, canvasWidth, canvasHeight);
canvas.toBlob((blob) => window.open(URL.createObjectURL(blob)));
};
}
}
};
return {
update: update,
};
})();
const FeatureThumbnail = (() => {
const update = (playerTools) => {
if (!iridiumSettings.videoThumbnail) {
document.getElementById("iridium-thumbnail")?.remove();
} else {
let thumbnailButton = document.getElementById("iridium-thumbnail");
if (!thumbnailButton) {
thumbnailButton = document.createElement("div");
thumbnailButton.id = "iridium-thumbnail";
thumbnailButton.title = "Thumbnail";
thumbnailButton.appendChild(
j2d.makeSVG(
"svg",
{ viewBox: "0 -960 960 960", height: "24", width: "24" },
[
j2d.makeSVG("path", {
d: "M224.615-134q-38.34 0-64.478-26.137Q134-186.275 134-224.615v-510.77q0-38.34 26.137-64.478Q186.275-826 224.615-826h510.77q38.34 0 64.478 26.137Q826-773.725 826-735.385v510.77q0 38.34-26.137 64.478Q773.725-134 735.385-134h-510.77Zm0-66h510.77q9.23 0 16.923-7.692Q760-215.385 760-224.615v-510.77q0-9.23-7.692-16.923Q744.615-760 735.385-760h-510.77q-9.23 0-16.923 7.692Q200-744.615 200-735.385v510.77q0 9.23 7.692 16.923Q215.385-200 224.615-200ZM200-200v-560 560Zm127.307-84h311.539q14.693 0 20.154-12.192 5.462-12.193-2.23-23.654L573-433.385q-7.231-9.461-17.923-8.961-10.692.5-17.923 9.961l-88.692 111.923-54.077-64q-7.077-8.692-17.462-8.692t-17.615 9.461l-49.154 63.847q-8.462 11.461-3 23.654Q312.615-284 327.307-284ZM340-567q21.955 0 37.478-15.522Q393-598.045 393-620q0-21.955-15.522-37.478Q361.955-673 340-673q-21.955 0-37.478 15.522Q287-641.955 287-620q0 21.955 15.522 37.478Q318.045-567 340-567Z",
}),
],
),
);
playerTools?.appendChild(thumbnailButton);
}
if (thumbnailButton) {
thumbnailButton.onclick = function () {
const playerResponse =
document.querySelector("ytd-app")?.["data"]?.["playerResponse"];
const thumbnails = Util.getSingleObjectByKey(
playerResponse,
"thumbnails",
);
if (thumbnails) {
let maxRes = null;
for (let thumbnailsKey in thumbnails) {
const thumbnail = thumbnails[thumbnailsKey];
if (
maxRes === null ||
thumbnail?.["width"] > maxRes?.["width"]
) {
maxRes = thumbnail;
}
}
if (maxRes?.["url"]) {
window.open(maxRes["url"]);
}
}
};
}
}
};
return {
update: update,
};
})();
const FeatureMonetizationInfo = (() => {
const monetizationKey = crypto.randomUUID();
const getData = (data) => {
if (data.constructor === Object) {
if (data["videoDetails"]) {
return data;
} else {
return Util.getSingleObjectByKey(data, "raw_player_response");
}
} else {
return document
.querySelector("ytd-page-manager")
?.["getCurrentData"]?.()?.["playerResponse"];
}
};
const update = (data) => {
if (!iridiumSettings.monetizationInfo) {
document.getElementById("iridium-monetization")?.remove();
} else {
const playerResponse = getData(data);
if (!playerResponse) {
return;
}
const videoId = playerResponse["videoDetails"]?.["videoId"];
const stored = (playerResponse[monetizationKey] ??= {
id: "",
sponsored: false,
adCount: 0,
});
if (stored.id !== videoId) {
stored.id = videoId;
stored.sponsored = playerResponse["paidContentOverlay"];
stored.adCount = playerResponse["adPlacements"]?.length || 0;
}
const playerTools = data instanceof HTMLElement ? data : null;
let monetizationButton = document.getElementById(
"iridium-monetization",
);
if (!monetizationButton && playerTools) {
monetizationButton = j2d.make(
"div",
{ id: "iridium-monetization", title: "Monetization state" },
[
j2d.makeSVG(
"svg",
{ viewBox: "0 -960 960 960", height: "24", width: "24" },
[
j2d.makeSVG("path", {
class: "iridium-on",
d: "M324-370q14 48 43.5 77.5T444-252v17q0 14 10.5 24.5T479-200q14 0 24.5-10.5T514-235v-15q50-9 86-39t36-89q0-42-24-77t-96-61q-60-20-83-35t-23-41q0-26 18.5-41t53.5-15q32 0 50 15.5t26 38.5l64-26q-11-35-40.5-61T516-710v-15q0-14-10.5-24.5T481-760q-14 0-24.5 10.5T446-725v15q-50 11-78 44t-28 74q0 47 27.5 76t86.5 50q63 23 87.5 41t24.5 47q0 33-23.5 48.5T486-314q-33 0-58.5-20.5T390-396l-66 26ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z",
}),
j2d.makeSVG("path", {
class: "iridium-off",
d: "M328-364.769q15.077 47.077 45.808 74.538 30.73 27.462 75.577 37.923V-231q0 12.846 8.384 21.231 8.385 8.384 21.231 8.384 12.846 0 21.231-8.384 8.384-8.385 8.384-21.231v-19.308q49.077-8.615 86.539-36.923 37.461-28.307 37.461-88.769 0-44-24.538-76.615-24.538-32.616-98.538-58.616-65.154-23.077-86.039-38.615-20.885-15.539-20.885-44.154 0-27.615 21.385-45.5t58-17.885q31.462 0 50.269 15.462 18.808 15.462 29.423 37.923l54.77-21.692q-12.077-32.846-40.77-57.308-28.692-24.462-65.077-26.692V-729q0-12.846-8.384-21.231-8.385-8.384-21.231-8.384-12.846 0-21.231 8.384-8.384 8.385-8.384 21.231v19.308q-52.308 9.692-80.154 42.5-27.846 32.807-27.846 73.192 0 46.154 28.115 75.077Q399.615-490 462-467.692q64.538 23.769 88.731 41.923 24.192 18.154 24.192 49.769 0 38.231-27.308 54.808-27.307 16.577-61.23 16.577-34.923 0-62.231-20.616-27.308-20.615-40.923-60.769L328-364.769Zm152.134 276.77q-81.673 0-152.91-30.84t-124.365-83.922q-53.127-53.082-83.993-124.257Q88-398.194 88-479.866q0-81.673 30.839-153.41 30.84-71.737 83.922-124.865 53.082-53.127 124.257-83.493Q398.194-872 479.866-872q81.673 0 153.41 30.339 71.737 30.34 124.865 83.422 53.127 53.082 83.493 124.757Q872-561.806 872-480.134q0 81.673-30.339 152.91-30.34 71.237-83.422 124.365-53.082 53.127-124.757 83.993Q561.806-88 480.134-88ZM480-154q137 0 231.5-94.5T806-480q0-137-94.5-231.5T480-806q-137 0-231.5 94.5T154-480q0 137 94.5 231.5T480-154Zm0-326Z",
}),
],
),
j2d.make("div", { id: "iridium-monetization-count" }),
],
);
playerTools.appendChild(monetizationButton);
}
if (monetizationButton) {
const title = [];
if (stored.sponsored) {
monetizationButton.classList.add("sponsored");
title.push("Sponsored");
} else {
monetizationButton.classList.remove("sponsored");
}
if (stored.adCount > 0) {
monetizationButton.classList.add("monetized");
title.push(`Monetized (${stored.adCount} ads)`);
document
.getElementById("iridium-monetization-count")
?.replaceChildren(document.createTextNode(stored.adCount));
} else {
monetizationButton.classList.remove("monetized");
title.push("Not monetized");
document
.getElementById("iridium-monetization-count")
?.replaceChildren(document.createTextNode(""));
}
monetizationButton.title = title.join("\n");
}
}
};
OverrideFetch.onFetchListener(update);
OverrideHandleResponse.onHandleResponseListener(update);
return {
update: update,
};
})();
const FeatureAutoplayShortcut = (() => {
const update = () => {
const button = document.getElementById("iridium-autoplay-shortcut");
if (!button) {
return;
}
if (iridiumSettings.autoplay) {
button.setAttribute("iridium-enabled", "");
button.title = "Autoplay on";
} else {
button.removeAttribute("iridium-enabled");
button.title = "Autoplay off";
}
};
const check = (playerTools) => {
if (!iridiumSettings.autoplayShortcut) {
document.getElementById("iridium-autoplay-shortcut")?.remove();
} else if (playerTools) {
let button = document.getElementById("iridium-autoplay-shortcut");
if (!button) {
button = j2d.make(
"div",
{
id: "iridium-autoplay-shortcut",
title: "Monetization state",
"iridium-enabled": "",
},
[
j2d.makeSVG(
"svg",
{ viewBox: "0 -960 960 960", height: "24", width: "24" },
[
j2d.makeSVG("path", {
d: "M320-263v-438q0-15 10-24.167 10-9.167 23.333-9.167 4.333 0 8.833 1.167t8.834 3.5L715.667-510q7.667 5.333 11.5 12.333T731-482q0 8.667-3.833 15.667-3.833 6.999-11.5 12.333L371-234.333q-4.334 2.333-8.834 3.5-4.5 1.166-8.833 1.166-13.333 0-23.333-9.166Q320-248 320-263Z",
}),
],
),
],
);
playerTools.appendChild(button);
}
if (button) {
update();
button.onclick = () => {
iridiumSettings.autoplay = !iridiumSettings.autoplay;
Broadcaster.saveSetting(SettingData.autoplay.id);
update();
};
}
}
};
FeatureUpdater.register(SettingData.autoplayShortcut.id, update);
return {
check: check,
};
})();
const FeatureDefaultQuality = (() => {
const qualityList = [
"highres",
"hd2880",
"hd2160",
"hd1440",
"hd1080",
"hd720",
"large",
"medium",
"small",
"tiny",
"auto",
];
const isValidQuality = (quality) => qualityList.indexOf(quality) > -1;
const getClosestAvailableQuality = (quality, availableList) => {
const qualityIndex = qualityList.indexOf(quality);
if (qualityIndex === -1 || availableList?.length < 1) {
return availableList[0] || "auto";
}
const total = qualityList.length - 1;
let top = qualityIndex + 1;
let bottom = qualityIndex - 1;
for (let i = 0; i < total; i++) {
const topIndex = availableList.indexOf(qualityList[top]);
const bottomIndex = availableList.indexOf(qualityList[bottom]);
if (topIndex > -1) {
return availableList[topIndex];
}
if (bottomIndex > -1) {
return availableList[bottomIndex];
}
top += 1;
bottom -= 1;
}
return availableList[0] || "auto";
};
const getAvailableQuality = (quality, availableList) => {
if (availableList.indexOf(quality) > -1) {
return quality;
}
return getClosestAvailableQuality(quality, availableList);
};
const onCreated = (api) =>
api.addEventListener("onStateChange", () => {
if (
iridiumSettings.defaultQuality !== "auto" &&
isValidQuality(iridiumSettings.defaultQuality)
) {
const currentQuality = api?.["getPreferredQuality"]?.();
if (currentQuality === "auto") {
const availableQualityList = api?.["getAvailableQualityLevels"]?.();
if (availableQualityList?.length > 0) {
const qualitySet = getAvailableQuality(
iridiumSettings.defaultQuality,
availableQualityList,
);
api?.["setPlaybackQuality"]?.(qualitySet);
api?.["setPlaybackQualityRange"]?.(qualitySet);
}
}
}
});
OverrideOnPlayerReady.onReadyListener(onCreated);
return {};
})();
const FeatureDefaultSpeed = (() => {
const isValidSpeed = (speed) =>
["0.25", "0.5", "0.75", "1", "1.25", "1.5", "1.75", "2", "-1"].indexOf(
speed,
) > -1;
const getAvailableSpeed = (speed, availableList) => {
const speedNumber = Number(speed);
if (availableList.indexOf(speedNumber) > -1) {
return speedNumber;
} else {
return 1;
}
};
let currentId = "";
const onCreated = (api) =>
api.addEventListener("onStateChange", () => {
if (
iridiumSettings.defaultSpeed !== "-1" &&
isValidSpeed(iridiumSettings.defaultSpeed)
) {
const videoId = api?.["getVideoData"]?.()?.["video_id"] || "";
const currentSpeed = api?.["getPlaybackRate"]?.()?.toString();
if (
iridiumSettings.defaultSpeed !== currentSpeed &&
currentId !== videoId
) {
const availableSpeedList = api?.["getAvailablePlaybackRates"]?.();
if (availableSpeedList?.length > 0) {
currentId = videoId;
const speedSet = getAvailableSpeed(
iridiumSettings.defaultSpeed,
availableSpeedList,
);
api?.["setPlaybackRate"]?.(speedSet);
}
}
}
});
OverrideOnPlayerReady.onReadyListener(onCreated);
return {};
})();
const FeatureAnnotations = (() => {
const onCreated = (api) =>
api.addEventListener("videodatachange", () => {
if (!iridiumSettings.annotations) {
api?.["unloadModule"]?.("annotations_module");
}
});
OverrideOnPlayerReady.onReadyListener(onCreated);
return {};
})();
const FeatureAdManager = (() => {
const isAdAllowed = (isSubscribed, enabled) => {
// when all ads are opted out
if (iridiumSettings.adOptOutAll) {
// ads will be allowed only if:
// the exception for videos from subscribed channels is enabled
// and the video belongs to a subscribed channel
return iridiumSettings.adSubscribed && isSubscribed;
}
// ads will be allowed if:
// the exception for videos from subscribed channels is enabled
// the video belongs to a subscribed channel
// and the option is enabled
if (iridiumSettings.adSubscribed) {
return isSubscribed && enabled;
}
// otherwise the setting itself prevails
return enabled;
};
const checkHomePageAds = (itemContainer) => {
for (let i = itemContainer.length - 1; i >= 0; i--) {
const itemRenderer = itemContainer[i];
const exists =
itemRenderer?.["richItemRenderer"]?.["content"]?.["adSlotRenderer"] ||
itemRenderer?.["richSectionRenderer"]?.["content"]?.[
"statementBannerRenderer"
] ||
itemRenderer?.["richSectionRenderer"]?.["content"]?.[
"brandVideoShelfRenderer"
] ||
itemRenderer?.["richSectionRenderer"]?.["content"]?.[
"brandVideoSingletonRenderer"
];
if (exists) {
itemContainer.splice(i, 1);
}
}
};
const listener = (data) => {
const subscribeButtonRenderer = Util.getSingleObjectByKey(
data,
"subscribeButtonRenderer",
);
const isSubscribed = subscribeButtonRenderer?.["subscribed"] === true;
const isAdTaggedProductsAllowed = isAdAllowed(
isSubscribed,
iridiumSettings.adTaggedProducts,
);
if (!isAdTaggedProductsAllowed) {
// player args
const playerOverlayRenderer = Util.getSingleObjectByKey(
data,
"playerOverlayRenderer",
);
// video tagged products
if (playerOverlayRenderer?.["productsInVideoOverlayRenderer"]) {
delete playerOverlayRenderer["productsInVideoOverlayRenderer"];
}
}
const isAdMastheadAllowed = isAdAllowed(
isSubscribed,
iridiumSettings.adMasthead,
);
const isAdHomeFeedAllowed = isAdAllowed(
isSubscribed,
iridiumSettings.adHomeFeed,
);
if (!isAdMastheadAllowed || !isAdHomeFeedAllowed) {
// home page ads
const richGridRenderer = Util.getSingleObjectByKey(
data,
"richGridRenderer",
);
if (!isAdMastheadAllowed) {
const exists =
richGridRenderer?.["masthead"]?.["bannerPromoRenderer"] ||
richGridRenderer?.["masthead"]?.["adSlotRenderer"];
if (exists) {
delete richGridRenderer["masthead"];
}
const bigYoodle = richGridRenderer?.["bigYoodle"];
if (bigYoodle) {
delete richGridRenderer?.["bigYoodle"];
}
}
if (!isAdHomeFeedAllowed) {
const itemContainer = richGridRenderer?.["contents"];
// home page list ads
if (
itemContainer?.constructor === Array &&
itemContainer.length > 0
) {
checkHomePageAds(itemContainer);
}
const actions = data?.["onResponseReceivedActions"];
// home page list continuation ads
if (actions?.constructor === Array && actions.length > 0) {
for (let i = actions.length - 1; i >= 0; i--) {
const continuationItems =
actions[i]?.["appendContinuationItemsAction"]?.[
"continuationItems"
];
if (
continuationItems?.constructor === Array &&
continuationItems.length > 0
) {
checkHomePageAds(continuationItems);
}
}
}
}
}
const isAdSearchFeedAllowed = isAdAllowed(
isSubscribed,
iridiumSettings.adSearchFeed,
);
if (!isAdSearchFeedAllowed) {
// search results ads
const sectionListRenderer = Util.getSingleObjectByKey(
data,
"sectionListRenderer",
);
const sectionListRendererContents = sectionListRenderer?.["contents"];
if (
sectionListRendererContents?.constructor === Array &&
sectionListRendererContents.length > 0
) {
for (let i = sectionListRendererContents.length - 1; i >= 0; i--) {
const itemRenderer = sectionListRendererContents[i];
const itemRendererContents =
itemRenderer?.["itemSectionRenderer"]?.["contents"];
if (
itemRendererContents?.constructor === Array &&
itemRendererContents.length > 0
) {
for (let j = itemRendererContents.length - 1; j >= 0; j--) {
const subItemRender = itemRendererContents[j];
if (
subItemRender?.["adSlotRenderer"] ||
subItemRender?.["searchPyvRenderer"]
) {
itemRendererContents.splice(j, 1);
}
}
if (itemRendererContents.length === 0) {
sectionListRendererContents.splice(i, 1);
}
}
}
}
}
const isAdVideoFeedAllowed = isAdAllowed(
isSubscribed,
iridiumSettings.adVideoFeed,
);
if (!isAdVideoFeedAllowed) {
// video page sidebar ads
const secondaryResults = Util.getSingleObjectByKey(
data,
"secondaryResults",
)?.["secondaryResults"];
const results = secondaryResults?.["results"];
if (results?.constructor === Array && results.length > 0) {
for (let i = results.length - 1; i >= 0; i--) {
const itemRenderer = results[i];
const adSlotRenderer = itemRenderer["adSlotRenderer"];
if (adSlotRenderer) {
results.splice(i, 1);
} else {
const itemRendererContents =
itemRenderer?.["itemSectionRenderer"]?.["contents"];
if (
itemRendererContents?.constructor === Array &&
itemRendererContents.length > 0
) {
for (let j = itemRendererContents.length - 1; j >= 0; j--) {
const subItemRender = itemRendererContents[j];
if (subItemRender?.["adSlotRenderer"]) {
itemRendererContents.splice(j, 1);
}
}
if (itemRendererContents.length === 0) {
results.splice(i, 1);
}
}
}
}
}
}
};
const playerConfig = (args) => {
const subscribeButtonRenderer = Util.getSingleObjectByKey(
args,
"subscribeButtonRenderer",
);
const isSubscribed = subscribeButtonRenderer?.["subscribed"] === true;
const isInVideoAdsAllowed = isAdAllowed(
isSubscribed,
iridiumSettings.adInVideo,
);
if (!isInVideoAdsAllowed) {
const adPlacementsParent = Util.getSingleObjectAndParentByKey(
args,
"adPlacements",
);
const adSlotsParent = Util.getSingleObjectAndParentByKey(
args,
"adSlots",
);
const playerAdsParent = Util.getSingleObjectAndParentByKey(
args,
"playerAds",
);
if (adPlacementsParent?.parent?.["adPlacements"]) {
delete adPlacementsParent.parent["adPlacements"];
}
if (adSlotsParent?.parent?.["adSlots"]) {
delete adSlotsParent.parent["adSlots"];
}
if (playerAdsParent?.parent?.["playerAds"]) {
delete playerAdsParent.parent["playerAds"];
}
}
};
OnYtPageDataFetched.addListener(listener);
OverrideFetch.onFetchListener(listener);
OverrideFetch.onFetchListener(playerConfig);
OverrideHandleResponse.onHandleResponseListener(listener);
OverrideHandleResponse.onHandleResponseListener(playerConfig);
return {};
})();
const FeatureShorts = (() => {
const listener = (data) => {
if (!iridiumSettings.searchShorts) {
// search results shorts
const listContents =
Util.getSingleObjectByKey(data, "sectionListRenderer")?.[
"contents"
] ||
Util.getSingleObjectByKey(data, "appendContinuationItemsAction")?.[
"continuationItems"
];
if (listContents?.constructor === Array && listContents.length > 0) {
for (let i = listContents.length - 1; i >= 0; i--) {
const itemSectionRenderer =
listContents[i]?.["itemSectionRenderer"];
const itemSectionRendererContents =
itemSectionRenderer?.["contents"];
if (
itemSectionRendererContents?.constructor === Array &&
itemSectionRendererContents.length > 0
) {
for (
let j = itemSectionRendererContents.length - 1;
j >= 0;
j--
) {
if (
itemSectionRendererContents[j]?.["reelShelfRenderer"] ||
itemSectionRendererContents[j]?.["videoRenderer"]?.[
"navigationEndpoint"
]?.["reelWatchEndpoint"]
) {
itemSectionRendererContents.splice(j, 1);
} else if (itemSectionRendererContents[j]?.["shelfRenderer"]) {
const shelfRendererItems =
itemSectionRendererContents[j]?.["shelfRenderer"]?.[
"content"
]?.["verticalListRenderer"]?.["items"];
if (
shelfRendererItems?.constructor === Array &&
shelfRendererItems.length > 0
) {
for (let k = shelfRendererItems.length - 1; k >= 0; k--) {
const reelWatchEndpoint =
shelfRendererItems[k]?.["videoRenderer"]?.[
"navigationEndpoint"
]?.["reelWatchEndpoint"];
if (reelWatchEndpoint) {
shelfRendererItems.splice(k, 1);
}
}
if (shelfRendererItems.length === 0) {
itemSectionRendererContents.splice(j, 1);
}
}
}
}
if (itemSectionRendererContents.length === 0) {
listContents.splice(i, 1);
}
}
}
}
}
if (
(!iridiumSettings.homeShorts && window.location.pathname === "/") ||
(!iridiumSettings.subscriptionsShorts &&
window.location.pathname === "/feed/subscriptions")
) {
// home page and grid view subscriptions shorts
const richGridRenderer = Util.getSingleObjectByKey(
data,
"richGridRenderer",
);
const richGridRendererContents = richGridRenderer?.["contents"];
if (
richGridRendererContents?.constructor === Array &&
richGridRendererContents.length > 0
) {
for (let i = richGridRendererContents.length - 1; i >= 0; i--) {
const richShelfRendererContents =
richGridRendererContents[i]?.["richSectionRenderer"]?.[
"content"
]?.["richShelfRenderer"]?.["contents"];
if (
richShelfRendererContents?.constructor === Array &&
richShelfRendererContents.length > 0
) {
for (let j = richShelfRendererContents.length - 1; j >= 0; j--) {
const richItemRendererContent =
richShelfRendererContents[j]?.["richItemRenderer"]?.[
"content"
];
const reelItemRenderer =
richItemRendererContent?.["reelItemRenderer"];
const shortsLockupViewModel =
richItemRendererContent?.["shortsLockupViewModel"];
if (reelItemRenderer || shortsLockupViewModel) {
richShelfRendererContents.splice(j, 1);
}
}
if (richShelfRendererContents.length === 0) {
richGridRendererContents.splice(i, 1);
}
}
}
}
// list view subscriptions shorts
const sectionListRenderer = Util.getSingleObjectByKey(
data,
"sectionListRenderer",
);
const sectionListRendererContents = sectionListRenderer?.["contents"];
if (
sectionListRendererContents?.constructor === Array &&
sectionListRendererContents.length > 0
) {
for (let i = sectionListRendererContents.length - 1; i >= 0; i--) {
const itemSectionRendererContents =
sectionListRendererContents[i]?.["itemSectionRenderer"]?.[
"contents"
];
if (
itemSectionRendererContents?.constructor === Array &&
itemSectionRendererContents.length > 0
) {
for (
let j = itemSectionRendererContents.length - 1;
j >= 0;
j--
) {
const reelShelfRenderer =
itemSectionRendererContents[j]?.["reelShelfRenderer"];
if (reelShelfRenderer) {
itemSectionRendererContents.splice(j, 1);
}
}
if (itemSectionRendererContents.length === 0) {
sectionListRendererContents.splice(i, 1);
}
}
}
}
}
// video page sidebar shorts
if (!iridiumSettings.videoPageShorts && Util.isWatchPage()) {
const twoColumnWatchNextResults = Util.getSingleObjectByKey(
data,
"twoColumnWatchNextResults",
);
const itemContainer =
twoColumnWatchNextResults?.["secondaryResults"]?.[
"secondaryResults"
]?.["results"];
if (itemContainer?.constructor === Array && itemContainer.length > 0) {
for (let i = itemContainer.length - 1; i >= 0; i--) {
const itemRendererContents =
itemContainer[i]?.["itemSectionRenderer"]?.["contents"];
if (
itemRendererContents?.constructor === Array &&
itemRendererContents.length > 0
) {
for (let j = itemRendererContents.length - 1; j >= 0; j--) {
const reelShelfRenderer =
itemRendererContents[j]?.["reelShelfRenderer"];
if (reelShelfRenderer) {
itemRendererContents.splice(j, 1);
}
}
}
}
}
}
};
OnYtPageDataFetched.addListener(listener);
OverrideFetch.onFetchListener(listener);
OverrideHandleResponse.onHandleResponseListener(listener);
return {};
})();
const FeatureCreatorMerch = (() => {
const listener = (data) => {
if (!iridiumSettings.creatorMerch) {
const contents = Util.getSingleObjectByKey(
data,
"contents",
(matched, _) =>
matched?.find((item) =>
Object.hasOwn(item, "merchandiseShelfRenderer"),
),
);
if (contents?.length > 0) {
for (let i = contents.length - 1; i >= 0; i--) {
const item = contents[i];
if (Object.hasOwn(item, "merchandiseShelfRenderer")) {
contents.splice(i, 1);
}
}
}
}
};
OnYtPageDataFetched.addListener(listener);
OverrideFetch.onFetchListener(listener);
OverrideHandleResponse.onHandleResponseListener(listener);
return {};
})();
const FeatureVideoCount = (() => {
const onVideoCountData = (pageData, subCount, profileCounts) => {
if (!subCount?.["text"]?.["simpleText"]) {
return;
}
if (subCount["textChanged_"]) {
subCount["textChanged_"]?.({ runs: [{ text: profileCounts }] });
} else if (subCount.textContent === subCount["text"]["simpleText"]) {
subCount.textContent = profileCounts;
}
const parent = subCount.parentElement;
if (parent?.nodeName === "A") {
parent.title = subCount.textContent;
}
};
const onEvent = () => {
const subCount = document.getElementById("owner-sub-count");
if (!subCount) {
return;
}
const channelId =
document.getElementById("movie_player")?.["getPlayerResponse"]()?.[
"videoDetails"
]?.["channelId"] || "";
let url = null;
if (channelId.startsWith("UC")) {
url = `/channel/${channelId}`;
} else if (channelId.startsWith("/@")) {
url = channelId;
} else {
return;
}
if (subCount.parentElement.nodeName !== "A") {
const link = document.createElement("a");
link.href = `${url}/videos`;
link.className = "yt-simple-endpoint";
subCount.parentElement.appendChild(link);
link.appendChild(subCount);
}
fetch(url).then((response) =>
response.text().then((data) => {
let matched = data.match(/var ytInitialData = (\{.*?});/)?.[1];
if (matched) {
try {
const pageData = JSON.parse(matched);
const metadataRows =
pageData?.header?.["pageHeaderRenderer"]?.["content"]?.[
"pageHeaderViewModel"
]?.["metadata"]?.["contentMetadataViewModel"]?.["metadataRows"];
if (
metadataRows?.constructor === Array &&
metadataRows.length > 0
) {
for (let i = metadataRows.length - 1; i >= 0; i--) {
const metadataParts = metadataRows[i]?.["metadataParts"];
if (
metadataParts?.constructor === Array &&
metadataParts.length > 0
) {
onVideoCountData(
pageData,
subCount,
metadataParts
.map((entry) => entry?.text?.content)
?.join(" ‧ "),
);
break;
}
}
}
} catch (e) {}
}
}),
);
};
const update = () => {
if (iridiumSettings.videoCount) {
window.addEventListener("yt-page-data-updated", onEvent, true);
onEvent();
} else {
window.removeEventListener("yt-page-data-updated", onEvent, true);
const subCount = document.getElementById("owner-sub-count");
const link = subCount?.parentElement;
if (link?.nodeName === "A") {
link?.parentElement?.appendChild(subCount);
link?.remove();
}
if (subCount?.["textChanged_"] && subCount?.["text"]?.["simpleText"]) {
subCount["textChanged_"]?.({
runs: [{ text: subCount["text"]["simpleText"] }],
});
}
}
};
FeatureUpdater.register(SettingData.videoCount.id, update);
return {};
})();
const FeatureAmbientMode = (() => {
const created = (api) => {
api?.["updateCinematicSettings"]?.(iridiumSettings.ambientMode);
};
const update = () => {
if (iridiumSettings.ambientMode) {
document.documentElement.removeAttribute("ambient-mode-disabled");
} else {
document.documentElement.setAttribute("ambient-mode-disabled", "");
}
document
.getElementById("movie_player")
?.["updateCinematicSettings"]?.(iridiumSettings.ambientMode);
};
FeatureUpdater.register(SettingData.ambientMode.id, update);
OverrideOnPlayerReady.onReadyListener(created);
return {};
})();
const FeatureInfoCards = (() => {
const listener = (data) => {
if (!iridiumSettings.infoCards) {
const cards = Util.getSingleObjectAndParentByKey(
data,
"cards",
(cards, _) => !!cards?.["cardCollectionRenderer"],
);
if (cards?.parent?.["cards"]) {
delete cards.parent["cards"];
}
}
};
OnYtPageDataFetched.addListener(listener);
OverrideFetch.onFetchListener(listener);
OverrideHandleResponse.onHandleResponseListener(listener);
return {};
})();
const FeatureEndScreen = (() => {
const update = () => {
if (iridiumSettings.endScreen) {
document.documentElement.classList.add("iridium-hide-end-screen-cards");
} else {
document.documentElement.classList.remove(
"iridium-hide-end-screen-cards",
);
}
};
FeatureUpdater.register(SettingData.endScreen.id, update);
return {
update: update,
};
})();
const FeatureLoudness = (() => {
const listener = (data) => {
if (!iridiumSettings.loudness) {
const audioConfig = Util.getSingleObjectByKey(data, "audioConfig");
const adaptiveFormats = Util.getSingleObjectByKey(
data,
"adaptiveFormats",
);
if (audioConfig && Object.hasOwn(audioConfig, "loudnessDb")) {
audioConfig["loudnessDb"] = 0;
}
if (adaptiveFormats) {
if (
adaptiveFormats.constructor === Array &&
adaptiveFormats.length > 0
) {
for (let i = 0; i < adaptiveFormats.length; i++) {
if (adaptiveFormats[i]?.["loudnessDb"]) {
adaptiveFormats[i]["loudnessDb"] = 0;
}
}
}
}
}
};
OverrideFetch.onFetchListener(listener);
OverrideHandleResponse.onHandleResponseListener(listener);
return {};
})();
const FeatureHFRAllowed = (() => {
const listener = (data) => {
if (!iridiumSettings.hfrAllowed) {
const adaptiveFormats = Util.getSingleObjectByKey(
data,
"adaptiveFormats",
);
if (
adaptiveFormats?.constructor === Array &&
adaptiveFormats.length > 0
) {
for (let i = adaptiveFormats.length - 1; i >= 0; i--) {
if (adaptiveFormats[i]?.["fps"] > 30) {
adaptiveFormats.splice(i, 1);
}
}
}
}
};
OverrideFetch.onFetchListener(listener);
OverrideHandleResponse.onHandleResponseListener(listener);
return {};
})();
const FeatureScrollVolume = (() => {
const iniScrollVolume = (event) => {
if (iridiumSettings.scrollVolumeShift && !event.shiftKey) {
return;
}
const ytdApp = document.querySelector("ytd-app");
if (ytdApp?.["isWatchPage"] !== true) {
return;
}
const api = document.getElementById("movie_player");
if (!api) {
return;
}
if (!iridiumSettings.scrollVolumeShift) {
const playerState = api["getPlayerState"]?.() || -1;
if (
!api.contains(event.target) ||
!(playerState > 0) ||
!(playerState < 5)
) {
return;
}
const canScroll =
document
.querySelector(".ytp-playlist-menu")
?.contains(event.target) === true ||
document.querySelector(".iv-drawer")?.contains(event.target) ===
true ||
document
.querySelector(".ytp-settings-menu")
?.contains(event.target) === true;
if (canScroll) {
return;
}
}
event.preventDefault();
const direction = event.deltaY;
const oldVolume = api?.["getVolume"]?.() || 0;
const steps = Math.max(
1,
Math.min(iridiumSettings.scrollVolumeStep || 5, 10),
);
let newVolume = oldVolume - Math.sign(direction) * steps;
if (newVolume < 0) {
newVolume = 0;
} else if (newVolume > 100) {
newVolume = 100;
}
if (newVolume > oldVolume && api?.["isMuted"]?.() === true) {
api?.["unMute"]?.();
}
api?.["setVolume"]?.(newVolume, true);
let levelText = document.getElementById("iridium-scroll-volume-level");
if (!levelText) {
levelText = document.createElement("div");
levelText.id = "iridium-scroll-volume-level";
}
levelText.textContent = `${newVolume}%`;
let levelContainer = document.getElementById(
"iridium-scroll-volume-level-container",
);
if (!levelContainer) {
levelContainer = document.createElement("div");
levelContainer.id = "iridium-scroll-volume-level-container";
levelContainer.appendChild(levelText);
api.prepend(levelContainer);
} else {
levelContainer.style.display = "";
}
clearTimeout(levelContainer.timeoutId);
levelContainer.timeoutId = setTimeout(
() => (levelContainer.style.display = "none"),
1000,
);
};
const update = () => {
if (iridiumSettings.scrollVolume) {
document.addEventListener("wheel", iniScrollVolume, { passive: false });
} else {
document.removeEventListener("wheel", iniScrollVolume);
}
};
FeatureUpdater.register(SettingData.scrollVolume.id, update);
return {
update: update,
};
})();
const FeatureBlacklist = (() => {
const targetTags = [
"ytd-rich-item-renderer",
"ytd-video-renderer",
"ytd-grid-video-renderer",
"ytd-compact-video-renderer",
];
const richGridPages = ["/", "/podcasts"];
const sectionListPages = ["/results", "/feed/trending", "/gaming"];
const removeItems = (data) => {
if (!data) {
return;
}
if (richGridPages.indexOf(window.location.pathname) > -1) {
const richGridRenderer = Util.getSingleObjectByKey(
data,
"richGridRenderer",
);
const itemContainer = richGridRenderer?.["contents"];
if (itemContainer?.constructor === Array && itemContainer.length > 0) {
for (let i = itemContainer.length - 1; i >= 0; i--) {
const richShelfRendererContents =
itemContainer[i]?.["richSectionRenderer"]?.["content"]?.[
"richShelfRenderer"
]?.["contents"];
if (
richShelfRendererContents?.constructor === Array &&
richShelfRendererContents.length > 0
) {
for (let j = richShelfRendererContents.length - 1; j >= 0; j--) {
const browseId =
richShelfRendererContents[j]?.["richItemRenderer"]?.[
"content"
]?.["videoRenderer"]?.["shortBylineText"]?.["runs"]?.[0]?.[
"navigationEndpoint"
]?.["browseEndpoint"]?.["browseId"];
if (iridiumSettings.blacklist[browseId]) {
richShelfRendererContents.splice(j, 1);
}
}
if (richShelfRendererContents.length === 0) {
itemContainer.splice(i, 1);
}
}
const browseId =
itemContainer[i]?.["richItemRenderer"]?.["content"]?.[
"videoRenderer"
]?.["shortBylineText"]?.["runs"]?.[0]?.["navigationEndpoint"]?.[
"browseEndpoint"
]?.["browseId"];
if (iridiumSettings.blacklist[browseId]) {
itemContainer.splice(i, 1);
}
}
}
} else if (Util.isWatchPage()) {
const secondaryResults = Util.getSingleObjectByKey(
data,
"secondaryResults",
);
const itemContainer =
secondaryResults?.["secondaryResults"]?.["results"];
if (itemContainer?.constructor === Array && itemContainer.length > 0) {
for (let i = itemContainer.length - 1; i >= 0; i--) {
const itemSectionRendererContents =
itemContainer[i]?.["itemSectionRenderer"]?.["contents"];
if (
itemSectionRendererContents?.constructor === Array &&
itemSectionRendererContents.length > 0
) {
for (
let j = itemSectionRendererContents.length - 1;
j >= 0;
j--
) {
const browseId =
itemSectionRendererContents[j]?.["compactVideoRenderer"]?.[
"shortBylineText"
]?.["runs"]?.[0]?.["navigationEndpoint"]?.[
"browseEndpoint"
]?.["browseId"];
if (iridiumSettings.blacklist[browseId]) {
itemSectionRendererContents.splice(j, 1);
}
}
}
}
}
const playerOverlays = Util.getSingleObjectByKey(
data,
"playerOverlays",
);
const endScreenRenderer =
playerOverlays?.["playerOverlayRenderer"]?.["endScreen"]?.[
"watchNextEndScreenRenderer"
];
const endScreenRendererResults = endScreenRenderer?.["results"];
if (
endScreenRendererResults?.constructor === Array &&
endScreenRendererResults.length > 0
) {
for (let i = endScreenRendererResults.length - 1; i >= 0; i--) {
const browseId =
endScreenRendererResults[i]?.["endScreenVideoRenderer"]?.[
"shortBylineText"
]?.["runs"]?.[0]?.["navigationEndpoint"]?.["browseEndpoint"]?.[
"browseId"
];
if (iridiumSettings.blacklist[browseId]) {
endScreenRendererResults.splice(i, 1);
}
}
}
} else if (sectionListPages.indexOf(window.location.pathname) > -1) {
const sectionListRenderer = Util.getSingleObjectByKey(
data,
"sectionListRenderer",
);
const itemContainer = sectionListRenderer?.["contents"];
if (itemContainer?.constructor === Array && itemContainer.length > 0) {
for (let i = itemContainer.length - 1; i >= 0; i--) {
const items =
itemContainer[i]?.["itemSectionRenderer"]?.["contents"] ||
itemContainer[i]?.["shelfRenderer"]?.["content"]?.[
"verticalListRenderer"
]?.["items"];
if (items?.constructor === Array && items.length > 0) {
for (let j = items.length - 1; j >= 0; j--) {
const browseId =
items[j]?.["channelRenderer"]?.["channelId"] ||
items[j]?.["videoRenderer"]?.["ownerText"]?.["runs"]?.[0]?.[
"navigationEndpoint"
]?.["browseEndpoint"]?.["browseId"];
if (iridiumSettings.blacklist[browseId]) {
items.splice(j, 1);
}
const shelfRenderer = items[j]?.["shelfRenderer"];
const shelfRendererId =
shelfRenderer?.["endpoint"]?.["browseEndpoint"]?.["browseId"];
if (iridiumSettings.blacklist[shelfRendererId]) {
items.splice(j, 1);
} else {
const shelfRendererContent = shelfRenderer?.["content"];
const shelfItems =
shelfRendererContent?.["verticalListRenderer"]?.["items"] ||
shelfRendererContent?.["horizontalListRenderer"]?.[
"items"
] ||
shelfRendererContent?.["expandedShelfContentsRenderer"]?.[
"items"
];
if (
shelfItems?.constructor === Array &&
shelfItems.length > 0
) {
for (let k = shelfItems.length - 1; k >= 0; k--) {
const shelfItemId =
shelfItems[k]?.["videoRenderer"]?.["ownerText"]?.[
"runs"
]?.[0]?.["navigationEndpoint"]?.["browseEndpoint"]?.[
"browseId"
];
if (iridiumSettings.blacklist[shelfItemId]) {
shelfItems.splice(k, 1);
}
}
if (shelfItems.length === 0) {
items.splice(j, 1);
}
}
}
}
if (items.length === 0) {
itemContainer.splice(i, 1);
}
}
}
}
}
};
const update = (data) => {
if (!iridiumSettings.blacklistEnabled) {
return;
}
if (data?.constructor !== Object || !data?.["responseContext"]) {
return;
}
removeItems(data);
const items = Array.from(document.querySelectorAll(targetTags.join(",")));
items.forEach((item) => {
const renderer =
item?.["data"]?.["content"]?.["videoRenderer"] || item?.["data"];
const browseId =
renderer?.["shortBylineText"]?.["runs"]?.[0]?.[
"navigationEndpoint"
]?.["browseEndpoint"]?.["browseId"];
if (iridiumSettings.blacklist[browseId]) {
item.remove();
}
});
document.querySelector("ytd-rich-grid-renderer")?.["reflowContent"]?.();
document.querySelector("ytd-video-preview")?.["deactivate"]?.();
};
FeatureUpdater.register(SettingData.blacklist.id, () => {
update(document.querySelector("ytd-app")?.["data"]?.["response"]);
});
OnYtPageDataFetched.addListener(update);
return {
targetTags: targetTags,
update: update,
allowedRichGridPages: richGridPages,
allowedSectionListPages: sectionListPages,
};
})();
const FeatureBlacklistButton = (() => {
const update = () => {
document.querySelector("tp-yt-iron-dropdown")?.["close"]?.();
if (!iridiumSettings.blacklistButton) {
const menus = Array.from(
document.querySelectorAll("ytd-menu-renderer"),
);
menus.forEach((menu) => {
const items = menu?.["data"]?.["items"];
const backupItems = menu?.["__data"]?.["items"];
if (items?.constructor === Array) {
for (let i = items.length - 1; i >= 0; i--) {
if (
items[i]?.["menuServiceItemRenderer"]?.["id"] === "blockChannel"
) {
items.splice(i, 1);
}
}
}
if (backupItems?.constructor === Array) {
for (let i = backupItems.length - 1; i >= 0; i--) {
if (
backupItems[i]?.["menuServiceItemRenderer"]?.["id"] ===
"blockChannel"
) {
backupItems.splice(i, 1);
}
}
}
});
}
};
const onItemSelectedAction = (event) => {
const channelData = event.detail?.["args"]?.[0]?.["channelData"];
const channelUC = channelData?.["channelUC"];
if (!channelUC) {
return;
}
const channelName = channelData?.["channelName"];
const canonicalBaseUrl = channelData?.["canonicalBaseUrl"];
if (!channelName && !canonicalBaseUrl) {
return;
}
if (!iridiumSettings.blacklist[channelUC]) {
iridiumSettings.blacklist[channelUC] = {
name: channelName,
handle: canonicalBaseUrl,
};
Broadcaster.saveSetting(SettingData.blacklist.id);
}
FeatureBlacklist.update(
document.querySelector("ytd-app")?.["data"]?.["response"],
);
};
const iniBlacklistButton = (event) => {
if (
!iridiumSettings.blacklistEnabled ||
!iridiumSettings.blacklistButton
) {
return;
}
if (
window.location.pathname !== "/" &&
!Util.isWatchPage() &&
FeatureBlacklist.allowedRichGridPages.indexOf(
window.location.pathname,
) < 0 &&
FeatureBlacklist.allowedSectionListPages.indexOf(
window.location.pathname,
) < 0
) {
return;
}
if (
event.detail?.["actionName"] === "yt-menu-service-item-selected-action"
) {
onItemSelectedAction(event);
}
};
const checkBlacklistButton = (data) => {
const hostElement = data?.["hostElement"];
const items = data?.["items"];
if (!hostElement || items?.constructor !== Array) {
return;
}
const parent = Array.from(
document.querySelectorAll(FeatureBlacklist.targetTags.join(",")),
).find((item) => item.contains(hostElement));
if (!parent) {
for (let i = items.length - 1; i >= 0; i--) {
if (
items[i]?.["menuServiceItemRenderer"]?.["id"] === "blockChannel"
) {
items.splice(i, 1);
}
}
} else if (
!items.some(
(item) =>
item?.["menuServiceItemRenderer"]?.["id"] === "blockChannel",
)
) {
const renderer =
parent?.["data"]?.["content"]?.["videoRenderer"] || parent?.["data"];
const channelUC =
renderer?.["shortBylineText"]?.["runs"]?.[0]?.[
"navigationEndpoint"
]?.["browseEndpoint"]?.["browseId"];
if (!channelUC) {
return;
}
const channelName = parent.querySelector(
"yt-formatted-string.ytd-channel-name",
)?.title;
const canonicalBaseUrl = Util.getSingleObjectByKey(
parent.data,
"canonicalBaseUrl",
);
if (!channelName && !canonicalBaseUrl) {
return;
}
items.unshift({
menuServiceItemRenderer: {
id: "blockChannel",
channelData: {
channelUC: channelUC,
channelName: channelName,
canonicalBaseUrl: canonicalBaseUrl,
},
hasSeparator: true,
text: { runs: [{ text: "Block channel" }] },
icon: { iconType: "CANCEL" },
},
});
}
};
const onOverflowTapKey = crypto.randomUUID();
Object.defineProperty(Object.prototype, "onOverflowTap", {
set(data) {
this[onOverflowTapKey] = data;
},
get() {
if (
iridiumSettings.blacklistEnabled &&
iridiumSettings.blacklistButton
) {
checkBlacklistButton(this);
}
return this[onOverflowTapKey]?.apply(this, arguments);
},
});
FeatureUpdater.register(SettingData.blacklistButton.id, update);
document.documentElement.addEventListener(
"yt-action",
iniBlacklistButton,
false,
);
return {
update: update,
};
})();
// end features
}
// END: js/background-inject.js
// START: js/content-script.js
const channel = new BroadcastChannel(browser.runtime.id);
let port = null;
const validContext = () => {
const isValid = !!browser.runtime?.id;
if (!isValid) {
channel.removeEventListener("message", channelListener);
channel.close();
}
return isValid;
};
const channelListener = message => {
if (!validContext()) {
return;
}
if (message?.data?.type === "setting") {
saveSettings(message?.data?.payload).then();
} else if (message?.data?.type === "action") {
portMessage(message?.data);
}
};
const portMessage = data => {
if (!data) {
return;
}
if (!validContext()) {
return;
}
if (!port) {
port = browser.runtime.connect(browser.runtime.id);
port?.onDisconnect?.addListener(() => port = null);
}
port?.postMessage(data);
};
const saveSettings = async data => {
if (!data) {
return;
}
if (!validContext()) {
return;
}
const dataSync = await browser.storage.sync.get();
if (dataSync?.[SettingData.syncSettings.id] === true) {
await browser.storage.sync.set(data);
} else {
await browser.storage.local.set(data);
}
};
const onStorageChanged = changes => {
if (!validContext()) {
return;
}
const changeData = {};
for (let key in changes) {
const change = changes[key];
if (change.newValue !== change.oldValue) {
changeData[key] = change.newValue;
}
}
if (Object.keys(changeData).length > 0) {
channel.postMessage(changeData);
// check if user switched storage areas
if (Object.hasOwn(changeData, SettingData.syncSettings.id)) {
if (changeData[SettingData.syncSettings.id] === true) {
browser.storage.local.onChanged.removeListener(onStorageChanged);
browser.storage.sync.onChanged.addListener(onStorageChanged);
} else {
browser.storage.sync.onChanged.removeListener(onStorageChanged);
browser.storage.local.onChanged.addListener(onStorageChanged);
}
}
}
};
const checkNewFeatures = async settings => {
const newFeatures = {};
for (let key in SettingData) {
if (!Object.hasOwn(settings, key)) {
settings[key] = newFeatures[key] = SettingData[key].default;
}
}
if (Object.keys(newFeatures).length > 0) {
if (settings[SettingData.syncSettings.id] === true) {
await browser.storage.sync.set(newFeatures);
} else {
await browser.storage.local.set(newFeatures);
}
}
return settings;
};
const getSettings = async () => {
const dataSync = await browser.storage.sync.get();
if (dataSync?.[SettingData.syncSettings.id] === true) {
return await checkNewFeatures(dataSync);
}
const dataLocal = await browser.storage.local.get();
// storage will be empty during first installation
// this ensures the first load stores the default settings
if (Object.keys(dataLocal).length === 0) {
const defaultSettings = getDefaultSettings();
await browser.storage.local.set(defaultSettings);
return defaultSettings;
}
return await checkNewFeatures(dataLocal);
};
channel.addEventListener("message", channelListener);
browser.storage.sync.get().then(dataSync => {
if (dataSync?.[SettingData.syncSettings.id] === true) {
browser.storage.sync.onChanged.addListener(onStorageChanged);
} else {
browser.storage.local.onChanged.addListener(onStorageChanged);
}
});
getSettings().then(settings => channel.postMessage(settings));
if (!document.getElementById("iridium-inject")) {
const script = document.createElement("script");
script.id = "iridium-inject";
script.textContent = `(${mainScript}("${browser.runtime.id}",${JSON.stringify(SettingData)},${JSON.stringify(getDefaultSettings())}))`;
document.documentElement.appendChild(script);
}
// END: js/content-script.js
;}
} 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 scriptPaths = [];
_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){;
;}
} 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 \"iridium-for-youtube\",\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