Sven 1 rok pred
rodič
commit
51b514d99c
1 zmenil súbory, kde vykonal 348 pridanie a 331 odobranie
  1. 348 331
      dist/BetterYTM.user.js

+ 348 - 331
dist/BetterYTM.user.js

@@ -17,7 +17,7 @@
 // @license           AGPL-3.0-only
 // @author            Sv443
 // @copyright         Sv443 (https://github.com/Sv443)
-// @icon              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=70dad24
+// @icon              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=4d06e4aa
 // @match             https://music.youtube.com/*
 // @match             https://www.youtube.com/*
 // @run-at            document-start
@@ -34,35 +34,35 @@
 // @grant             GM.xmlHttpRequest
 // @grant             unsafeWindow
 // @noframes
-// @resource          css-anchor_improvements https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/anchorImprovements.css?b=70dad24
-// @resource          css-fix_spacing         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/fixSpacing.css?b=70dad24
-// @resource          doc-changelog           https://raw.githubusercontent.com/Sv443/BetterYTM/develop/changelog.md?b=70dad24
-// @resource          icon-advanced_mode      https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/plus_circle_small.svg?b=70dad24
-// @resource          icon-arrow_down         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/arrow_down.svg?b=70dad24
-// @resource          icon-delete             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/delete.svg?b=70dad24
-// @resource          icon-error              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/error.svg?b=70dad24
-// @resource          icon-experimental       https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/beaker_small.svg?b=70dad24
-// @resource          icon-globe              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/globe.svg?b=70dad24
-// @resource          icon-help               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/help.svg?b=70dad24
-// @resource          icon-lock               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/lock.svg?b=70dad24
-// @resource          icon-lyrics             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/lyrics.svg?b=70dad24
-// @resource          icon-skip_to            https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/skip_to.svg?b=70dad24
-// @resource          icon-spinner            https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/spinner.svg?b=70dad24
-// @resource          img-logo                https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=70dad24
-// @resource          img-close               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/close.png?b=70dad24
-// @resource          img-discord             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/discord.png?b=70dad24
-// @resource          img-github              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/github.png?b=70dad24
-// @resource          img-greasyfork          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/greasyfork.png?b=70dad24
-// @resource          img-openuserjs          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/openuserjs.png?b=70dad24
-// @resource          trans-de_DE             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/de_DE.json?b=70dad24
-// @resource          trans-en_US             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_US.json?b=70dad24
-// @resource          trans-en_UK             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_UK.json?b=70dad24
-// @resource          trans-es_ES             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/es_ES.json?b=70dad24
-// @resource          trans-fr_FR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/fr_FR.json?b=70dad24
-// @resource          trans-hi_IN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/hi_IN.json?b=70dad24
-// @resource          trans-ja_JA             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/ja_JA.json?b=70dad24
-// @resource          trans-pt_BR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/pt_BR.json?b=70dad24
-// @resource          trans-zh_CN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/zh_CN.json?b=70dad24
+// @resource          css-anchor_improvements https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/anchorImprovements.css?b=4d06e4aa
+// @resource          css-fix_spacing         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/fixSpacing.css?b=4d06e4aa
+// @resource          doc-changelog           https://raw.githubusercontent.com/Sv443/BetterYTM/develop/changelog.md?b=4d06e4aa
+// @resource          icon-advanced_mode      https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/plus_circle_small.svg?b=4d06e4aa
+// @resource          icon-arrow_down         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/arrow_down.svg?b=4d06e4aa
+// @resource          icon-delete             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/delete.svg?b=4d06e4aa
+// @resource          icon-error              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/error.svg?b=4d06e4aa
+// @resource          icon-experimental       https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/beaker_small.svg?b=4d06e4aa
+// @resource          icon-globe              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/globe.svg?b=4d06e4aa
+// @resource          icon-help               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/help.svg?b=4d06e4aa
+// @resource          icon-lock               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/lock.svg?b=4d06e4aa
+// @resource          icon-lyrics             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/lyrics.svg?b=4d06e4aa
+// @resource          icon-skip_to            https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/skip_to.svg?b=4d06e4aa
+// @resource          icon-spinner            https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/spinner.svg?b=4d06e4aa
+// @resource          img-logo                https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=4d06e4aa
+// @resource          img-close               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/close.png?b=4d06e4aa
+// @resource          img-discord             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/discord.png?b=4d06e4aa
+// @resource          img-github              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/github.png?b=4d06e4aa
+// @resource          img-greasyfork          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/greasyfork.png?b=4d06e4aa
+// @resource          img-openuserjs          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/openuserjs.png?b=4d06e4aa
+// @resource          trans-de_DE             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/de_DE.json?b=4d06e4aa
+// @resource          trans-en_US             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_US.json?b=4d06e4aa
+// @resource          trans-en_UK             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_UK.json?b=4d06e4aa
+// @resource          trans-es_ES             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/es_ES.json?b=4d06e4aa
+// @resource          trans-fr_FR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/fr_FR.json?b=4d06e4aa
+// @resource          trans-hi_IN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/hi_IN.json?b=4d06e4aa
+// @resource          trans-ja_JA             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/ja_JA.json?b=4d06e4aa
+// @resource          trans-pt_BR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/pt_BR.json?b=4d06e4aa
+// @resource          trans-zh_CN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/zh_CN.json?b=4d06e4aa
 // @require           https://cdn.jsdelivr.net/npm/@sv443-network/[email protected]/dist/index.global.js
 // @require           https://cdn.jsdelivr.net/npm/[email protected]/dist/fuse.basic.js
 // @require           https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.js
@@ -239,7 +239,7 @@ var LogLevel;
 })(LogLevel || (LogLevel = {}));const modeRaw = "development";
 const branchRaw = "develop";
 const hostRaw = "github";
-const buildNumberRaw = "70dad24";
+const buildNumberRaw = "4d06e4aa";
 /** The mode in which the script was built (production or development) */
 const mode = (modeRaw.match(/^#{{.+}}$/) ? "production" : modeRaw);
 /** The branch to use in various URLs that point to the GitHub repo */
@@ -398,248 +398,7 @@ var locales = {
 	ja_JA: ja_JA,
 	pt_BR: pt_BR,
 	zh_CN: zh_CN
-};let features$3;
-function setBehaviorConfig(feats) {
-    features$3 = feats;
-}
-//#MARKER beforeunload popup
-let beforeUnloadEnabled = true;
-/** Disables the popup before leaving the site */
-function disableBeforeUnload() {
-    beforeUnloadEnabled = false;
-    info("Disabled popup before leaving the site");
-}
-/**
- * Adds a spy function into `window.__proto__.addEventListener` to selectively discard `beforeunload`
- * event listeners before they can be called by the site.
- */
-function initBeforeUnloadHook() {
-    return __awaiter(this, void 0, void 0, function* () {
-        Error.stackTraceLimit = 1000; // default is 25 on FF so this should hopefully be more than enough
-        (function (original) {
-            // @ts-ignore
-            window.__proto__.addEventListener = function (...args) {
-                const origListener = typeof args[1] === "function" ? args[1] : args[1].handleEvent;
-                args[1] = function (...a) {
-                    if (!beforeUnloadEnabled && args[0] === "beforeunload") {
-                        info("Prevented beforeunload event listener from being called");
-                        return false;
-                    }
-                    else
-                        return origListener.apply(this, a);
-                };
-                original.apply(this, args);
-            };
-            // @ts-ignore
-        })(window.__proto__.addEventListener);
-    });
-}
-//#MARKER auto close toasts
-/** Closes toasts after a set amount of time */
-function initAutoCloseToasts() {
-    return __awaiter(this, void 0, void 0, function* () {
-        try {
-            const animTimeout = 300;
-            const closeTimeout = Math.max(features$3.closeToastsTimeout * 1000 + animTimeout, animTimeout);
-            onSelectorOld("tp-yt-paper-toast#toast", {
-                all: true,
-                continuous: true,
-                listener: (toastElems) => __awaiter(this, void 0, void 0, function* () {
-                    var _a;
-                    for (const toastElem of toastElems) {
-                        if (!toastElem.hasAttribute("allow-click-through"))
-                            continue;
-                        if (toastElem.classList.contains("bytm-closing"))
-                            continue;
-                        toastElem.classList.add("bytm-closing");
-                        yield UserUtils.pauseFor(closeTimeout);
-                        toastElem.classList.remove("paper-toast-open");
-                        log(`Automatically closed toast '${(_a = toastElem.querySelector("#text-container yt-formatted-string")) === null || _a === void 0 ? void 0 : _a.textContent}' after ${features$3.closeToastsTimeout * 1000}ms`);
-                        // wait for the transition to finish
-                        yield UserUtils.pauseFor(animTimeout);
-                        toastElem.style.display = "none";
-                    }
-                }),
-            });
-            log("Initialized automatic toast closing");
-        }
-        catch (err) {
-            error("Error in automatic toast closing:", err);
-        }
-    });
-}
-/** After how many milliseconds a remembered entry should expire */
-const remSongEntryExpiry = 1000 * 60 * 1;
-/** Minimum time a song has to be played before it is committed to GM storage */
-const remSongMinPlayTime = 10;
-let remSongsCache = [];
-/** Remembers the time of the last played song and resumes playback from that time */
-function initRememberSongTime() {
-    return __awaiter(this, void 0, void 0, function* () {
-        if (features$3.rememberSongTimeSites !== "all" && features$3.rememberSongTimeSites !== getDomain())
-            return;
-        const storedDataRaw = yield GM.getValue("bytm-rem-songs");
-        if (!storedDataRaw)
-            yield GM.setValue("bytm-rem-songs", "[]");
-        remSongsCache = JSON.parse(String(storedDataRaw !== null && storedDataRaw !== void 0 ? storedDataRaw : "[]"));
-        log(`Initialized song time remembering with ${remSongsCache.length} initial entries`);
-        if (location.pathname.startsWith("/watch"))
-            yield restoreSongTime();
-        remSongUpdateEntry();
-        setInterval(remSongUpdateEntry, 1000);
-    });
-}
-/** Tries to restore the time of the currently playing song */
-function restoreSongTime() {
-    return __awaiter(this, void 0, void 0, function* () {
-        if (location.pathname.startsWith("/watch")) {
-            const { searchParams } = new URL(location.href);
-            const watchID = searchParams.get("v");
-            if (!watchID)
-                return;
-            const entry = remSongsCache.find(entry => entry.watchID === watchID);
-            if (entry) {
-                if (Date.now() - entry.updateTimestamp > remSongEntryExpiry) {
-                    yield delRemSongData(entry.watchID);
-                    return;
-                }
-                else {
-                    onSelectorOld(videoSelector, {
-                        listener: (vidElem) => __awaiter(this, void 0, void 0, function* () {
-                            if (vidElem) {
-                                const applyTime = () => __awaiter(this, void 0, void 0, function* () {
-                                    if (isNaN(entry.songTime))
-                                        return;
-                                    vidElem.currentTime = UserUtils.clamp(Math.max(entry.songTime, 0), 0, vidElem.duration);
-                                    yield delRemSongData(entry.watchID);
-                                    info(`Restored song time to ${Math.floor(entry.songTime / 60)}m, ${(entry.songTime % 60).toFixed(1)}s`, LogLevel.Info);
-                                });
-                                if (vidElem.readyState === 4)
-                                    applyTime();
-                                else
-                                    vidElem.addEventListener("canplay", applyTime, { once: true });
-                            }
-                        }),
-                    });
-                }
-            }
-        }
-    });
-}
-/** Updates the currently playing song's entry in GM storage */
-function remSongUpdateEntry() {
-    var _a, _b, _c;
-    return __awaiter(this, void 0, void 0, function* () {
-        if (location.pathname.startsWith("/watch")) {
-            const { searchParams } = new URL(location.href);
-            const watchID = searchParams.get("v");
-            if (!watchID)
-                return;
-            const songTime = (_a = yield getVideoTime()) !== null && _a !== void 0 ? _a : 0;
-            const paused = (_c = (_b = document.querySelector(videoSelector)) === null || _b === void 0 ? void 0 : _b.paused) !== null && _c !== void 0 ? _c : false;
-            // don't immediately update to reduce race conditions and only update if the video is playing
-            // also it just sounds better if the song starts at the beginning if only a couple seconds have passed
-            if (songTime > remSongMinPlayTime && !paused) {
-                const entry = {
-                    watchID,
-                    songTime,
-                    updateTimestamp: Date.now(),
-                };
-                yield setRemSongData(entry);
-            }
-            // if the song is rewound to the beginning, delete the entry
-            else {
-                const entry = remSongsCache.find(entry => entry.watchID === watchID);
-                if (entry && songTime <= remSongMinPlayTime)
-                    yield delRemSongData(entry.watchID);
-            }
-        }
-        const expiredEntries = remSongsCache.filter(entry => Date.now() - entry.updateTimestamp > remSongEntryExpiry);
-        for (const entry of expiredEntries)
-            yield delRemSongData(entry.watchID);
-    });
-}
-/** Adds an entry or updates it if it already exists */
-function setRemSongData(data) {
-    return __awaiter(this, void 0, void 0, function* () {
-        const foundIdx = remSongsCache.findIndex(entry => entry.watchID === data.watchID);
-        if (foundIdx >= 0)
-            remSongsCache[foundIdx] = data;
-        else
-            remSongsCache.push(data);
-        yield GM.setValue("bytm-rem-songs", JSON.stringify(remSongsCache));
-    });
-}
-/** Deletes an entry */
-function delRemSongData(watchID) {
-    return __awaiter(this, void 0, void 0, function* () {
-        remSongsCache = [...remSongsCache.filter(entry => entry.watchID !== watchID)];
-        yield GM.setValue("bytm-rem-songs", JSON.stringify(remSongsCache));
-    });
-}
-//#MARKER disable darkreader
-/** Disables Dark Reader if it is enabled */
-function disableDarkReader() {
-    if (document.querySelector(".darkreader")) {
-        const metaElem = document.createElement("meta");
-        metaElem.name = "darkreader-lock";
-        metaElem.classList.add("bytm-disable-darkreader");
-        document.head.appendChild(metaElem);
-        info("Sent hint to Dark Reader to disable itself");
-    }
-}
-//#MARKER lock volume
-let volumeSliderObserverActive = false;
-let sliderElem;
-function overrideVolValues() {
-    if (!sliderElem || !getFeatures().lockVolume)
-        return;
-    volumeSliderObserverActive = false;
-    setTimeout(() => {
-        const vidElem = document.querySelector(videoSelector);
-        if (vidElem)
-            vidElem.volume = getFeatures().lockVolumeLevel / 100;
-        if (!sliderElem) {
-            volumeSliderObserverActive = true;
-            return;
-        }
-        sliderElem.value = String(getFeatures().lockVolumeLevel);
-        sliderElem.setAttribute("aria-valuenow", String(getFeatures().lockVolumeLevel));
-        const knobElem = document.querySelector("#volume-slider #sliderKnobContainer #sliderKnob");
-        if (knobElem)
-            knobElem.style.left = `${getFeatures().lockVolumeLevel}%`;
-        const labelElem = document.querySelector("#bytm-vol-slider-label .label");
-        const newLabelContent = `${getFeatures().lockVolumeLevel}%`;
-        if (labelElem && labelElem.textContent !== newLabelContent)
-            labelElem.textContent = newLabelContent;
-        volumeSliderObserverActive = true;
-    }, 10);
-}
-/** Locks the volume slider at a specific level */
-function enableLockVolume() {
-    return __awaiter(this, void 0, void 0, function* () {
-        const observer = new MutationObserver((mutations) => {
-            for (const mutation of mutations) {
-                if (!volumeSliderObserverActive)
-                    return;
-                if (mutation.target.id === "sliderBar" && mutation.type === "attributes") {
-                    if (mutation.attributeName === "value" || mutation.attributeName === "aria-valuenow")
-                        overrideVolValues();
-                }
-            }
-        });
-        onSelectorOld("#volume-slider tp-yt-paper-progress#sliderBar", {
-            listener: (elem) => {
-                sliderElem = elem;
-                overrideVolValues();
-                volumeSliderObserverActive = true;
-                observer.observe(elem, {
-                    attributeFilter: ["value", "aria-valuenow"],
-                });
-            }
-        });
-    });
-}/** A fraction of this max value will be removed from the "last viewed" timestamp when adding penalized cache entries */
+};/** A fraction of this max value will be removed from the "last viewed" timestamp when adding penalized cache entries */
 const maxViewedPenalty = 1000 * 60 * 60 * 24 * 5; // 5 days
 /** A fraction of this max value will be removed from the "added" timestamp when adding penalized cache entries */
 const maxAddedPenalty = 1000 * 60 * 60 * 24 * 15; // 15 days
@@ -1403,7 +1162,7 @@ var scripts = {
 	"tr-format": "npm run node-ts -- ./src/tools/tr-format.ts",
 	"gen-readme": "npm run node-ts -- ./src/tools/gen-readme.ts",
 	"node-ts": "node --no-warnings=ExperimentalWarning --enable-source-maps --loader ts-node/esm",
-	invisible: "node src/tools/run-invisible.mjs",
+	invisible: "node --enable-source-maps src/tools/run-invisible.mjs",
 	test: "npm run node-ts -- ./test.ts"
 };
 var engines = {
@@ -1694,7 +1453,7 @@ let initConfig$1;
  * @deprecated to be replaced with new menu - see https://github.com/Sv443/BetterYTM/issues/23
  */
 function addCfgMenu() {
-    var _a, _b, _c, _d, _e;
+    var _a, _b, _c, _d, _f;
     return __awaiter(this, void 0, void 0, function* () {
         if (isCfgMenuAdded)
             return;
@@ -1851,14 +1610,14 @@ function addCfgMenu() {
         featuresCont.id = "bytm-menu-opts";
         /** Gets called whenever the feature config is changed */
         const confChanged = UserUtils.debounce((key, initialVal, newVal) => __awaiter(this, void 0, void 0, function* () {
-            var _f, _g;
+            var _g, _h;
             const fmt = (val) => typeof val === "object" ? JSON.stringify(val) : String(val);
             info(`Feature config changed at key '${key}', from value '${fmt(initialVal)}' to '${fmt(newVal)}'`);
             const featConf = JSON.parse(JSON.stringify(getFeatures()));
             featConf[key] = newVal;
             yield saveFeatures(featConf);
             // @ts-ignore
-            (_g = (_f = featInfo[key]) === null || _f === void 0 ? void 0 : _f.change) === null || _g === void 0 ? void 0 : _g.call(_f, featConf);
+            (_h = (_g = featInfo[key]) === null || _g === void 0 ? void 0 : _g.change) === null || _h === void 0 ? void 0 : _h.call(_g, featConf);
             if (initConfig$1 !== JSON.stringify(featConf))
                 footerElem.classList.remove("hidden");
             else
@@ -1885,7 +1644,15 @@ function addCfgMenu() {
             acc[category][key] = featureCfg[key];
             return acc;
         }, {});
-        const fmtVal = (v) => typeof v === "object" ? JSON.stringify(v) : String(v).trim();
+        const fmtVal = (v) => {
+            try {
+                return (typeof v === "object" ? JSON.stringify(v) : String(v)).trim();
+            }
+            catch (_e) {
+                // because stringify throws on circular refs
+                return String(v).trim();
+            }
+        };
         for (const category in featureCfgWithCategories) {
             const featObj = featureCfgWithCategories[category];
             const catHeaderElem = document.createElement("h3");
@@ -2023,7 +1790,7 @@ function addCfgMenu() {
                             labelElem.textContent = `${fmtVal(initialVal)}${unitTxt}`;
                             inputElem.addEventListener("input", () => {
                                 if (labelElem && lastDisplayedVal !== inputElem.value) {
-                                    labelElem.textContent = `${fmtVal(inputElem.value)} ${unitTxt}`;
+                                    labelElem.textContent = `${fmtVal(inputElem.value)}${unitTxt}`;
                                     lastDisplayedVal = inputElem.value;
                                 }
                             });
@@ -2186,23 +1953,7 @@ function addCfgMenu() {
         });
         versionEl.addEventListener("click", versionElemClicked);
         versionEl.addEventListener("keydown", (e) => e.key === "Enter" && versionElemClicked(e));
-        let advancedIndicatorEl;
-        if (getFeatures().advancedMode) {
-            const indicatorIconHtml = yield resourceToHTMLString("icon-advanced_mode");
-            const advancedIndicatorIconEl = document.createElement("span");
-            advancedIndicatorIconEl.classList.add("bytm-advanced-mode-icon");
-            if (indicatorIconHtml)
-                advancedIndicatorIconEl.innerHTML = indicatorIconHtml;
-            const advancedIndicatorLabelEl = document.createElement("span");
-            advancedIndicatorLabelEl.classList.add("bytm-advanced-mode-indicator-label");
-            advancedIndicatorLabelEl.textContent = t("advanced_mode");
-            advancedIndicatorEl = document.createElement("span");
-            advancedIndicatorEl.id = "bytm-cfg-menu-advanced-mode-indicator-wrapper";
-            advancedIndicatorEl.appendChild(advancedIndicatorIconEl);
-            advancedIndicatorEl.appendChild(advancedIndicatorLabelEl);
-        }
         subtitleElemCont.appendChild(versionEl);
-        advancedIndicatorEl && subtitleElemCont.appendChild(advancedIndicatorEl);
         titleElem.appendChild(subtitleElemCont);
         menuContainer.appendChild(footerCont);
         backgroundElem.appendChild(menuContainer);
@@ -2212,7 +1963,7 @@ function addCfgMenu() {
         // ensure stuff is reset if menu was opened before being added
         isCfgMenuOpen = false;
         document.body.classList.remove("bytm-disable-scroll");
-        (_e = document.querySelector("ytmusic-app")) === null || _e === void 0 ? void 0 : _e.removeAttribute("inert");
+        (_f = document.querySelector("ytmusic-app")) === null || _f === void 0 ? void 0 : _f.removeAttribute("inert");
         backgroundElem.style.visibility = "hidden";
         backgroundElem.style.display = "none";
     });
@@ -3281,6 +3032,242 @@ function addScrollToActiveBtn() {
             }),
         });
     });
+}//#MARKER beforeunload popup
+let beforeUnloadEnabled = true;
+/** Disables the popup before leaving the site */
+function disableBeforeUnload() {
+    beforeUnloadEnabled = false;
+    info("Disabled popup before leaving the site");
+}
+/**
+ * Adds a spy function into `window.__proto__.addEventListener` to selectively discard `beforeunload`
+ * event listeners before they can be called by the site.
+ */
+function initBeforeUnloadHook() {
+    return __awaiter(this, void 0, void 0, function* () {
+        Error.stackTraceLimit = 1000; // default is 25 on FF so this should hopefully be more than enough
+        (function (original) {
+            // @ts-ignore
+            window.__proto__.addEventListener = function (...args) {
+                const origListener = typeof args[1] === "function" ? args[1] : args[1].handleEvent;
+                args[1] = function (...a) {
+                    if (!beforeUnloadEnabled && args[0] === "beforeunload") {
+                        info("Prevented 'beforeunload' event listener");
+                        return false;
+                    }
+                    else
+                        return origListener.apply(this, a);
+                };
+                original.apply(this, args);
+            };
+            // @ts-ignore
+        })(window.__proto__.addEventListener);
+    });
+}
+//#MARKER auto close toasts
+/** Closes toasts after a set amount of time */
+function initAutoCloseToasts() {
+    return __awaiter(this, void 0, void 0, function* () {
+        try {
+            const animTimeout = 300;
+            const closeTimeout = Math.max(getFeatures().closeToastsTimeout * 1000 + animTimeout, animTimeout);
+            onSelectorOld("tp-yt-paper-toast#toast", {
+                all: true,
+                continuous: true,
+                listener: (toastElems) => __awaiter(this, void 0, void 0, function* () {
+                    var _a;
+                    for (const toastElem of toastElems) {
+                        if (!toastElem.hasAttribute("allow-click-through"))
+                            continue;
+                        if (toastElem.classList.contains("bytm-closing"))
+                            continue;
+                        toastElem.classList.add("bytm-closing");
+                        yield UserUtils.pauseFor(closeTimeout);
+                        toastElem.classList.remove("paper-toast-open");
+                        log(`Automatically closed toast '${(_a = toastElem.querySelector("#text-container yt-formatted-string")) === null || _a === void 0 ? void 0 : _a.textContent}' after ${getFeatures().closeToastsTimeout * 1000}ms`);
+                        // wait for the transition to finish
+                        yield UserUtils.pauseFor(animTimeout);
+                        toastElem.style.display = "none";
+                    }
+                }),
+            });
+            log("Initialized automatic toast closing");
+        }
+        catch (err) {
+            error("Error in automatic toast closing:", err);
+        }
+    });
+}
+let remSongsCache = [];
+/** Remembers the time of the last played song and resumes playback from that time */
+function initRememberSongTime() {
+    return __awaiter(this, void 0, void 0, function* () {
+        if (getFeatures().rememberSongTimeSites !== "all" && getFeatures().rememberSongTimeSites !== getDomain())
+            return;
+        const storedDataRaw = yield GM.getValue("bytm-rem-songs");
+        if (!storedDataRaw)
+            yield GM.setValue("bytm-rem-songs", "[]");
+        remSongsCache = JSON.parse(String(storedDataRaw !== null && storedDataRaw !== void 0 ? storedDataRaw : "[]"));
+        log(`Initialized song time remembering with ${remSongsCache.length} initial entries`);
+        if (location.pathname.startsWith("/watch"))
+            yield restoreSongTime();
+        remSongUpdateEntry();
+        setInterval(remSongUpdateEntry, 1000);
+    });
+}
+/** Tries to restore the time of the currently playing song */
+function restoreSongTime() {
+    return __awaiter(this, void 0, void 0, function* () {
+        if (location.pathname.startsWith("/watch")) {
+            const { searchParams } = new URL(location.href);
+            const watchID = searchParams.get("v");
+            if (!watchID)
+                return;
+            const entry = remSongsCache.find(entry => entry.watchID === watchID);
+            if (entry) {
+                if (Date.now() - entry.updateTimestamp > getFeatures().rememberSongTimeDuration * 1000) {
+                    yield delRemSongData(entry.watchID);
+                    return;
+                }
+                else {
+                    onSelectorOld(videoSelector, {
+                        listener: (vidElem) => __awaiter(this, void 0, void 0, function* () {
+                            if (vidElem) {
+                                const applyTime = () => __awaiter(this, void 0, void 0, function* () {
+                                    var _a;
+                                    if (isNaN(entry.songTime))
+                                        return;
+                                    const vidRestoreTime = entry.songTime - ((_a = getFeatures().rememberSongTimeReduction) !== null && _a !== void 0 ? _a : 0);
+                                    vidElem.currentTime = UserUtils.clamp(Math.max(vidRestoreTime, 0), 0, vidElem.duration);
+                                    yield delRemSongData(entry.watchID);
+                                    info(`Restored song time to ${Math.floor(vidRestoreTime / 60)}m, ${(vidRestoreTime % 60).toFixed(1)}s`, LogLevel.Info);
+                                });
+                                // jump to video time just after YT has finished doing their own shenanigans
+                                if (vidElem.readyState === 4)
+                                    applyTime();
+                                else
+                                    vidElem.addEventListener("canplay", applyTime, { once: true });
+                            }
+                        }),
+                    });
+                }
+            }
+        }
+    });
+}
+/** Updates the currently playing song's entry in GM storage */
+function remSongUpdateEntry() {
+    var _a, _b, _c;
+    return __awaiter(this, void 0, void 0, function* () {
+        if (location.pathname.startsWith("/watch")) {
+            const { searchParams } = new URL(location.href);
+            const watchID = searchParams.get("v");
+            if (!watchID)
+                return;
+            const songTime = (_a = yield getVideoTime()) !== null && _a !== void 0 ? _a : 0;
+            const paused = (_c = (_b = document.querySelector(videoSelector)) === null || _b === void 0 ? void 0 : _b.paused) !== null && _c !== void 0 ? _c : false;
+            // don't immediately update to reduce race conditions and only update if the video is playing
+            // also it just sounds better if the song starts at the beginning if only a couple seconds have passed
+            if (songTime > getFeatures().rememberSongTimeMinPlayTime && !paused) {
+                const entry = {
+                    watchID,
+                    songTime,
+                    updateTimestamp: Date.now(),
+                };
+                yield setRemSongData(entry);
+            }
+            // if the song is rewound to the beginning, delete the entry
+            else {
+                const entry = remSongsCache.find(entry => entry.watchID === watchID);
+                if (entry && songTime <= getFeatures().rememberSongTimeMinPlayTime)
+                    yield delRemSongData(entry.watchID);
+            }
+        }
+        const expiredEntries = remSongsCache.filter(entry => Date.now() - entry.updateTimestamp > getFeatures().rememberSongTimeDuration * 1000);
+        for (const entry of expiredEntries)
+            yield delRemSongData(entry.watchID);
+    });
+}
+/** Adds an entry or updates it if it already exists */
+function setRemSongData(data) {
+    return __awaiter(this, void 0, void 0, function* () {
+        const foundIdx = remSongsCache.findIndex(entry => entry.watchID === data.watchID);
+        if (foundIdx >= 0)
+            remSongsCache[foundIdx] = data;
+        else
+            remSongsCache.push(data);
+        yield GM.setValue("bytm-rem-songs", JSON.stringify(remSongsCache));
+    });
+}
+/** Deletes an entry */
+function delRemSongData(watchID) {
+    return __awaiter(this, void 0, void 0, function* () {
+        remSongsCache = [...remSongsCache.filter(entry => entry.watchID !== watchID)];
+        yield GM.setValue("bytm-rem-songs", JSON.stringify(remSongsCache));
+    });
+}
+//#MARKER disable darkreader
+/** Disables Dark Reader if it is enabled */
+function disableDarkReader() {
+    if (document.querySelector(".darkreader")) {
+        const metaElem = document.createElement("meta");
+        metaElem.name = "darkreader-lock";
+        metaElem.classList.add("bytm-disable-darkreader");
+        document.head.appendChild(metaElem);
+        info("Sent hint to Dark Reader to disable itself");
+    }
+}
+//#MARKER lock volume
+let volumeSliderObserverActive = false;
+let sliderElem;
+function overrideVolValues() {
+    if (!sliderElem || !getFeatures().lockVolume)
+        return;
+    volumeSliderObserverActive = false;
+    setTimeout(() => {
+        const vidElem = document.querySelector(videoSelector);
+        if (vidElem)
+            vidElem.volume = getFeatures().lockVolumeLevel / 100;
+        if (!sliderElem) {
+            volumeSliderObserverActive = true;
+            return;
+        }
+        sliderElem.value = String(getFeatures().lockVolumeLevel);
+        sliderElem.setAttribute("aria-valuenow", String(getFeatures().lockVolumeLevel));
+        const knobElem = document.querySelector("#volume-slider #sliderKnobContainer #sliderKnob");
+        if (knobElem)
+            knobElem.style.left = `${getFeatures().lockVolumeLevel}%`;
+        const labelElem = document.querySelector("#bytm-vol-slider-label .label");
+        const newLabelContent = `${getFeatures().lockVolumeLevel}%`;
+        if (labelElem && labelElem.textContent !== newLabelContent)
+            labelElem.textContent = newLabelContent;
+        volumeSliderObserverActive = true;
+    }, 10);
+}
+/** Locks the volume slider at a specific level */
+function enableLockVolume() {
+    return __awaiter(this, void 0, void 0, function* () {
+        const observer = new MutationObserver((mutations) => {
+            for (const mutation of mutations) {
+                if (!volumeSliderObserverActive)
+                    return;
+                if (mutation.target.id === "sliderBar" && mutation.type === "attributes") {
+                    if (mutation.attributeName === "value" || mutation.attributeName === "aria-valuenow")
+                        overrideVolValues();
+                }
+            }
+        });
+        onSelectorOld("#volume-slider tp-yt-paper-progress#sliderBar", {
+            listener: (elem) => {
+                sliderElem = elem;
+                overrideVolValues();
+                volumeSliderObserverActive = true;
+                observer.observe(elem, {
+                    attributeFilter: ["value", "aria-valuenow"],
+                });
+            }
+        });
+    });
 }const inputIgnoreTagNames = ["INPUT", "TEXTAREA", "SELECT", "BUTTON", "A"];
 let features$1;
 function setInputConfig(feats) {
@@ -3949,7 +3936,7 @@ const localeOptions = Object.entries(locales).reduce((a, [locale, { name }]) =>
     .sort((a, b) => a.label.localeCompare(b.label));
 /** Decoration elements that can be added next to the label */
 const adornments = {
-    advancedMode: () => __awaiter(void 0, void 0, void 0, function* () { var _a; return `<span class="bytm-advanced-mode-icon bytm-adorn-icon" title="${t("advanced_mode")}">${(_a = yield resourceToHTMLString("icon-advanced_mode")) !== null && _a !== void 0 ? _a : ""}</span>`; }),
+    advanced: () => __awaiter(void 0, void 0, void 0, function* () { var _a; return `<span class="bytm-advanced-mode-icon bytm-adorn-icon" title="${t("advanced_mode")}">${(_a = yield resourceToHTMLString("icon-advanced_mode")) !== null && _a !== void 0 ? _a : ""}</span>`; }),
     experimental: () => __awaiter(void 0, void 0, void 0, function* () { var _b; return `<span class="bytm-experimental-icon bytm-adorn-icon" title="${t("experimental_feature")}">${(_b = yield resourceToHTMLString("icon-experimental")) !== null && _b !== void 0 ? _b : ""}</span>`; }),
     globe: () => __awaiter(void 0, void 0, void 0, function* () { var _c; return (_c = yield resourceToHTMLString("icon-globe")) !== null && _c !== void 0 ? _c : ""; }),
 };
@@ -3973,7 +3960,7 @@ const adornments = {
  * | `click: () => void`                               | for type `button` only - function that will be called when the button is clicked |
  * | `helpText: string / () => string`                 | function that returns an HTML string or the literal string itself that will be the help text for this feature - writing as function is useful for pluralizing or inserting values into the translation at runtime - if not set, translation with key `feature_helptext_featureKey` will be used instead, if available |
  * | `textAdornment: () => string / Promise<string>`   | function that returns an HTML string that will be appended to the text in the config menu as an adornment element - TODO: to be replaced in the big menu rework |
- * | `unit: string / (val: number) => string`          | Only if type is `number` or `slider` - The unit text that is displayed next to the input element, i.e. "px" |
+ * | `unit: string / (val: number) => string`          | Only if type is `number` or `slider` - The unit text that is displayed next to the input element, i.e. " px" - a leading space need to be added by hand! |
  * | `min: number`                                     | Only if type is `number` or `slider` - Overwrites the default of the `min` property of the HTML input element |
  * | `max: number`                                     | Only if type is `number` or `slider` - Overwrites the default of the `max` property of the HTML input element |
  * | `step: number`                                    | Only if type is `number` or `slider` - Overwrites the default of the `step` property of the HTML input element |
@@ -3988,12 +3975,6 @@ const adornments = {
  */
 const featInfo = {
     //#SECTION layout
-    removeUpgradeTab: {
-        type: "toggle",
-        category: "layout",
-        default: true,
-        enable: noopTODO,
-    },
     volumeSliderLabel: {
         type: "toggle",
         category: "layout",
@@ -4060,6 +4041,12 @@ const featInfo = {
         enable: noopTODO,
         disable: noopTODO,
     },
+    removeUpgradeTab: {
+        type: "toggle",
+        category: "layout",
+        default: true,
+        enable: noopTODO,
+    },
     //#SECTION song lists
     lyricsQueueButton: {
         type: "toggle",
@@ -4110,7 +4097,7 @@ const featInfo = {
         default: true,
         enable: noopTODO,
         disable: noopTODO, // TODO: feasible?
-        helpText: () => tp("feature_helptext_rememberSongTime", remSongMinPlayTime, remSongMinPlayTime)
+        helpText: () => tp("feature_helptext_rememberSongTime", getFeatures().rememberSongTimeMinPlayTime, getFeatures().rememberSongTimeMinPlayTime)
     },
     rememberSongTimeSites: {
         type: "select",
@@ -4124,6 +4111,48 @@ const featInfo = {
         enable: noopTODO,
         change: noopTODO,
     },
+    rememberSongTimeDuration: {
+        type: "number",
+        category: "behavior",
+        min: 3,
+        max: 60 * 60 * 24 * 7,
+        step: 1,
+        default: 60,
+        unit: "s",
+        enable: noopTODO,
+        change: noopTODO,
+        advanced: true,
+        // TODO: to be reworked or removed in the big menu rework
+        textAdornment: adornments.advanced,
+    },
+    rememberSongTimeReduction: {
+        type: "number",
+        category: "behavior",
+        min: 0,
+        max: 30,
+        step: 0.1,
+        default: 0,
+        unit: "s",
+        enable: noopTODO,
+        change: noopTODO,
+        advanced: true,
+        // TODO: to be reworked or removed in the big menu rework
+        textAdornment: adornments.advanced,
+    },
+    rememberSongTimeMinPlayTime: {
+        type: "slider",
+        category: "behavior",
+        min: 1,
+        max: 30,
+        step: 0.5,
+        default: 10,
+        unit: "s",
+        enable: noopTODO,
+        change: noopTODO,
+        advanced: true,
+        // TODO: to be reworked or removed in the big menu rework
+        textAdornment: adornments.advanced,
+    },
     lockVolume: {
         type: "toggle",
         category: "behavior",
@@ -4208,7 +4237,7 @@ const featInfo = {
         normalize: (val) => val.trim().replace(/\/+$/, ""),
         advanced: true,
         // TODO: to be reworked or removed in the big menu rework
-        textAdornment: adornments.advancedMode,
+        textAdornment: adornments.advanced,
     },
     geniUrlToken: {
         type: "text",
@@ -4218,7 +4247,7 @@ const featInfo = {
         normalize: (val) => val.trim(),
         advanced: true,
         // TODO: to be reworked or removed in the big menu rework
-        textAdornment: adornments.advancedMode,
+        textAdornment: adornments.advanced,
     },
     lyricsCacheMaxSize: {
         type: "slider",
@@ -4232,7 +4261,7 @@ const featInfo = {
         change: noopTODO,
         advanced: true,
         // TODO: to be reworked or removed in the big menu rework
-        textAdornment: adornments.advancedMode,
+        textAdornment: adornments.advanced,
     },
     lyricsCacheTTL: {
         type: "slider",
@@ -4246,7 +4275,7 @@ const featInfo = {
         change: noopTODO,
         advanced: true,
         // TODO: to be reworked or removed in the big menu rework
-        textAdornment: adornments.advancedMode,
+        textAdornment: adornments.advanced,
     },
     clearLyricsCache: {
         type: "button",
@@ -4261,7 +4290,7 @@ const featInfo = {
         },
         advanced: true,
         // TODO: to be reworked or removed in the big menu rework
-        textAdornment: adornments.advancedMode,
+        textAdornment: adornments.advanced,
     },
     advancedLyricsFilter: {
         type: "toggle",
@@ -4315,7 +4344,7 @@ const featInfo = {
         enable: noopTODO,
         disable: noopTODO,
         // TODO: to be reworked or removed in the big menu rework
-        textAdornment: () => getFeatures().advancedMode ? adornments.advancedMode() : undefined,
+        textAdornment: () => getFeatures().advancedMode ? adornments.advanced() : undefined,
     },
 };
 function noop() {
@@ -4357,6 +4386,8 @@ const migrations = {
         "clearLyricsCache", "advancedMode",
         "lockVolume", "lockVolumeLevel",
         "checkVersionNow", "advancedLyricsFilter",
+        "rememberSongTimeDuration", "rememberSongTimeReduction",
+        "rememberSongTimeMinPlayTime",
     ], oldData),
 };
 /** Uses the passed {@linkcode oldData} as the base (if given) and sets all passed {@linkcode keys} to their feature default - returns a copy of the object */
@@ -4959,7 +4990,7 @@ function showWelcomeMenu() {
     const styleGradient = "background: rgba(165, 38, 38, 1); background: linear-gradient(90deg, rgb(154, 31, 103) 0%, rgb(135, 31, 31) 40%, rgb(184, 64, 41) 100%);";
     const styleCommon = "color: #fff; font-size: 1.5em; padding-left: 6px; padding-right: 6px;";
     console.log();
-    console.log(`%c${scriptInfo.name}%cv${scriptInfo.version}%c\n\nBuild ${buildNumber} ─ ${scriptInfo.namespace}`, `font-weight: bold; ${styleCommon} ${styleGradient}`, `background-color: #333; ${styleCommon}`, "padding: initial;");
+    console.log(`%c${scriptInfo.name}%cv${scriptInfo.version}%c\n\nBuild #${buildNumber} ─ ${scriptInfo.namespace}`, `font-weight: bold; ${styleCommon} ${styleGradient}`, `background-color: #333; ${styleCommon}`, "padding: initial;");
     console.log([
         "Powered by:",
         "─ Lots of ambition and dedication",
@@ -4990,12 +5021,12 @@ function init() {
                 domLoaded = true;
             });
             const features = yield initConfig();
+            setLogLevel(features.logLevel);
             yield initLyricsCache();
             yield initTranslations((_a = features.locale) !== null && _a !== void 0 ? _a : "en_US");
             setLocale((_b = features.locale) !== null && _b !== void 0 ? _b : "en_US");
-            setLogLevel(features.logLevel);
+            // TODO(v1.2): remove these
             setLayoutConfig(features);
-            setBehaviorConfig(features);
             setInputConfig(features);
             setSongListsConfig(features);
             if (features.disableBeforeUnloadPopup && domain === "ytm")
@@ -5792,11 +5823,11 @@ hr {
 }
 
 #bytm-menu-subtitle-cont {
-  width: auto;
+  width: 100%;
   display: flex;
   flex-direction: row;
   justify-content: space-between;
-  align-items: flex-end;
+  align-items: end;
   position: absolute;
   bottom: -12px;
   left: 0;
@@ -5809,21 +5840,7 @@ hr {
   color: var(--bytm-menu-subtitle-color);
 }
 
-#bytm-menu-subtitle-cont .bytm-advanced-mode-indicator-label {
-  display: inline-block;
-  transform: translate(0px, 1px);
-  color: var(--bytm-menu-subtitle-color);
-}
-
-#bytm-menu-subtitle-cont .bytm-advanced-mode-icon svg {
-  width: 19px;
-  height: 19px;
-  transform: translate(1px, 7px);
-}
-
-#bytm-menu-version-anchor,
-#bytm-cfg-menu-advanced-mode-indicator-wrapper
-{
+#bytm-menu-version-anchor {
   overflow: hidden;
   text-wrap: nowrap;
   text-overflow: ellipsis;