Browse Source

chore: build preview

Sv443 7 months ago
parent
commit
25da1d9a5b
1 changed files with 428 additions and 411 deletions
  1. 428 411
      dist/BetterYTM.user.js

+ 428 - 411
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/2feaf6db/assets/images/logo/logo_dev_48.png
+// @icon              https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/images/logo/logo_dev_48.png
 // @match             https://music.youtube.com/*
 // @match             https://www.youtube.com/*
 // @run-at            document-start
@@ -33,56 +33,56 @@
 // @grant             GM.openInTab
 // @grant             unsafeWindow
 // @noframes
-// @resource          css-above_queue_btns       https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/style/aboveQueueBtns.css
-// @resource          css-anchor_improvements    https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/style/anchorImprovements.css
-// @resource          css-auto_like              https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/style/autoLike.css
-// @resource          css-bundle                 https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/dist/BetterYTM.css
-// @resource          css-fix_hdr                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/style/fixHDR.css
-// @resource          css-fix_playerpage_theming https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/style/fixPlayerPageTheming.css
-// @resource          css-fix_spacing            https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/style/fixSpacing.css
-// @resource          css-fix_sponsorblock       https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/style/fixSponsorBlock.css
-// @resource          css-show_votes             https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/style/showVotes.css
-// @resource          css-vol_slider_size        https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/style/volSliderSize.css
-// @resource          doc-changelog              https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/changelog.md
-// @resource          icon-advanced_mode         https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/plus_circle_small.svg
-// @resource          icon-alert                 https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/alert.svg
-// @resource          icon-arrow_down            https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/arrow_down.svg
-// @resource          icon-auto_like             https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/auto_like.svg
-// @resource          icon-auto_like_enabled     https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/auto_like_enabled.svg
-// @resource          icon-clear_list            https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/clear_list.svg
-// @resource          icon-confirm               https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/help.svg
-// @resource          icon-copy                  https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/copy.svg
-// @resource          icon-delete                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/delete.svg
-// @resource          icon-edit                  https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/edit.svg
-// @resource          icon-error                 https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/error.svg
-// @resource          icon-experimental          https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/beaker_small.svg
-// @resource          icon-globe                 https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/globe.svg
-// @resource          icon-globe_small           https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/globe_small.svg
-// @resource          icon-help                  https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/help.svg
-// @resource          icon-image                 https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/image.svg
-// @resource          icon-image_filled          https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/image_filled.svg
-// @resource          icon-link                  https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/link.svg
-// @resource          icon-lyrics                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/lyrics.svg
-// @resource          icon-reload                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/refresh.svg
-// @resource          icon-skip_to               https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/skip_to.svg
-// @resource          icon-spinner               https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/spinner.svg
-// @resource          icon-upload                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/icons/upload.svg
-// @resource          img-close                  https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/images/close.png
-// @resource          img-discord                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/images/external/discord.png
-// @resource          img-github                 https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/images/external/github.png
-// @resource          img-greasyfork             https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/images/external/greasyfork.png
-// @resource          img-logo                   https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/images/logo/logo_48.png
-// @resource          img-logo_dev               https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/images/logo/logo_dev_48.png
-// @resource          img-openuserjs             https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/images/external/openuserjs.png
-// @resource          trans-de_DE                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/translations/de_DE.json
-// @resource          trans-en_UK                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/translations/en_UK.json
-// @resource          trans-en_US                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/translations/en_US.json
-// @resource          trans-es_ES                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/translations/es_ES.json
-// @resource          trans-fr_FR                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/translations/fr_FR.json
-// @resource          trans-hi_IN                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/translations/hi_IN.json
-// @resource          trans-ja_JA                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/translations/ja_JA.json
-// @resource          trans-pt_BR                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/translations/pt_BR.json
-// @resource          trans-zh_CN                https://raw.githubusercontent.com/Sv443/BetterYTM/2feaf6db/assets/translations/zh_CN.json
+// @resource          css-above_queue_btns       https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/style/aboveQueueBtns.css
+// @resource          css-anchor_improvements    https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/style/anchorImprovements.css
+// @resource          css-auto_like              https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/style/autoLike.css
+// @resource          css-bundle                 https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/dist/BetterYTM.css
+// @resource          css-fix_hdr                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/style/fixHDR.css
+// @resource          css-fix_playerpage_theming https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/style/fixPlayerPageTheming.css
+// @resource          css-fix_spacing            https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/style/fixSpacing.css
+// @resource          css-fix_sponsorblock       https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/style/fixSponsorBlock.css
+// @resource          css-show_votes             https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/style/showVotes.css
+// @resource          css-vol_slider_size        https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/style/volSliderSize.css
+// @resource          doc-changelog              https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/changelog.md
+// @resource          icon-advanced_mode         https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/plus_circle_small.svg
+// @resource          icon-alert                 https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/alert.svg
+// @resource          icon-arrow_down            https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/arrow_down.svg
+// @resource          icon-auto_like             https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/auto_like.svg
+// @resource          icon-auto_like_enabled     https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/auto_like_enabled.svg
+// @resource          icon-clear_list            https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/clear_list.svg
+// @resource          icon-confirm               https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/help.svg
+// @resource          icon-copy                  https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/copy.svg
+// @resource          icon-delete                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/delete.svg
+// @resource          icon-edit                  https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/edit.svg
+// @resource          icon-error                 https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/error.svg
+// @resource          icon-experimental          https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/beaker_small.svg
+// @resource          icon-globe                 https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/globe.svg
+// @resource          icon-globe_small           https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/globe_small.svg
+// @resource          icon-help                  https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/help.svg
+// @resource          icon-image                 https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/image.svg
+// @resource          icon-image_filled          https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/image_filled.svg
+// @resource          icon-link                  https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/link.svg
+// @resource          icon-lyrics                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/lyrics.svg
+// @resource          icon-reload                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/refresh.svg
+// @resource          icon-skip_to               https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/skip_to.svg
+// @resource          icon-spinner               https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/spinner.svg
+// @resource          icon-upload                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/icons/upload.svg
+// @resource          img-close                  https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/images/close.png
+// @resource          img-discord                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/images/external/discord.png
+// @resource          img-github                 https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/images/external/github.png
+// @resource          img-greasyfork             https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/images/external/greasyfork.png
+// @resource          img-logo                   https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/images/logo/logo_48.png
+// @resource          img-logo_dev               https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/images/logo/logo_dev_48.png
+// @resource          img-openuserjs             https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/images/external/openuserjs.png
+// @resource          trans-de_DE                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/translations/de_DE.json
+// @resource          trans-en_UK                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/translations/en_UK.json
+// @resource          trans-en_US                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/translations/en_US.json
+// @resource          trans-es_ES                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/translations/es_ES.json
+// @resource          trans-fr_FR                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/translations/fr_FR.json
+// @resource          trans-hi_IN                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/translations/hi_IN.json
+// @resource          trans-ja_JA                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/translations/ja_JA.json
+// @resource          trans-pt_BR                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/translations/pt_BR.json
+// @resource          trans-zh_CN                https://raw.githubusercontent.com/Sv443/BetterYTM/ff15e1e3/assets/translations/zh_CN.json
 // @require           https://cdn.jsdelivr.net/npm/@sv443-network/[email protected]/dist/index.global.js
 // @require           https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.js
 // @require           https://cdn.jsdelivr.net/npm/[email protected]/lib/umd/index.js
@@ -134,7 +134,7 @@ var PluginIntent;
 const modeRaw = "development";
 const branchRaw = "develop";
 const hostRaw = "github";
-const buildNumberRaw = "2feaf6db";
+const buildNumberRaw = "ff15e1e3";
 /** 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 */
@@ -607,6 +607,7 @@ class BytmDialog extends UserUtils.NanoEmitter {
         if (footer) {
             const footerWrapper = document.createElement("div");
             footerWrapper.classList.add("bytm-dialog-footer-cont");
+            this.options.small && footerWrapper.classList.add("small");
             dialogWrapperEl.appendChild(footerWrapper);
             footerWrapper.appendChild(footer instanceof Promise ? await footer : footer);
         }
@@ -1200,6 +1201,346 @@ async function createLongBtn(_a) {
     btnElem.appendChild(txtElem);
     iconPosition === "right" && btnElem.appendChild(imgElem);
     return ripple ? createRipple(btnElem, { speed: "normal" }) : btnElem;
+}var de_DE = {
+	name: "Deutsch (Deutschland)",
+	nameEnglish: "German",
+	emoji: "🇩🇪",
+	userscriptDesc: "Konfigurierbare Layout- und Benutzererfahrungs-Verbesserungen für YouTube Music™ und YouTube™",
+	authors: [
+		"Sv443"
+	]
+};
+var en_US = {
+	name: "English (United States)",
+	nameEnglish: "English (US)",
+	emoji: "🇺🇸",
+	userscriptDesc: "Configurable layout and user experience improvements for YouTube Music™ and YouTube™",
+	authors: [
+		"Sv443"
+	]
+};
+var en_UK = {
+	name: "English (United Kingdom)",
+	nameEnglish: "English (UK)",
+	emoji: "🇬🇧",
+	userscriptDesc: "Configurable layout and user experience improvements for YouTube Music™ and YouTube™",
+	authors: [
+		"Sv443"
+	]
+};
+var es_ES = {
+	name: "Español (España)",
+	nameEnglish: "Spanish",
+	emoji: "🇪🇸",
+	userscriptDesc: "Mejoras de diseño y experiencia de usuario configurables para YouTube Music™ y YouTube™",
+	authors: [
+		"Sv443"
+	]
+};
+var fr_FR = {
+	name: "Français (France)",
+	nameEnglish: "French",
+	emoji: "🇫🇷",
+	userscriptDesc: "Améliorations de la mise en page et de l'expérience utilisateur configurables pour YouTube Music™ et YouTube™",
+	authors: [
+		"Sv443"
+	]
+};
+var hi_IN = {
+	name: "हिंदी (भारत)",
+	nameEnglish: "Hindi",
+	emoji: "🇮🇳",
+	userscriptDesc: "YouTube Music™ और YouTube™ के लिए कॉन्फ़िगर करने योग्य लेआउट और उपयोगकर्ता अनुभव में सुधार",
+	authors: [
+		"Sv443"
+	]
+};
+var ja_JA = {
+	name: "日本語 (日本)",
+	nameEnglish: "Japanese",
+	emoji: "🇯🇵",
+	userscriptDesc: "YouTube Music™ と YouTube™ の構成可能なレイアウトとユーザー エクスペリエンスの向上",
+	authors: [
+		"Sv443"
+	]
+};
+var pt_BR = {
+	name: "Português (Brasil)",
+	nameEnglish: "Portuguese",
+	emoji: "🇵🇹",
+	userscriptDesc: "Melhorias configuráveis no layout e na experiência do usuário para o YouTube Music™ e o YouTube™",
+	authors: [
+		"Sv443"
+	]
+};
+var zh_CN = {
+	name: "中文(简化,中国)",
+	nameEnglish: "Chinese (simpl.)",
+	emoji: "🇨🇳",
+	userscriptDesc: "YouTube Music™ 和 YouTube™ 的可配置布局和用户体验改进",
+	authors: [
+		"Sv443"
+	]
+};
+var langMapping = {
+	de_DE: de_DE,
+	en_US: en_US,
+	en_UK: en_UK,
+	es_ES: es_ES,
+	fr_FR: fr_FR,
+	hi_IN: hi_IN,
+	ja_JA: ja_JA,
+	pt_BR: pt_BR,
+	zh_CN: zh_CN
+};//#region misc
+let domain;
+/**
+ * Returns the current domain as a constant string representation
+ * @throws Throws if script runs on an unexpected website
+ */
+function getDomain() {
+    if (domain)
+        return domain;
+    if (location.hostname.match(/^music\.youtube/))
+        return domain = "ytm";
+    else if (location.hostname.match(/youtube\./))
+        return domain = "yt";
+    else
+        throw new Error("BetterYTM is running on an unexpected website. Please don't tamper with the @match directives in the userscript header.");
+}
+/** Returns a pseudo-random ID unique to each session - returns null if sessionStorage is unavailable */
+function getSessionId() {
+    try {
+        if (!sessionStorageAvailable)
+            throw new Error("Session storage unavailable");
+        let sesId = window.sessionStorage.getItem("_bytm-session-id");
+        if (!sesId)
+            window.sessionStorage.setItem("_bytm-session-id", sesId = UserUtils.randomId(8, 36));
+        return sesId;
+    }
+    catch (err) {
+        warn("Couldn't get session ID, sessionStorage / cookies might be disabled:", err);
+        return null;
+    }
+}
+let isCompressionSupported;
+/** Tests whether compression via the predefined {@linkcode compressionFormat} is supported (only on the first call, then returns the cached result) */
+async function compressionSupported() {
+    if (typeof isCompressionSupported === "boolean")
+        return isCompressionSupported;
+    try {
+        await UserUtils.compress(".", compressionFormat, "string");
+        return isCompressionSupported = true;
+    }
+    catch (_a) {
+        return isCompressionSupported = false;
+    }
+}
+/** Returns a string with the given array's items separated by a default separator (`", "` by default), with an optional different separator for the last item */
+function arrayWithSeparators(array, separator = ", ", lastSeparator) {
+    const arr = [...array];
+    if (arr.length === 0)
+        return "";
+    else if (arr.length <= 2)
+        return arr.join(lastSeparator);
+    else
+        return `${arr.slice(0, -1).join(separator)}${lastSeparator}${arr.at(-1)}`;
+}
+/** Returns the watch ID of the current video or null if not on a video page */
+function getWatchId() {
+    const { searchParams, pathname } = new URL(location.href);
+    return pathname.includes("/watch") ? searchParams.get("v") : null;
+}
+/**
+ * Returns the ID of the current channel in the format `@User` or `UC...` from URLs with the path `/@User`, `/@User/videos`, `/channel/UC...` or `/channel/UC.../videos`
+ * Returns null if the current page is not a channel page or there was an error parsing the URL
+ */
+function getCurrentChannelId() {
+    return parseChannelIdFromUrl(location.href);
+}
+/** Returns the channel ID from a URL or null if the URL is invalid */
+function parseChannelIdFromUrl(url) {
+    try {
+        const { pathname } = url instanceof URL ? url : new URL(url);
+        if (pathname.includes("/channel/"))
+            return sanitizeChannelId(pathname.split("/channel/")[1].split("/")[0]);
+        else if (pathname.includes("/@"))
+            return sanitizeChannelId(pathname.split("/@")[1].split("/")[0]);
+        else
+            return null;
+    }
+    catch (_a) {
+        return null;
+    }
+}
+/** Sanitizes a channel ID by adding a leading `@` if the ID doesn't start with `UC...` */
+function sanitizeChannelId(channelId) {
+    channelId = String(channelId).trim();
+    return isValidChannelId(channelId)
+        ? channelId
+        : `@${channelId}`;
+}
+/** Tests whether a string is a valid channel ID in the format `@User` or `.C...` */
+function isValidChannelId(channelId) {
+    return channelId.match(/^([A-Z]C|@)\w+$/) !== null;
+}
+/** Returns the thumbnail URL for a video with either a given quality identifier or index */
+function getThumbnailUrl(watchId, qualityOrIndex = "maxresdefault") {
+    return `https://img.youtube.com/vi/${watchId}/${qualityOrIndex}.jpg`;
+}
+/** Returns the best available thumbnail URL for a video with the given watch ID */
+async function getBestThumbnailUrl(watchId) {
+    try {
+        const priorityList = ["maxresdefault", "sddefault", "hqdefault", 0];
+        for (const quality of priorityList) {
+            let response;
+            const url = getThumbnailUrl(watchId, quality);
+            try {
+                response = await sendRequest({ url, method: "HEAD", timeout: 6000 });
+            }
+            catch (err) {
+                error(`Error while sending HEAD request to thumbnail URL for video '${watchId}' with quality '${quality}':`, err);
+                void err;
+            }
+            if (response && response.status < 300 && response.status >= 200)
+                return url;
+        }
+    }
+    catch (err) {
+        throw new Error(`Couldn't get thumbnail URL for video '${watchId}': ${err}`);
+    }
+}
+/** Opens the given URL in a new tab, using GM.openInTab if available */
+function openInTab(href, background = false) {
+    try {
+        UserUtils.openInNewTab(href, background);
+    }
+    catch (_a) {
+        window.open(href, "_blank", "noopener noreferrer");
+    }
+}
+/** Tries to parse an uncompressed or compressed input string as a JSON object */
+async function tryToDecompressAndParse(input) {
+    let parsed = null;
+    try {
+        parsed = JSON.parse(input);
+    }
+    catch (_a) {
+        try {
+            parsed = JSON.parse(await UserUtils.decompress(input, compressionFormat, "string"));
+        }
+        catch (err) {
+            error("Couldn't decompress and parse data due to an error:", err);
+            return null;
+        }
+    }
+    // artificial timeout to allow animations to finish and because dumb monkey brains *expect* a delay
+    await UserUtils.pauseFor(250);
+    return parsed;
+}
+/** Very crude OS detection */
+function getOS() {
+    if (navigator.userAgent.match(/mac(\s?os|intel)/i))
+        return "mac";
+    return "other";
+}
+/** Turns the passed StringGen (either a string, stringifiable object or a sync or async function returning a string or stringifiable object) into a string */
+async function consumeStringGen(strGen) {
+    return String(typeof strGen === "function" ? await strGen() : strGen);
+}
+//#region resources
+/**
+ * Returns the URL of a resource by its name, as defined in `assets/resources.json`, from GM resource cache - [see GM.getResourceUrl docs](https://wiki.greasespot.net/GM.getResourceUrl)
+ * Falls back to a `raw.githubusercontent.com` URL or base64-encoded data URI if the resource is not available in the GM resource cache
+ */
+async function getResourceUrl(name) {
+    var _a;
+    let url = await GM.getResourceUrl(name);
+    if (!url || url.length === 0) {
+        const resource = (_a = GM.info.script.resources) === null || _a === void 0 ? void 0 : _a[name].url;
+        if (typeof resource === "string") {
+            const resourceUrl = new URL(resource);
+            const resourcePath = resourceUrl.pathname;
+            if (resourcePath)
+                return `https://raw.githubusercontent.com/${repo}/${branch}${resourcePath}`;
+        }
+        warn(`Couldn't get blob URL nor external URL for @resource '${name}', trying to use base64-encoded fallback`);
+        // @ts-ignore
+        url = await GM.getResourceUrl(name, false);
+    }
+    return url;
+}
+/**
+ * Returns the preferred locale of the user, provided it is supported by the userscript.
+ * Prioritizes `navigator.language`, then `navigator.languages`, then `"en_US"` as a fallback.
+ */
+function getPreferredLocale() {
+    var _a;
+    const navLang = navigator.language.replace(/-/g, "_");
+    const navLangs = navigator.languages
+        .filter(lang => lang.match(/^[a-z]{2}(-|_)[A-Z]$/) !== null)
+        .map(lang => lang.replace(/-/g, "_"));
+    if (Object.entries(langMapping).find(([key]) => key === navLang))
+        return navLang;
+    for (const loc of navLangs) {
+        if (Object.entries(langMapping).find(([key]) => key === loc))
+            return loc;
+    }
+    // if navigator.languages has entries that aren't locale codes in the format xx_XX
+    if (navigator.languages.some(lang => lang.match(/^[a-z]{2}$/))) {
+        for (const lang of navLangs) {
+            const foundLoc = (_a = Object.entries(langMapping).find(([key]) => key.startsWith(lang))) === null || _a === void 0 ? void 0 : _a[0];
+            if (foundLoc)
+                return foundLoc;
+        }
+    }
+    return "en_US";
+}
+/** Returns the content behind the passed resource identifier as a string, for example to be assigned to an element's innerHTML property */
+async function resourceAsString(resource) {
+    try {
+        const resourceUrl = await getResourceUrl(resource);
+        if (!resourceUrl)
+            throw new Error(`Couldn't find URL for resource '${resource}'`);
+        return await (await UserUtils.fetchAdvanced(resourceUrl)).text();
+    }
+    catch (err) {
+        error("Couldn't get SVG element from resource:", err);
+        return null;
+    }
+}
+/** Parses a markdown string using marked and turns it into an HTML string with default settings - doesn't sanitize against XSS! */
+function parseMarkdown(mdString) {
+    return marked.marked.parse(mdString, {
+        async: true,
+        gfm: true,
+    });
+}
+/** Returns the content of the changelog markdown file */
+async function getChangelogMd() {
+    return await (await UserUtils.fetchAdvanced(await getResourceUrl("doc-changelog"))).text();
+}
+/** Returns the changelog as HTML with a details element for each version */
+async function getChangelogHtmlWithDetails() {
+    try {
+        const changelogMd = await getChangelogMd();
+        let changelogHtml = await parseMarkdown(changelogMd);
+        const getVerId = (verStr) => verStr.trim().replace(/[._#\s-]/g, "");
+        changelogHtml = changelogHtml.replace(/<div\s+class="split">\s*<\/div>\s*\n?\s*<br(\s\/)?>/gm, "</details>\n<br>\n<details class=\"bytm-changelog-version-details\" tabindex=\"0\">");
+        const h2Matches = Array.from(changelogHtml.matchAll(/<h2(\s+id=".+")?>([\d\w\s.]+)<\/h2>/gm));
+        for (const match of h2Matches) {
+            const [fullMatch, , verStr] = match;
+            const verId = getVerId(verStr);
+            const h2Elem = `<h2 id="${verId}" role="subheading" aria-level="1">${verStr}</h2>`;
+            const summaryElem = `<summary tab-index="0">${h2Elem}</summary>`;
+            changelogHtml = changelogHtml.replace(fullMatch, `${summaryElem}`);
+        }
+        changelogHtml = `<details class="bytm-changelog-version-details" tabindex="0">${changelogHtml}</details>`;
+        return changelogHtml;
+    }
+    catch (err) {
+        return `Error while preparing changelog: ${err}`;
+    }
 }class MarkdownDialog extends BytmDialog {
     constructor(options) {
         super(Object.assign(Object.assign({}, options), { id: `md-${options.id}`, renderBody: () => this.renderBody() }));
@@ -1225,7 +1566,7 @@ async function createLongBtn(_a) {
         bodyEl.classList.add("bytm-md-dialog-body");
         const mdCont = typeof this.opts.body === "string"
             ? this.opts.body
-            : await this.opts.body();
+            : await consumeStringGen(this.opts.body);
         const markdownEl = document.createElement("div");
         markdownEl.classList.add("bytm-markdown-dialog-content", "bytm-markdown-container");
         markdownEl.tabIndex = 0;
@@ -1403,6 +1744,7 @@ class PromptDialog extends BytmDialog {
             small: true,
             renderHeader: () => this.renderHeader(props),
             renderBody: () => this.renderBody(props),
+            renderFooter: () => this.renderFooter(props),
         });
     }
     async renderHeader({ type }) {
@@ -1416,8 +1758,8 @@ class PromptDialog extends BytmDialog {
     async renderBody(_a) {
         var _b;
         var { type, message } = _a, rest = __rest(_a, ["type", "message"]);
-        const resolve = (val) => this.events.emit("resolve", val);
         const contElem = document.createElement("div");
+        contElem.classList.add(`bytm-prompt-type-${type}`);
         const upperContElem = document.createElement("div");
         upperContElem.id = "bytm-prompt-dialog-upper-cont";
         contElem.appendChild(upperContElem);
@@ -1437,6 +1779,11 @@ class PromptDialog extends BytmDialog {
             inputElem.autofocus = true;
             upperContElem.appendChild(inputElem);
         }
+        return contElem;
+    }
+    async renderFooter(_a) {
+        var { type } = _a, rest = __rest(_a, ["type"]);
+        const resolve = (val) => this.events.emit("resolve", val);
         const buttonsWrapper = document.createElement("div");
         buttonsWrapper.id = "bytm-prompt-dialog-button-wrapper";
         const buttonsCont = document.createElement("div");
@@ -1446,8 +1793,8 @@ class PromptDialog extends BytmDialog {
             confirmBtn = document.createElement("button");
             confirmBtn.id = "bytm-prompt-dialog-confirm";
             confirmBtn.classList.add("bytm-prompt-dialog-button");
-            confirmBtn.textContent = t("prompt_confirm");
-            confirmBtn.ariaLabel = confirmBtn.title = t("click_to_confirm_tooltip");
+            confirmBtn.textContent = await this.consumePromptStringGen(type, rest.confirmBtnText, t("prompt_confirm"));
+            confirmBtn.ariaLabel = confirmBtn.title = await this.consumePromptStringGen(type, rest.confirmBtnTooltip, t("click_to_confirm_tooltip"));
             confirmBtn.tabIndex = 0;
             confirmBtn.autofocus = type === "confirm";
             confirmBtn.addEventListener("click", () => {
@@ -1459,8 +1806,8 @@ class PromptDialog extends BytmDialog {
         const closeBtn = document.createElement("button");
         closeBtn.id = "bytm-prompt-dialog-close";
         closeBtn.classList.add("bytm-prompt-dialog-button");
-        closeBtn.textContent = t(type === "alert" ? "prompt_close" : "prompt_cancel");
-        closeBtn.ariaLabel = closeBtn.title = t(type === "alert" ? "click_to_close_tooltip" : "click_to_cancel_tooltip");
+        closeBtn.textContent = await this.consumePromptStringGen(type, rest.denyBtnText, t(type === "alert" ? "prompt_close" : "prompt_cancel"));
+        closeBtn.ariaLabel = closeBtn.title = await this.consumePromptStringGen(type, rest.denyBtnTooltip, t(type === "alert" ? "click_to_close_tooltip" : "click_to_cancel_tooltip"));
         closeBtn.tabIndex = 0;
         if (type === "alert")
             closeBtn.autofocus = true;
@@ -1477,8 +1824,12 @@ class PromptDialog extends BytmDialog {
         buttonsCont.appendChild(closeBtn);
         confirmBtn && getOS() === "mac" && buttonsCont.appendChild(confirmBtn);
         buttonsWrapper.appendChild(buttonsCont);
-        contElem.appendChild(buttonsWrapper);
-        return contElem;
+        return buttonsWrapper;
+    }
+    async consumePromptStringGen(type, txtGen, fallback) {
+        if (typeof txtGen === "function")
+            return await txtGen(type);
+        return String(txtGen !== null && txtGen !== void 0 ? txtGen : fallback);
     }
 }
 function showPrompt(_a) {
@@ -2367,7 +2718,7 @@ async function mountCfgMenu() {
     const featuresCont = document.createElement("div");
     featuresCont.id = "bytm-menu-opts";
     const onCfgChange = async (key, initialVal, newVal) => {
-        var _a, _b;
+        var _a, _b, _c, _d;
         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()));
@@ -2392,7 +2743,9 @@ async function mountCfgMenu() {
             await initTranslations(featConf.locale);
             setLocale(featConf.locale);
             const newText = t("lang_changed_prompt_reload");
-            const confirmText = newText !== initLangReloadText ? `${newText}\n\n────────────────────────────────\n\n${initLangReloadText}` : newText;
+            const newLangEmoji = ((_c = langMapping[featConf.locale]) === null || _c === void 0 ? void 0 : _c.emoji) ? `${langMapping[featConf.locale].emoji}\n` : "";
+            const initLangEmoji = initLocale && ((_d = langMapping[initLocale]) === null || _d === void 0 ? void 0 : _d.emoji) ? `${langMapping[initLocale].emoji}\n` : "";
+            const confirmText = newText !== initLangReloadText ? `${newLangEmoji}${newText}\n\n\n${initLangEmoji}${initLangReloadText}` : newText;
             if (await showPrompt({ type: "confirm", message: confirmText })) {
                 closeCfgMenu();
                 disableBeforeUnload();
@@ -2882,110 +3235,19 @@ function checkToggleScrollIndicator() {
     const scrollIndicator = document.querySelector("#bytm-menu-scroll-indicator");
     // disable scroll indicator if container doesn't scroll
     if (featuresCont && scrollIndicator) {
-        const verticalScroll = UserUtils.isScrollable(featuresCont).vertical;
-        /** If true, the indicator's threshold is under the available scrollable space and so it should be disabled */
-        const underThreshold = featuresCont.scrollHeight - featuresCont.clientHeight <= scrollIndicatorOffsetThreshold;
-        if (!underThreshold && verticalScroll && !scrollIndicatorEnabled) {
-            scrollIndicatorEnabled = true;
-            scrollIndicator.classList.remove("bytm-hidden");
-        }
-        if ((!verticalScroll && scrollIndicatorEnabled) || underThreshold) {
-            scrollIndicatorEnabled = false;
-            scrollIndicator.classList.add("bytm-hidden");
-        }
-    }
-}var de_DE = {
-	name: "Deutsch (Deutschland)",
-	nameEnglish: "German",
-	emoji: "🇩🇪",
-	userscriptDesc: "Konfigurierbare Layout- und Benutzererfahrungs-Verbesserungen für YouTube Music™ und YouTube™",
-	authors: [
-		"Sv443"
-	]
-};
-var en_US = {
-	name: "English (United States)",
-	nameEnglish: "English (US)",
-	emoji: "🇺🇸",
-	userscriptDesc: "Configurable layout and user experience improvements for YouTube Music™ and YouTube™",
-	authors: [
-		"Sv443"
-	]
-};
-var en_UK = {
-	name: "English (United Kingdom)",
-	nameEnglish: "English (UK)",
-	emoji: "🇬🇧",
-	userscriptDesc: "Configurable layout and user experience improvements for YouTube Music™ and YouTube™",
-	authors: [
-		"Sv443"
-	]
-};
-var es_ES = {
-	name: "Español (España)",
-	nameEnglish: "Spanish",
-	emoji: "🇪🇸",
-	userscriptDesc: "Mejoras de diseño y experiencia de usuario configurables para YouTube Music™ y YouTube™",
-	authors: [
-		"Sv443"
-	]
-};
-var fr_FR = {
-	name: "Français (France)",
-	nameEnglish: "French",
-	emoji: "🇫🇷",
-	userscriptDesc: "Améliorations de la mise en page et de l'expérience utilisateur configurables pour YouTube Music™ et YouTube™",
-	authors: [
-		"Sv443"
-	]
-};
-var hi_IN = {
-	name: "हिंदी (भारत)",
-	nameEnglish: "Hindi",
-	emoji: "🇮🇳",
-	userscriptDesc: "YouTube Music™ और YouTube™ के लिए कॉन्फ़िगर करने योग्य लेआउट और उपयोगकर्ता अनुभव में सुधार",
-	authors: [
-		"Sv443"
-	]
-};
-var ja_JA = {
-	name: "日本語 (日本)",
-	nameEnglish: "Japanese",
-	emoji: "🇯🇵",
-	userscriptDesc: "YouTube Music™ と YouTube™ の構成可能なレイアウトとユーザー エクスペリエンスの向上",
-	authors: [
-		"Sv443"
-	]
-};
-var pt_BR = {
-	name: "Português (Brasil)",
-	nameEnglish: "Portuguese",
-	emoji: "🇵🇹",
-	userscriptDesc: "Melhorias configuráveis no layout e na experiência do usuário para o YouTube Music™ e o YouTube™",
-	authors: [
-		"Sv443"
-	]
-};
-var zh_CN = {
-	name: "中文(简化,中国)",
-	nameEnglish: "Chinese (simpl.)",
-	emoji: "🇨🇳",
-	userscriptDesc: "YouTube Music™ 和 YouTube™ 的可配置布局和用户体验改进",
-	authors: [
-		"Sv443"
-	]
-};
-var langMapping = {
-	de_DE: de_DE,
-	en_US: en_US,
-	en_UK: en_UK,
-	es_ES: es_ES,
-	fr_FR: fr_FR,
-	hi_IN: hi_IN,
-	ja_JA: ja_JA,
-	pt_BR: pt_BR,
-	zh_CN: zh_CN
-};let welcomeDialog = null;
+        const verticalScroll = UserUtils.isScrollable(featuresCont).vertical;
+        /** If true, the indicator's threshold is under the available scrollable space and so it should be disabled */
+        const underThreshold = featuresCont.scrollHeight - featuresCont.clientHeight <= scrollIndicatorOffsetThreshold;
+        if (!underThreshold && verticalScroll && !scrollIndicatorEnabled) {
+            scrollIndicatorEnabled = true;
+            scrollIndicator.classList.remove("bytm-hidden");
+        }
+        if ((!verticalScroll && scrollIndicatorEnabled) || underThreshold) {
+            scrollIndicatorEnabled = false;
+            scrollIndicator.classList.add("bytm-hidden");
+        }
+    }
+}let welcomeDialog = null;
 /** Creates and/or returns the import dialog */
 async function getWelcomeDialog() {
     if (!welcomeDialog) {
@@ -4389,251 +4651,6 @@ async function addAutoLikeToggleBtn(siblingEl, channelId, channelName, extraClas
         if (imgEl && imgHtml)
             setInnerHtml(imgEl, imgHtml);
     });
-}//#region misc
-let domain;
-/**
- * Returns the current domain as a constant string representation
- * @throws Throws if script runs on an unexpected website
- */
-function getDomain() {
-    if (domain)
-        return domain;
-    if (location.hostname.match(/^music\.youtube/))
-        return domain = "ytm";
-    else if (location.hostname.match(/youtube\./))
-        return domain = "yt";
-    else
-        throw new Error("BetterYTM is running on an unexpected website. Please don't tamper with the @match directives in the userscript header.");
-}
-/** Returns a pseudo-random ID unique to each session - returns null if sessionStorage is unavailable */
-function getSessionId() {
-    try {
-        if (!sessionStorageAvailable)
-            throw new Error("Session storage unavailable");
-        let sesId = window.sessionStorage.getItem("_bytm-session-id");
-        if (!sesId)
-            window.sessionStorage.setItem("_bytm-session-id", sesId = UserUtils.randomId(8, 36));
-        return sesId;
-    }
-    catch (err) {
-        warn("Couldn't get session ID, sessionStorage / cookies might be disabled:", err);
-        return null;
-    }
-}
-let isCompressionSupported;
-/** Tests whether compression via the predefined {@linkcode compressionFormat} is supported (only on the first call, then returns the cached result) */
-async function compressionSupported() {
-    if (typeof isCompressionSupported === "boolean")
-        return isCompressionSupported;
-    try {
-        await UserUtils.compress(".", compressionFormat, "string");
-        return isCompressionSupported = true;
-    }
-    catch (_a) {
-        return isCompressionSupported = false;
-    }
-}
-/** Returns a string with the given array's items separated by a default separator (`", "` by default), with an optional different separator for the last item */
-function arrayWithSeparators(array, separator = ", ", lastSeparator) {
-    const arr = [...array];
-    if (arr.length === 0)
-        return "";
-    else if (arr.length <= 2)
-        return arr.join(lastSeparator);
-    else
-        return `${arr.slice(0, -1).join(separator)}${lastSeparator}${arr.at(-1)}`;
-}
-/** Returns the watch ID of the current video or null if not on a video page */
-function getWatchId() {
-    const { searchParams, pathname } = new URL(location.href);
-    return pathname.includes("/watch") ? searchParams.get("v") : null;
-}
-/**
- * Returns the ID of the current channel in the format `@User` or `UC...` from URLs with the path `/@User`, `/@User/videos`, `/channel/UC...` or `/channel/UC.../videos`
- * Returns null if the current page is not a channel page or there was an error parsing the URL
- */
-function getCurrentChannelId() {
-    return parseChannelIdFromUrl(location.href);
-}
-/** Returns the channel ID from a URL or null if the URL is invalid */
-function parseChannelIdFromUrl(url) {
-    try {
-        const { pathname } = url instanceof URL ? url : new URL(url);
-        if (pathname.includes("/channel/"))
-            return sanitizeChannelId(pathname.split("/channel/")[1].split("/")[0]);
-        else if (pathname.includes("/@"))
-            return sanitizeChannelId(pathname.split("/@")[1].split("/")[0]);
-        else
-            return null;
-    }
-    catch (_a) {
-        return null;
-    }
-}
-/** Sanitizes a channel ID by adding a leading `@` if the ID doesn't start with `UC...` */
-function sanitizeChannelId(channelId) {
-    channelId = String(channelId).trim();
-    return isValidChannelId(channelId)
-        ? channelId
-        : `@${channelId}`;
-}
-/** Tests whether a string is a valid channel ID in the format `@User` or `.C...` */
-function isValidChannelId(channelId) {
-    return channelId.match(/^([A-Z]C|@)\w+$/) !== null;
-}
-/** Returns the thumbnail URL for a video with either a given quality identifier or index */
-function getThumbnailUrl(watchId, qualityOrIndex = "maxresdefault") {
-    return `https://img.youtube.com/vi/${watchId}/${qualityOrIndex}.jpg`;
-}
-/** Returns the best available thumbnail URL for a video with the given watch ID */
-async function getBestThumbnailUrl(watchId) {
-    try {
-        const priorityList = ["maxresdefault", "sddefault", "hqdefault", 0];
-        for (const quality of priorityList) {
-            let response;
-            const url = getThumbnailUrl(watchId, quality);
-            try {
-                response = await sendRequest({ url, method: "HEAD", timeout: 6000 });
-            }
-            catch (err) {
-                error(`Error while sending HEAD request to thumbnail URL for video '${watchId}' with quality '${quality}':`, err);
-                void err;
-            }
-            if (response && response.status < 300 && response.status >= 200)
-                return url;
-        }
-    }
-    catch (err) {
-        throw new Error(`Couldn't get thumbnail URL for video '${watchId}': ${err}`);
-    }
-}
-/** Opens the given URL in a new tab, using GM.openInTab if available */
-function openInTab(href, background = false) {
-    try {
-        UserUtils.openInNewTab(href, background);
-    }
-    catch (_a) {
-        window.open(href, "_blank", "noopener noreferrer");
-    }
-}
-/** Tries to parse an uncompressed or compressed input string as a JSON object */
-async function tryToDecompressAndParse(input) {
-    let parsed = null;
-    try {
-        parsed = JSON.parse(input);
-    }
-    catch (_a) {
-        try {
-            parsed = JSON.parse(await UserUtils.decompress(input, compressionFormat, "string"));
-        }
-        catch (err) {
-            error("Couldn't decompress and parse data due to an error:", err);
-            return null;
-        }
-    }
-    // artificial timeout to allow animations to finish and because dumb monkey brains *expect* a delay
-    await UserUtils.pauseFor(250);
-    return parsed;
-}
-/** Very crude OS detection */
-function getOS() {
-    if (navigator.userAgent.match(/mac(\s?os|intel)/i))
-        return "mac";
-    return "other";
-}
-//#region resources
-/**
- * Returns the URL of a resource by its name, as defined in `assets/resources.json`, from GM resource cache - [see GM.getResourceUrl docs](https://wiki.greasespot.net/GM.getResourceUrl)
- * Falls back to a `raw.githubusercontent.com` URL or base64-encoded data URI if the resource is not available in the GM resource cache
- */
-async function getResourceUrl(name) {
-    var _a;
-    let url = await GM.getResourceUrl(name);
-    if (!url || url.length === 0) {
-        const resource = (_a = GM.info.script.resources) === null || _a === void 0 ? void 0 : _a[name].url;
-        if (typeof resource === "string") {
-            const resourceUrl = new URL(resource);
-            const resourcePath = resourceUrl.pathname;
-            if (resourcePath)
-                return `https://raw.githubusercontent.com/${repo}/${branch}${resourcePath}`;
-        }
-        warn(`Couldn't get blob URL nor external URL for @resource '${name}', trying to use base64-encoded fallback`);
-        // @ts-ignore
-        url = await GM.getResourceUrl(name, false);
-    }
-    return url;
-}
-/**
- * Returns the preferred locale of the user, provided it is supported by the userscript.
- * Prioritizes `navigator.language`, then `navigator.languages`, then `"en_US"` as a fallback.
- */
-function getPreferredLocale() {
-    var _a;
-    const navLang = navigator.language.replace(/-/g, "_");
-    const navLangs = navigator.languages
-        .filter(lang => lang.match(/^[a-z]{2}(-|_)[A-Z]$/) !== null)
-        .map(lang => lang.replace(/-/g, "_"));
-    if (Object.entries(langMapping).find(([key]) => key === navLang))
-        return navLang;
-    for (const loc of navLangs) {
-        if (Object.entries(langMapping).find(([key]) => key === loc))
-            return loc;
-    }
-    // if navigator.languages has entries that aren't locale codes in the format xx_XX
-    if (navigator.languages.some(lang => lang.match(/^[a-z]{2}$/))) {
-        for (const lang of navLangs) {
-            const foundLoc = (_a = Object.entries(langMapping).find(([key]) => key.startsWith(lang))) === null || _a === void 0 ? void 0 : _a[0];
-            if (foundLoc)
-                return foundLoc;
-        }
-    }
-    return "en_US";
-}
-/** Returns the content behind the passed resource identifier as a string, for example to be assigned to an element's innerHTML property */
-async function resourceAsString(resource) {
-    try {
-        const resourceUrl = await getResourceUrl(resource);
-        if (!resourceUrl)
-            throw new Error(`Couldn't find URL for resource '${resource}'`);
-        return await (await UserUtils.fetchAdvanced(resourceUrl)).text();
-    }
-    catch (err) {
-        error("Couldn't get SVG element from resource:", err);
-        return null;
-    }
-}
-/** Parses a markdown string using marked and turns it into an HTML string with default settings - doesn't sanitize against XSS! */
-function parseMarkdown(mdString) {
-    return marked.marked.parse(mdString, {
-        async: true,
-        gfm: true,
-    });
-}
-/** Returns the content of the changelog markdown file */
-async function getChangelogMd() {
-    return await (await UserUtils.fetchAdvanced(await getResourceUrl("doc-changelog"))).text();
-}
-/** Returns the changelog as HTML with a details element for each version */
-async function getChangelogHtmlWithDetails() {
-    try {
-        const changelogMd = await getChangelogMd();
-        let changelogHtml = await parseMarkdown(changelogMd);
-        const getVerId = (verStr) => verStr.trim().replace(/[._#\s-]/g, "");
-        changelogHtml = changelogHtml.replace(/<div\s+class="split">\s*<\/div>\s*\n?\s*<br(\s\/)?>/gm, "</details>\n<br>\n<details class=\"bytm-changelog-version-details\" tabindex=\"0\">");
-        const h2Matches = Array.from(changelogHtml.matchAll(/<h2(\s+id=".+")?>([\d\w\s.]+)<\/h2>/gm));
-        for (const match of h2Matches) {
-            const [fullMatch, , verStr] = match;
-            const verId = getVerId(verStr);
-            const h2Elem = `<h2 id="${verId}" role="subheading" aria-level="1">${verStr}</h2>`;
-            const summaryElem = `<summary tab-index="0">${h2Elem}</summary>`;
-            changelogHtml = changelogHtml.replace(fullMatch, `${summaryElem}`);
-        }
-        changelogHtml = `<details class="bytm-changelog-version-details" tabindex="0">${changelogHtml}</details>`;
-        return changelogHtml;
-    }
-    catch (err) {
-        return `Error while preparing changelog: ${err}`;
-    }
 }let curLogLevel = LogLevel.Info;
 /** Common prefix to be able to tell logged messages apart and filter them in devtools */
 const consPrefix = `[${scriptInfo.name}]`;