// ==UserScript==
// @name Netflix Marathon (Pausable)
// @name:zh-CN 网飞马拉松赛(可暫停)
// @name:zh-TW 网飞马拉松赛(可暫停)
// @name:ja Netflix Marathon(一時停止できます)
// @name:ar ماراثون Netflix (يمكن إيقافه مؤقتًا)
// @namespace https://github.com/aminomancer
// @version 4.4.3
// @description A configurable script that automatically skips recaps, intros, credits, and ads, and clicks "next episode" prompts on Netflix and Amazon Prime Video. Customizable hotkey to pause/resume the auto-skipping functionality. Alt + N for settings.
// @description:zh-CN 一个可配置的脚本,该脚本自动跳过介绍,信用和广告,并单击Netflix和Amazon Prime Video上的“下一个节目”提示。包括一个可自定义的热键,以暂停/恢复自动跳过功能。按Alt + N进行配置。
// @description:zh-TW 一个可配置的脚本,该脚本自动跳过介绍,信用和广告,并单击Netflix和Amazon Prime Video上的“下一个节目”提示。包括一个可自定义的热键,以暂停/恢复自动跳过功能。按Alt + N进行配置。
// @description:ja イントロ、クレジット、広告を自動的にスキップし、NetflixとAmazon PrimeVideoの「次のエピソード」プロンプトをクリックする構成可能なスクリプト。自動スキップ機能を一時停止/再開するためのカスタマイズ可能なホットキーが含まれています。Alt + Nを押して構成します。
// @description:ar برنامج نصي قابل للتكوين يتخطى تلقائيًا المقدمات والاعتمادات والإعلانات وينقر على "الحلقة التالية" على Netflix و Amazon Prime Video.يتضمن مفتاح اختصار قابل للتخصيص لإيقاف / استئناف وظيفة التخطي التلقائي.اضغط على Alt + N للتكوين.
// @author aminomancer
// @homepageURL https://github.com/aminomancer/Netflix-Marathon-Pausable
// @supportURL https://github.com/aminomancer/Netflix-Marathon-Pausable/issues
// @icon data:image/svg+xml;utf8,
// @include https://www.netflix.com/*
// @include https://*.amazon.com/*
// @include https://*.primevideo.com/*
// @require http://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js
// @require https://greasyfork.org/scripts/420683-gm-config-sizzle/code/GM_config_sizzle.js?version=894369
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_openInTab
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.deleteValue
// @grant GM.listValues
// @grant GM.openInTab
// @downloadURL none
// ==/UserScript==
const options = {}, // where settings are stored during runtime
GMObj = typeof GM === "object" && GM !== null && typeof GM.getValue === "function", // check whether the GM object exists so we can use the right GM API functions
GM4 = GMObj && GM.info.scriptHandler === "Greasemonkey" && GM.info.version.split(".")[0] >= 4, // check if the script handler is GM4, since if it is, we can't add a menu command
site = test("netflix") ? "netflix" : "amazon",
locale = {
// some basic localization for the settings menu.
get lang() {
delete this.lang;
return (this.lang = navigator.language.split("-")[0]);
},
get text() {
switch (this.lang) {
case "zh":
return "信息";
case "ja":
return "助けて";
case "ar":
return "تعليمات";
case "en":
default:
return "Support";
}
},
get title() {
switch (this.lang) {
case "zh":
return "设置的信息和翻译";
case "ja":
return "設定の情報と翻訳";
case "ar":
return "معلومات وترجمات للإعدادات";
case "en":
default:
return "Info and translations for the settings";
}
},
};
let marathon = {
count: 0,
results: null,
nDrain: "[data-uia='next-episode-seamless-button-draining']",
nReady: "[data-uia='next-episode-seamless-button']",
/**
* getElementsByClassName
* @param {string} s (class name)
*/
$c(s) {
return document.getElementsByClassName(s);
},
/**
* getElementsByTagName
* @param {string} s (tag name)
*/
$t(s) {
return document.getElementsByTagName(s);
},
/**
* getElementById
* @param {string} s (element id)
*/
$i(s) {
return document.getElementById(s);
},
/**
* querySelector
* @param {string} s (CSS selector e.g. ".class")
*/
$q(s) {
return document.querySelector(s);
},
/**
* querySelectorAll
* @param {string} s (CSS selector)
*/
$qa(s) {
return document.querySelectorAll(s);
},
/**
* document.evaluate
* @param {string} s (node's text content)
* @param {string} n (node's tag name. if not passed, then accept any tag)
* @param {string} p (node's parent's tag name. this is like saying button>div. if not passed, then just use div, ignoring the node's parent)
*/
$ev(s, n = "*", p) {
let exp = p ? `//${p}/child::${n}[text()="${s}"]` : `//${n}[text()="${s}"]`;
return document.evaluate(
exp,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
this.results
).singleNodeValue;
},
/**
* :contains
* @param {string} s (node's text content)
*/
$cnt(s) {
return $(`div *:contains('${s}')`);
},
/**
* click every element with the given text content
* @param {string} s (node's text content)
*/
$click(s) {
let divs = this.$cnt(s);
for (let i = 0; i < divs.length; i++) {
if (divs[i].innerText == s) {
divs[i].click();
this.count = 5;
}
}
},
/**
* find react property
* @param {object} d (DOM node)
*/
findReact(d) {
for (const k in d) {
if (k.startsWith("__reactInternalInstance$")) {
try {
return d[k].child;
} catch (e) {}
}
}
return null;
},
/**
* get a node's react children
* @param {string} s (CSS selector)
*/
getReact(s) {
const el = this.$qa(s);
try {
return el.length > 0 ? this.findReact(el[0]).memoizedProps.children : null;
} catch (e) {
return null;
}
},
/**
* determine if an element is visible (namely the amazon player)
* @param {string} s (element id)
*/
isVis(s) {
try {
return this.$i(s).offsetParent ? true : false;
} catch (e) {
return false;
}
},
// searches for elements that skip stuff. repeated every 300ms. change "rate" in the options if you want to make this more or less frequent.
async amazon() {
if (this.count === 0) {
// console.log(this.count);
if (this.isVis("dv-web-player")) {
if (this.$c("atvwebplayersdk-nextupcard-button").length) {
// console.log('Found Amazon video next.');
setTimeout(() => {
try {
this.$c("atvwebplayersdk-nextupcard-button")[0].click();
this.count = 5;
} catch (e) {}
}, 700);
} else if (this.$c("atvwebplayersdk-skipelement-button").length) {
try {
this.$c("atvwebplayersdk-skipelement-button")[0].click();
this.count = 5;
} catch (e) {}
} else if (this.$c("adSkipButton").length) {
// console.log('Found Amazon skip ad.');
this.$c("adSkipButton")[0].click();
this.count = 5;
} else if (this.$c("skipElement").length) {
// console.log('Found Amazon skip intro.');
this.$c("skipElement")[0].click();
this.count = 5;
} else if (this.$ev("Skip Intro")) {
// console.log('Found Amazon skip intro.');
this.$ev("Skip Intro").click();
this.count = 5;
} else if (this.$cnt("Skip").length) {
// amazon trailers
this.$click("Skip");
this.count = 5;
} else if (this.$cnt("Skip Intro").length) {
// amazon intro
this.$click("Skip Intro");
this.count = 5;
} else if (this.$cnt("Skip Recap").length) {
// amazon recap
this.$click("Skip Recap");
this.count = 5;
} else {
// console.log('404 keep looking.');
}
}
} else {
this.count--;
}
},
async netflix() {
if (this.count === 0) {
if (this.$c("skip-credits").length && this.$c("skip-credits-hidden").length == 0) {
// console.log('Found credits.');
await sleep(200);
this.$c("skip-credits")[0].firstElementChild.click();
await sleep(200);
this.$q(".button-nfplayerPlay").click();
this.count = 80;
// console.log('Found credits. +4s');
} else if (this.$q(this.nDrain)) {
// console.log('Netflix next episode draining button skipped');
this.getReact(this.nDrain)._owner.memoizedProps.handlePress();
this.count = 5;
} else if (this.$q(this.nReady)) {
// console.log('Netflix next episode button skipped');
this.getReact(
this.nReady
).props.children._owner.memoizedProps.onClickWatchNextEpisode();
this.count = 5;
} else if (this.$c("postplay-still-container").length) {
// console.log('Found autoplay.');
this.$c("postplay-still-container")[0].click();
this.count = 5;
} else if (this.$c("WatchNext-still-container").length) {
// console.log('Found autoplay.');
this.$c("WatchNext-still-container")[0].click();
this.count = 5;
} else {
// console.log('404 keep looking.');
}
} else {
this.count--;
}
},
};
/**
* pause execution for ms milliseconds
* @param {int} ms (milliseconds)
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* @param {string} u (a string to test the URL against)
*/
function test(u) {
return window.location.href.includes(u);
}
// an interval constructor that you can pause and resume, and which opens a brief popup when you do so.
class PauseUtil {
/**
* pausable interval utility
* @param {func} callback (the stuff you want to execute periodically, in this case marathon.netflix or marathon.amazon)
* @param {int} int (how often to repeat the callback)
*/
constructor(callback, int) {
this.callback = callback;
this.int = int;
this.popup = options.pop ? document.createElement("div") : null; // if popup is disabled, create nothing
this.text = options.pop ? document.createTextNode("Marathon: Paused") : null; // if popup is disabled, create nothing
this.remainder = 0; // how much time is remaining on the interval when we pause it
this.fading; // 3 second timeout (by default), after which the popup fades
this.pauseState = 0; // 0: idle, 1: running, 2: paused, 3: resumed
this.register("Pause Marathon", true); // initial creation of the menu command
// if popup is enabled in options, style it
if (options.pop) {
document.body.insertBefore(this.popup, document.body.firstElementChild);
this.popup.appendChild(this.text);
this.popup.style.cssText = `position:fixed;top:50%;right:3%;transform:translateY(-50%);z-index:2147483646;background:hsla(0, 0%, 8%, 0.7);color:hsla(0, 0%, 97%, 0.95);padding:17px 19px;border-radius:5px;pointer-events:none;letter-spacing:1px;transition:opacity 0.2s ease-in-out;opacity:0;`;
this.popup.style.fontFamily = options.font;
this.popup.style.fontSize = options.fontSize;
this.popup.style.fontWeight = options.fontWeight;
this.popup.style.fontStyle = options.italic ? "italic" : "";
}
this.time = new Date();
this.timer = window.setInterval(this.callback, this.int);
this.pauseState = 1;
}
// returns false if we're on a valid site but not actually in the video player (e.g. we're only browsing videos).
get playing() {
return site === "netflix" ? test("netflix.com/watch/") : marathon.isVis("dv-web-player");
}
// pause the interval
pause() {
if (this.pauseState !== 1) {
return;
}
this.remainder = this.int - (new Date() - this.time);
window.clearInterval(this.timer);
this.pauseState = 2;
this.register("Resume Marathon"); // update the menu command label
this.openPopup(false);
}
// resume the interval
async resume() {
if (this.pauseState !== 2) {
return;
}
this.pauseState = 3;
this.register("Pause Marathon");
this.openPopup(true);
await sleep(this.remainder);
this.run();
}
// when we pause, there's usually still time left on the interval. resume() calls this after waiting for the remaining duration. so this is what actually resumes the interval.
run() {
if (this.pauseState !== 3) {
return;
}
this.callback();
this.time = new Date();
this.timer = window.setInterval(this.callback, this.int);
this.pauseState = 1;
}
// toggle the interval on/off.
toggle() {
switch (this.pauseState) {
case 1:
return this.pause();
case 2:
return this.resume();
default:
return;
}
}
/**
* opens the popup and schedules it to close
* @param {bool} state (whether the popup should say "Resumed" or "Paused")
*/
openPopup(state) {
// if popup is disabled in options, do nothing
if (!options.pop) {
return;
}
// if window is netflix or amazon but there's no video player, (e.g. we're browsing titles) do nothing but ensure the popup is hidden.
if (!this.playing) {
this.popup.style.transitionDuration = "1s";
return (this.popup.style.opacity = "0");
}
let string = state ? "Resumed" : "Paused";
this.popup.textContent = `Marathon: ${string}`;
this.popup.style.transitionDuration = "0.2s";
this.popup.style.opacity = "1";
window.clearTimeout(this.fading); // clear any existing timeout since we're about to set a new one
// schedule the popup to fade into oblivion
this.fading = window.setTimeout(() => {
this.popup.style.transitionDuration = "1s";
this.popup.style.opacity = "0";
}, options.popDur);
}
/**
* register or change the label of the menu command
* @param {string} cap (intended caption to display on the menu command)
* @param {bool} firstRun (we call this function at startup and every time we pause/unpause. we don't need to register a menu command if this is the startup call, since none exists yet)
*/
register(cap, firstRun = false) {
if (GM4) {
return; // don't register a menu command if the script manager is greasemonkey 4.0+ since the function doesn't exist
}
if (!firstRun) {
GM_unregisterMenuCommand(this.caption);
}
GM_registerMenuCommand(cap, this.toggle.bind(this));
this.caption = cap;
}
}
// initial setup
function marathonSetUp() {
if (!options[site]) {
return; // if the site we're on is disabled in options, then don't bother setting up
}
let search = marathon[site].bind(marathon), // use the correct callback
searchInterval = new PauseUtil(search, options.rate), // create the interval with our rate setting
wf = options.webfont ? document.createElement("script") : null,
first = document.scripts[0],
ital = options.italic ? "ital," : "";
/**
* what to do when you press the hotkey.
* @param {object} e (event)
*/
function onKeyDown(e) {
if (e.repeat) {
return;
}
if (e.code == options.code && modTest(e)) {
e.stopImmediatePropagation();
e.stopPropagation();
searchInterval.toggle();
e.preventDefault();
}
}
/**
* check that the modifier keys match those defined in user settings
* @param {object} e (event)
*/
function modTest(e) {
let ctrl = options.ctrlKey,
alt = options.altKey,
shift = options.shiftKey,
meta = options.metaKey;
return e.ctrlKey == ctrl && e.altKey == alt && e.shiftKey == shift && e.metaKey == meta;
}
// start listening to key events
function startCapturing() {
window.addEventListener("keydown", onKeyDown, true);
}
// stop listening to key events (currently unused)
function stopCapturing() {
window.removeEventListener("keydown", onKeyDown, true);
}
WebFontConfig = {
classes: false, // don't bother changing the DOM at all, we aren't listening for it
events: false, // no need for events, not worth the execution
google: {
families: [
`${options.font}:${ital}wght@1,${options.fontWeight}`,
"Source Sans Pro:wght@1,300",
], // e.g. "Lobster Two:ital,wght@1,700"
display: "swap", // not really necessary since the popup doesn't appear until you press a button. but whatever
},
};
// load web font if enabled
if (options.webfont) {
wf.src = "https://cdn.jsdelivr.net/npm/webfontloader@latest/webfontloader.js";
wf.async = true;
first.parentNode.insertBefore(wf, first);
}
// if hotkey is enabled in options, start listening to keyboard events
if (options.hotkey) {
startCapturing();
}
return {
searchInterval,
startCapturing,
stopCapturing,
};
}
/**
* if using greasemonkey 4, remap the GM_* functions to GM.*
*/
async function checkGM() {
if (GM4) {
GM_getValue = GM.getValue;
GM_setValue = GM.setValue;
GM_listValues = GM.listValues;
GM_deleteValue = GM.deleteValue;
GM_openInTab = GM.openInTab;
}
}
/**
* set up the GM_config settings GUI
*/
async function initConfig() {
await checkGM();
const frame = document.createElement("div"),
resetti = document.createElement("button"),
supporti = document.createElement("button"),
keyframes = {
opacity: [0, 1],
},
animFwd = {
id: "GM_config_fwd",
direction: "normal",
duration: 200,
iterations: 1,
easing: "ease-in-out",
},
animBwd = {
id: "GM_config_bwd",
direction: "reverse",
duration: 500,
iterations: 1,
easing: "ease-in-out",
};
document.body.appendChild(frame);
frame.appendChild(resetti);
frame.appendChild(supporti);
resetti.addEventListener("click", () => GM_config.reset());
supporti.addEventListener("click", () =>
GM_openInTab(`https://greasyfork.org/scripts/420475-netflix-marathon-pausable`)
);
GM_config.close = function () {
window.clearTimeout(this.fading);
GM_config.animation = this.frame.animate(keyframes, animBwd);
this.onClose(); // Call the close() callback function
this.isOpen = false;
this.fading = window.setTimeout(() => {
let domSheets = document.getElementsByTagName("head")[0].getElementsByTagName("style"),
sheetArr = Array.from(domSheets);
for (let i of sheetArr) {
i instanceof HTMLStyleElement &&
i.sheet.cssRules[0].selectorText.includes("Marathon") &&
i.remove();
}
// If frame is an iframe then remove it
if (this.frame.contentDocument) {
this.remove(this.frame);
this.frame = null;
} else {
// else wipe its content
this.frame.innerHTML = "";
this.frame.style.display = "none";
}
// Null out all the fields so we don't leak memory
var fields = this.fields;
for (var id in fields) {
var field = fields[id];
field.wrapper = null;
field.node = null;
}
}, 500);
};
GM_config.open = function () {
window.clearTimeout(this.fading);
if (
GM_config.animation &&
GM_config.animation.id === "GM_config_bwd" &&
GM_config.animation.playState === "running"
) {
GM_config.animation.playbackRate = -2.5;
} else {
GM_config.animation = this.frame.animate(keyframes, animFwd);
}
this.isOpen = true;
GM_config.__proto__.open.call(this);
};
GM_config.init({
"id": "Marathon",
"title": "Netflix Marathon Settings",
"fields": {
"rate": {
"label": "Interval Rate",
"section": "Main Settings",
"type": "int",
"size": 8,
"min": 50,
"max": 5000,
"default": 300,
},
"amazon": {
"type": "checkbox",
"label": "Run on Amazon",
"default": true,
},
"netflix": {
"type": "checkbox",
"label": "Run on Netflix",
"default": true,
},
"code": {
"label": "Hotkey code",
"type": "text",
"section": "Hotkey Settings",
"size": 8,
"default": "F7",
},
"hotkey": {
"type": "checkbox",
"label": "Enable hotkey",
"default": true,
},
"ctrlKey": {
"type": "checkbox",
"label": "Ctrl key",
"default": true,
},
"altKey": {
"type": "checkbox",
"label": "Alt key",
"default": false,
},
"shiftKey": {
"type": "checkbox",
"label": "Shift key",
"default": false,
},
"metaKey": {
"type": "checkbox",
"label": "Meta key",
"default": false,
},
"pop": {
"type": "checkbox",
"label": "Enable popup",
"section": "Popup Settings",
"default": true,
},
"popDur": {
"label": "Popup duration",
"type": "int",
"size": 4,
"min": 500,
"max": 50000,
"default": 3000,
},
"webfont": {
"type": "checkbox",
"label": "Use Google Fonts",
"default": true,
},
"font": {
"label": "Popup font",
"type": "text",
"size": 12,
"default": "Source Sans Pro",
},
"fontSizeInt": {
"label": "Font size (px)",
"type": "int",
"size": 1,
"min": 6,
"max": 560,
"default": 24,
},
"fontWeight": {
"label": "Font weight",
"type": "select",
"options": ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
"default": 300,
},
"italic": {
"type": "checkbox",
"label": "Italic",
"default": false,
},
},
"events": {
"init": function () {
// migrate settings from previous versions, if any exist
let migrateKeys = GM_listValues().filter((key) => key !== "Marathon");
if (migrateKeys.length) {
for (const key of migrateKeys) {
let oldVal = GM_getValue(key);
if (key === "fontSize" && typeof oldVal === "string") {
let newVal = Number(oldVal.match(/\d+/g)[0]);
GM_config.set("fontSizeInt", newVal);
} else {
GM_config.set(key, oldVal);
}
GM_deleteValue(key);
}
}
GM_config.save();
// for all addons except greasemonkey 4, we can add a menu command
if (!GM4) {
GM_registerMenuCommand("Open Settings", () => {
if (!GM_config.isOpen) {
GM_config.open();
}
});
}
// add an Alt+N hotkey to pull up the settings menu, so greasemonkey 4 users can configure settings.
window.addEventListener("keydown", (e) => {
if (e.repeat) {
return;
}
if (e.code == "KeyN" && e.altKey) {
e.stopImmediatePropagation();
e.preventDefault();
e.stopPropagation();
if (GM_config.isOpen) {
GM_config.close();
} else {
GM_config.open();
}
}
});
// memoize the settings
settings();
},
"save": function () {
// close the settings menu upon save.
if (GM_config.isOpen) {
GM_config.close();
}
},
"open": function () {
let resetBtn = document.getElementById("Marathon_resetLink");
resetti.title = resetBtn.title;
resetti.textContent = resetBtn.textContent;
resetti.className = resetBtn.parentElement.className;
resetBtn.parentElement.replaceWith(resetti);
document.getElementById("Marathon_saveBtn").after(resetti);
supporti.title = locale.title;
supporti.textContent = locale.text;
supporti.className = "saveclose_buttons";
supporti.id = "Marathon_supportBtn";
document.getElementById("Marathon_closeBtn").after(supporti);
},
},
"frame": frame,
"css": `
#Marathon {
position: fixed !important;
z-index: 2147483646 !important;
inset: unset !important;
top: 50% !important;
left: 0% !important;
background: hsla(0, 0%, 8%, 0.7);
border: none !important;
color: hsla(0, 0%, 97%, 0.95);
max-width: min-content !important;
height: min-content !important;
border-radius: 5px;
padding: 10px !important;
transform: translate(50%, -60%);
}
#Marathon * {
font-family: Source Sans Pro;
font-weight: 300;
}
#Marathon_wrapper {
display: flex;
flex-direction: column;
align-content: center;
}
#Marathon_header {
font-size: 2em !important;
white-space: nowrap;
padding-inline: 6px;
}
#Marathon .section_header_holder {
display: flex;
flex-flow: row wrap;
border-top: 1px solid hsla(0, 0%, 100%, 0.1);
}
#Marathon .section_header {
font-size: 1.25em !important;
background: none !important;
border: none !important;
text-align: left !important;
padding-block: 5px !important;
flex-basis: 100%;
margin-inline: 6px;
}
#Marathon .config_var {
margin: 0 0 6px 8px;
display: flex;
flex-direction: row;
align-items: center;
line-height: normal;
flex-grow: 1;
}
input[type="text"] {
appearance: none;
color: inherit;
background: hsla(0, 0%, 25%, 50%) !important;
border: none;
border-radius: 3px;
padding-inline: 4px;
flex-grow: 1;
}
input[type="text"]:focus {
background-color: hsla(0, 0%, 25%, 70%) !important;
color: white !important;
}
input[type="checkbox"] {
appearance: none !important;
border: 2px solid hsl(240, 6.7%, 58.8%);
min-width: 14px;
min-height: 14px;
background: hsl(0, 0%, 100%) url("data:image/svg+xml;utf8,") center/contain no-repeat;
border-radius: 2.5px;
}
input[type="checkbox"]:focus {
box-shadow: 0 0 0 .1em hsl(214.3, 58.3%, 81.8%), 0 0 0 .15em hsl(214.2, 60%, 42.7%), 0 0 0 .25em hsl(214.3, 58.3%, 71.8%);
}
input[type="checkbox"]:hover {
border: 2px solid hsl(240, 6%, 43%);
}
input[type="checkbox"]:hover:active {
background-color: hsl(240, 8%, 83%);
border: 2px solid hsl(240, 6%, 30%);
}
input[type="checkbox"]:checked {
border: 2px solid transparent !important;
background: hsl(214.2, 100%, 43.7%) url("data:image/svg+xml;utf8,") center/contain no-repeat !important;
}
input[type="checkbox"]:checked:hover {
background-color: hsl(215, 98%, 37%) !important;
}
input[type="checkbox"]:checked:hover:active {
background-color: hsl(216, 94%, 30%) !important;
}
#Marathon_section_0 > .config-var,
#Marathon_field_rate {
flex-grow: unset;
}
#Marathon_buttons_holder {
display: flex;
flex-flow: row;
align-items: center;
border-top: 1px solid hsla(0, 0%, 100%, 0.1);
color: inherit !important;
}
#Marathon .saveclose_buttons,
#Marathon .reset_holder {
margin: 6px 6px 0px 0px;
padding: 2px 12px;
color: inherit;
background: hsla(0, 0%, 25%, 50%);
border: none !important;
border-radius: 3px;
padding-inline: 4px;
font-size: 15px;
padding-block: 2px;
flex-grow: 1;
}
#Marathon .saveclose_buttons:hover,
#Marathon .reset_holder:hover {
background-color: hsla(0, 0%, 25%, 70%) !important;
color: white !important;
}
#Marathon_saveBtn {
padding-inline: 16px 2px !important;
background: hsla(0, 0%, 25%, 50%) url("data:image/svg+xml;utf8,") 3.8px 48%/12.5px no-repeat !important;
}
#Marathon .reset_holder {
padding-inline: 16px 2px !important;
background: hsla(0, 0%, 25%, 50%) url("data:image/svg+xml;utf8,") 4.5px 50%/11px no-repeat !important;
}
#Marathon .reset {
color: inherit !important;
font-size: inherit !important;
}
#Marathon .field_label {
font-size: 12px;
font-weight: normal !important;
margin-inline: 6px 0 !important;
white-space: nowrap;
}
#Marathon .field_label:first-child {
margin-inline: 0 6px !important;
}
#Marathon select {
appearance: none;
color: inherit;
border: none;
border-radius: 3px;
padding-inline: 2px 13px;
background: hsla(0, 0%, 25%, 50%) url("data:image/svg+xml;utf8,") 100% 66%/18px no-repeat !important;
}
#Marathon option {
appearance: none;
color: inherit;
background: hsl(0, 1%, 17%) !important;
border: none;
}
#Marathon_rate_var,
#Marathon_pop_var,
#Marathon_font_var {
flex-basis: 100%;
}
`,
});
}
// after getting settings from *monkey storage, create properties in options (the js object) based on the stored settings.
async function settings() {
for (const key in GM_config.fields) {
options[key] = GM_config.get(`${key}`);
}
options.fontSize = `${options.fontSizeInt}px`;
}
async function start() {
await initConfig();
marathonSetUp();
}
start();