.dotfiles/.config/chromium/Default/Extensions/cimiefiiaegbelhefglklhhakcgmhkai/2.1_0/page-script.js
2026-01-18 21:08:16 -07:00

296 lines
13 KiB
JavaScript

(function() {
let purposeTransferObject = null;
let mprisTransferObject = null;
let eventCallback = function(e) {
e.stopPropagation();
const args = e.detail;
if (args.action == "unload") {
// TODO: Undo other operations
window.removeEventListener("org.kde.pbi.event", eventCallback, {"capture": true});
} else if (args.action == "mediaSessionsRegister") {
const MediaSessionsClassName_constructor = function() {
this.callbacks = {};
this.pendingCallbacksUpdate = 0;
this.metadata = null;
this.playbackState = "none";
this.sendMessage = function(action, payload) {
let event = new CustomEvent("org.kde.pbi.mpris.message", {
detail: {
action: action,
payload: payload
}
});
window.dispatchEvent(event);
};
this.executeCallback = function(action) {
let details = {
action: action
// for seekforward, seekbackward, seekto there's additional information one would need to add
};
this.callbacks[action](details);
};
this.setCallback = function(name, cb) {
const oldCallbacks = Object.keys(this.callbacks).sort();
if (cb) {
this.callbacks[name] = cb;
} else {
delete this.callbacks[name];
}
const newCallbacks = Object.keys(this.callbacks).sort();
if (oldCallbacks.toString() === newCallbacks.toString()) {
return;
}
if (this.pendingCallbacksUpdate) {
return;
}
this.pendingCallbacksUpdate = setTimeout(() => {
this.pendingCallbacksUpdate = 0;
// Make sure to send the current callbacks, not "newCallbacks" at the time of starting the timeout
const callbacks = Object.keys(this.callbacks);
this.sendMessage("callbacks", callbacks);
}, 0);
};
this.setMetadata = function(metadata) {
// MediaMetadata is not a regular Object so we cannot just JSON.stringify it
let newMetadata = {};
let dirty = (!metadata != !this.metadata);
if (metadata) {
const keys = ["title", "artist", "album", "artwork"];
const oldMetadata = this.metadata || {};
keys.forEach((key) => {
const value = metadata[key];
if (typeof value?.toString !== "function") {
return; // continue
}
// We only have Strings or the "artwork" Array, so a toString() comparison should suffice...
dirty |= (value.toString() !== (oldMetadata[key] || "").toString());
newMetadata[key] = value;
});
}
this.metadata = metadata;
if (dirty) {
this.sendMessage("metadata", newMetadata);
}
};
this.setPlaybackState = function(playbackState) {
if (this.playbackState === playbackState) {
return;
}
this.playbackState = playbackState;
this.sendMessage("playbackState", playbackState);
};
};
mprisTransferObject = new MediaSessionsClassName_constructor();
if (!navigator.mediaSession) {
navigator.mediaSession = {};
}
var noop = function() {};
var oldSetActionHandler = navigator.mediaSession.setActionHandler || noop;
navigator.mediaSession.setActionHandler = function(name, cb) {
mprisTransferObject.setCallback(name, cb);
// Call the original native implementation
// "call()" is needed as the real setActionHandler is a class member
// and calling it directly is illegal as it lacks the context
// This may throw for unsupported actions but we registered the callback
// ourselves before
return oldSetActionHandler.call(navigator.mediaSession, name, cb);
};
Object.defineProperty(navigator.mediaSession, "metadata", {
get: () => mprisTransferObject.metadata,
set: (newValue) => {
mprisTransferObject.setMetadata(newValue);
}
});
Object.defineProperty(navigator.mediaSession, "playbackState", {
get: () => mprisTransferObject.playbackState,
set: (newValue) => {
mprisTransferObject.setPlaybackState(newValue);
}
});
if (!window.MediaMetadata) {
window.MediaMetadata = function(data) {
Object.assign(this, data);
};
window.MediaMetadata.prototype.title = "";
window.MediaMetadata.prototype.artist = "";
window.MediaMetadata.prototype.album = "";
window.MediaMetadata.prototype.artwork = [];
}
// here we replace the document.createElement function with our own so we can detect
// when an <audio> tag is created that is not added to the DOM which most pages do
// while a <video> tag typically ends up being displayed to the user, audio is not.
// HACK We cannot really pass variables from the page's scope to our content-script's scope
// so we just blatantly insert the <audio> tag in the DOM and pick it up through our regular
// mechanism. Let's see how this goes :D
// HACK When removing a media object from DOM it is paused, so what we do here is once the
// player loaded some data we add it (doesn't work earlier since it cannot pause when
// there's nothing loaded to pause) to the DOM and before we remove it, we note down that
// we will now get a paused event because of that. When we get it, we just play() the player
// so it continues playing :-)
let addPlayerToDomEvadingAutoPlayBlocking = function(player) {
player.registerInDom = () => {
// Needs to be dataset so it's accessible from mutation observer on webpage
player.dataset.pbiPausedForDomRemoval = "true";
player.removeEventListener("play", player.registerInDom);
// If it is already in DOM by the time it starts playing, we don't need to do anything
// Also, if the page already parented it around, don't mess with it
if (document.documentElement.contains(player)
|| player.parentNode) {
delete player.dataset.pbiPausedForDomRemoval;
player.removeEventListener("pause", player.replayAfterRemoval);
} else {
(document.head || document.documentElement).appendChild(player);
player.parentNode.removeChild(player);
}
};
player.replayAfterRemoval = () => {
if (player.dataset.pbiPausedForDomRemoval === "true") {
delete player.dataset.pbiPausedForDomRemoval;
player.removeEventListener("pause", player.replyAfterRemoval);
player.play();
}
};
player.addEventListener("play", player.registerInDom);
player.addEventListener("pause", player.replayAfterRemoval);
}
const oldCreateElement = Document.prototype.createElement;
Document.prototype.createElement = function() {
const createdTag = oldCreateElement.apply(this, arguments);
const tagName = arguments[0];
if (typeof tagName === "string") {
if (tagName.toLowerCase() === "audio" || tagName.toLowerCase() === "video") {
const player = createdTag;
addPlayerToDomEvadingAutoPlayBlocking(player);
}
}
return createdTag;
};
// We also briefly add items created as new Audio() to the DOM so we can control it
// similar to the document.createElement hack above since we cannot share variables
// between the actual website and the background script despite them sharing the same DOM
var oldAudio = window.Audio;
window.Audio = function(...args) {
const player = new oldAudio(...args);
addPlayerToDomEvadingAutoPlayBlocking(player);
return player;
};
} else if (args.action == "mpris") {
try {
mprisTransferObject.executeCallback(args.mprisCallbackName);
} catch (e) {
console.warn("Exception executing '" + args.mprisCallbackName + "' media sessions callback", e);
}
} else if (args.action == "purposeRegister") {
purposeTransferObject = function() {};
purposeTransferObject.reset = () => {
purposeTransferObject.pendingResolve = null;
purposeTransferObject.pendingReject = null;
};
purposeTransferObject.reset();
if (!navigator.canShare) {
navigator.canShare = (data) => {
if (!data) {
return false;
}
if (data.title === undefined && data.text === undefined && data.url === undefined) {
return false;
}
if (data.url) {
// check if URL is valid
try {
new URL(data.url, document.location.href);
} catch (e) {
return false;
}
}
return true;
}
}
if (!navigator.share) {
navigator.share = (data) => {
return new Promise((resolve, reject) => {
if (!navigator.canShare(data)) {
return reject(new TypeError());
}
if (data.url) {
// validity already checked in canShare, hence no catch
data.url = new URL(data.url, document.location.href).toString();
}
if (!window.event || !window.event.isTrusted) {
return reject(new DOMException("navigator.share can only be called in response to user interaction", "NotAllowedError"));
}
if (purposeTransferObject.pendingResolve || purposeTransferObject.pendingReject) {
return reject(new DOMException("A share is already in progress", "AbortError"));
}
purposeTransferObject.pendingResolve = resolve;
purposeTransferObject.pendingReject = reject;
const event = new CustomEvent("org.kde.pbi.purpose.message", {
detail: {
action: "share",
payload: data
}
});
window.dispatchEvent(event);
});
};
}
} else if (args.action == "purposeShare") {
purposeTransferObject.pendingResolve();
} else if (args.action == "purposeReject") {
purposeTransferObject.pendingReject(new DOMException("Share request aborted", "AbortError"));
} else if (args.action == "purposeReset") {
purposeTransferObject.reset();
} else {
console.warn("Unknown page script action" + args.action, args);
}
};
window.addEventListener("org.kde.pbi.event", eventCallback, {"capture": true});
window.dispatchEvent(new CustomEvent("org.kde.pbi.inited"));
}());