Spoofs page visibility and focus state to keep pages always active.
// ==UserScript==
// @name Always Focused
// @namespace groknt
// @version 8.5.1
// @description Spoofs page visibility and focus state to keep pages always active.
// @author groknt
// @license MIT
// @match *://*/*
// @match file:///*
// @inject-into auto
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
"use strict";
const BLOCKED = new Set([
"blur",
"visibilitychange",
"webkitvisibilitychange",
"mozvisibilitychange",
"focusin",
"focusout",
"pagehide",
"mouseleave",
"freeze",
]);
const LIMITED = new Set(["focus", "pageshow", "mouseenter", "resume"]);
const VIS_PROPS = [
["hidden", false],
["visibilityState", "visible"],
["webkitHidden", false],
["webkitVisibilityState", "visible"],
["mozHidden", false],
["mozVisibilityState", "visible"],
];
function coreSetup(env) {
const blocked = env.blocked;
const limited = env.limited;
const realVisGet = env.desc(env.proto, "visibilityState")?.get;
const realHidGet = env.desc(env.proto, "hidden")?.get;
const isHidden = () =>
realHidGet
? realHidGet.call(env.doc)
: realVisGet?.call(env.doc) === "hidden";
let active = !isHidden();
const docElement = env.doc.documentElement;
const intercept = env.wrap(function (e) {
const target = e.target;
if (!target) return;
const isWindow = target.window === target;
const isDocument = target.nodeType === 9 || target === docElement;
const isFocusTarget =
(e.type === "focusin" || e.type === "focusout") &&
e.relatedTarget === null;
if (!isWindow && !isDocument && !isFocusTarget) return;
if (blocked.has(e.type)) {
env.stop(e);
} else if (limited.has(e.type)) {
if (!isWindow && !isDocument) return;
if (active) env.stop(e);
else active = true;
}
});
for (const evt of env.blocked) {
env.on(env.win, evt, intercept, true);
env.on(env.doc, evt, intercept, true);
}
for (const evt of env.limited) {
env.on(env.win, evt, intercept, true);
}
const onEventProps = [
["onblur", blocked],
["onpagehide", blocked],
["onfocus", limited],
["onpageshow", limited],
];
for (const [prop, eventSet] of onEventProps) {
const descriptor = env.desc(env.win, prop);
if (descriptor?.set) {
let stored = null;
const origSetter = descriptor.set;
env.def(env.win, prop, {
get: env.wrap(() => stored),
set: env.wrap(function (handler) {
stored = handler;
const wrapped = handler
? env.wrap(function (e) {
if (eventSet === blocked) return;
if (active) return;
active = true;
return handler.call(env.win, e);
})
: null;
origSetter.call(env.win, wrapped);
}),
configurable: true,
enumerable: true,
});
}
}
for (const [prop, value] of env.visProps) {
if (prop in env.proto) {
const origGet = env.desc(env.proto, prop)?.get;
env.def(
env.proto,
prop,
env.getter(function () {
if (origGet) origGet.call(this);
return value;
}, `get ${prop}`),
);
}
}
const origHasFocus = env.proto.hasFocus;
env.proto.hasFocus = env.func(function hasFocus() {
if (origHasFocus) origHasFocus.call(this);
return true;
}, "hasFocus");
if (env.NativeIO) {
const entryTrap = env.wrap(function (entry, prop) {
switch (prop) {
case "isIntersecting":
case "isVisible":
return true;
case "intersectionRatio":
return 1.0;
case "intersectionRect":
return env.reflect(entry, "boundingClientRect");
default:
return env.reflect(entry, prop);
}
});
env.replaceIO(function (callback, options) {
const wrapped = env.wrap(function (entries, observer) {
if (!isHidden()) return callback(entries, observer);
const patched = env.arr();
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (
entry.target.offsetWidth === 0 &&
entry.target.offsetHeight === 0
) {
patched.push(entry);
continue;
}
const handler = env.obj();
handler.get = entryTrap;
patched.push(env.proxy(entry, handler));
}
return callback(patched, observer);
});
return new env.NativeIO(wrapped, options);
});
}
}
function hasXray() {
try {
const page = window.wrappedJSObject;
if (!page || page === window) return false;
if (typeof exportFunction !== "function") return false;
const fn = exportFunction(() => true, page);
page.__xtest = fn;
const success = page.__xtest() === true;
delete page.__xtest;
return success;
} catch {
return false;
}
}
if (hasXray()) {
let activeState = !document.hidden;
const docElement = document.documentElement;
const contentIntercept = (e) => {
const target = e.target;
if (!target) return;
const isWindow = target.window === target;
const isDocument = target.nodeType === 9 || target === docElement;
const isFocusNode =
(e.type === "focusin" || e.type === "focusout") &&
e.relatedTarget === null;
if (BLOCKED.has(e.type)) {
if (isWindow || isDocument || isFocusNode) e.stopImmediatePropagation();
} else if (LIMITED.has(e.type)) {
if (isWindow || isDocument) {
if (activeState) e.stopImmediatePropagation();
else activeState = true;
}
}
};
for (const evt of BLOCKED) {
window.addEventListener(evt, contentIntercept, true);
document.addEventListener(evt, contentIntercept, true);
}
for (const evt of LIMITED) {
window.addEventListener(evt, contentIntercept, true);
}
xraySetup();
} else {
pageSetup();
}
function xraySetup() {
const page = window.wrappedJSObject;
const exportFn = (fn) => exportFunction(fn, page);
const pageObj = () => new page.Object();
const NativeIO = page.IntersectionObserver;
coreSetup({
blocked: BLOCKED,
limited: LIMITED,
visProps: VIS_PROPS,
win: page,
doc: page.document,
proto: page.Document.prototype,
wrap: exportFn,
stop: (e) => e.stopImmediatePropagation(),
on: (target, evt, fn, capture) =>
target.addEventListener(evt, fn, capture),
desc: (obj, prop) => page.Object.getOwnPropertyDescriptor(obj, prop),
def: (obj, prop, descriptor) =>
page.Object.defineProperty(obj, prop, descriptor),
getter: (fn) => {
const descriptor = pageObj();
descriptor.get = exportFn(fn);
descriptor.configurable = true;
descriptor.enumerable = true;
return descriptor;
},
func: (fn) => exportFn(fn),
NativeIO: NativeIO || null,
reflect: (target, prop) => page.Reflect.get(target, prop),
arr: () => new page.Array(),
obj: pageObj,
proxy: (target, handler) => new page.Proxy(target, handler),
replaceIO(constructFn) {
page.IntersectionObserver = exportFn(function IntersectionObserver(
callback,
...opts
) {
return constructFn(callback, opts[0]);
});
page.IntersectionObserver.prototype = NativeIO.prototype;
const descriptor = pageObj();
descriptor.value = page.IntersectionObserver;
descriptor.writable = true;
descriptor.configurable = true;
page.Object.defineProperty(
page.IntersectionObserver.prototype,
"constructor",
descriptor,
);
},
});
}
function pageSetup() {
const nativeStrings = new Map();
const patchedRealms = new WeakSet();
const nativeListen = EventTarget.prototype.addEventListener;
const nativeStop = Event.prototype.stopImmediatePropagation;
const nativeAppendChild = Node.prototype.appendChild;
const nativeInsertBefore = Node.prototype.insertBefore;
const nativeReplaceChild = Node.prototype.replaceChild;
const nativeAppend = Element.prototype.append;
const nativePrepend = Element.prototype.prepend;
const origToString = Function.prototype.toString;
const nativeTemplate = origToString.call(document.getElementById);
const toStringHandler = {
apply(target, thisArg, args) {
return nativeStrings.has(thisArg)
? nativeStrings.get(thisArg)
: Reflect.apply(target, thisArg, args);
},
};
const toStringProxy = new Proxy(origToString, toStringHandler);
Function.prototype.toString = toStringProxy;
nativeStrings.set(
toStringProxy,
nativeTemplate.replace("getElementById", "toString"),
);
const markNative = (fn, name) => {
nativeStrings.set(fn, nativeTemplate.replace("getElementById", name));
return fn;
};
const patchRealm = (win) => {
if (!win || patchedRealms.has(win)) return;
patchedRealms.add(win);
try {
const realToString = win.Function.prototype.toString;
const realmProxy = new Proxy(realToString, toStringHandler);
win.Function.prototype.toString = realmProxy;
nativeStrings.set(
realmProxy,
nativeTemplate.replace("getElementById", "toString"),
);
} catch {}
};
const patchIframesIn = (node) => {
try {
if (node instanceof HTMLIFrameElement) void node.contentWindow;
if (node.querySelectorAll) {
for (const el of node.querySelectorAll("iframe"))
void el.contentWindow;
}
} catch {}
};
const contentWindowDesc = Object.getOwnPropertyDescriptor(
HTMLIFrameElement.prototype,
"contentWindow",
);
const contentDocumentDesc = Object.getOwnPropertyDescriptor(
HTMLIFrameElement.prototype,
"contentDocument",
);
if (contentWindowDesc?.get) {
const orig = contentWindowDesc.get;
Object.defineProperty(HTMLIFrameElement.prototype, "contentWindow", {
get: markNative(function () {
const contentWin = orig.call(this);
patchRealm(contentWin);
return contentWin;
}, "get contentWindow"),
configurable: true,
enumerable: true,
});
}
if (contentDocumentDesc?.get) {
const orig = contentDocumentDesc.get;
Object.defineProperty(HTMLIFrameElement.prototype, "contentDocument", {
get: markNative(function () {
const contentDoc = orig.call(this);
if (contentDoc?.defaultView) patchRealm(contentDoc.defaultView);
return contentDoc;
}, "get contentDocument"),
configurable: true,
enumerable: true,
});
}
const wrapInsertion = (proto, method, original) => {
proto[method] = markNative(function (...args) {
const result = original.apply(this, args);
for (const arg of args) {
if (arg instanceof Node) patchIframesIn(arg);
}
return result;
}, method);
};
wrapInsertion(Node.prototype, "appendChild", nativeAppendChild);
wrapInsertion(Node.prototype, "insertBefore", nativeInsertBefore);
wrapInsertion(Node.prototype, "replaceChild", nativeReplaceChild);
wrapInsertion(Element.prototype, "append", nativeAppend);
wrapInsertion(Element.prototype, "prepend", nativePrepend);
const NativeIO = window.IntersectionObserver;
coreSetup({
blocked: BLOCKED,
limited: LIMITED,
visProps: VIS_PROPS,
win: window,
doc: document,
proto: Document.prototype,
wrap: (fn) => fn,
stop: (e) => nativeStop.call(e),
on: (target, evt, fn, capture) =>
nativeListen.call(target, evt, fn, capture),
desc: Object.getOwnPropertyDescriptor,
def: Object.defineProperty,
getter: (fn, name) => ({
get: markNative(fn, name),
configurable: true,
enumerable: true,
}),
func: (fn, name) => markNative(fn, name),
NativeIO: NativeIO || null,
reflect: Reflect.get,
arr: () => [],
obj: () => ({}),
proxy: (target, handler) => new Proxy(target, handler),
replaceIO(constructFn) {
window.IntersectionObserver = new Proxy(NativeIO, {
construct(target, args) {
return constructFn(args[0], args[1]);
},
});
markNative(window.IntersectionObserver, "IntersectionObserver");
Object.defineProperty(
window.IntersectionObserver.prototype,
"constructor",
{
value: window.IntersectionObserver,
writable: true,
configurable: true,
},
);
},
});
}
})();