浏览代码

fix: automatically fix broken config on migration

Sv443 11 月之前
父节点
当前提交
575031f57a
共有 4 个文件被更改,包括 122 次插入84 次删除
  1. 79 60
      dist/BetterYTM.user.js
  2. 31 11
      src/config.ts
  3. 10 11
      src/features/index.ts
  4. 2 2
      src/index.ts

+ 79 - 60
dist/BetterYTM.user.js

@@ -17,7 +17,7 @@
 // @license           AGPL-3.0-or-later
 // @license           AGPL-3.0-or-later
 // @author            Sv443
 // @author            Sv443
 // @copyright         Sv443 (https://github.com/Sv443)
 // @copyright         Sv443 (https://github.com/Sv443)
-// @icon              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=791ad8d
+// @icon              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=9fa2f5d
 // @match             https://music.youtube.com/*
 // @match             https://music.youtube.com/*
 // @match             https://www.youtube.com/*
 // @match             https://www.youtube.com/*
 // @run-at            document-start
 // @run-at            document-start
@@ -35,42 +35,42 @@
 // @grant             GM.openInTab
 // @grant             GM.openInTab
 // @grant             unsafeWindow
 // @grant             unsafeWindow
 // @noframes
 // @noframes
-// @resource          css-bundle              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/dist/BetterYTM.css?b=791ad8d
-// @resource          css-above_queue_btns    https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/aboveQueueBtns.css?b=791ad8d
-// @resource          css-anchor_improvements https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/anchorImprovements.css?b=791ad8d
-// @resource          css-fix_hdr             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/fixHDR.css?b=791ad8d
-// @resource          css-fix_spacing         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/fixSpacing.css?b=791ad8d
-// @resource          doc-changelog           https://raw.githubusercontent.com/Sv443/BetterYTM/develop/changelog.md?b=791ad8d
-// @resource          icon-advanced_mode      https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/plus_circle_small.svg?b=791ad8d
-// @resource          icon-arrow_down         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/arrow_down.svg?b=791ad8d
-// @resource          icon-clear_list         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/clear_list.svg?b=791ad8d
-// @resource          icon-delete             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/delete.svg?b=791ad8d
-// @resource          icon-error              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/error.svg?b=791ad8d
-// @resource          icon-experimental       https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/beaker_small.svg?b=791ad8d
-// @resource          icon-globe              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/globe.svg?b=791ad8d
-// @resource          icon-help               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/help.svg?b=791ad8d
-// @resource          icon-image_filled       https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/image_filled.svg?b=791ad8d
-// @resource          icon-image              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/image.svg?b=791ad8d
-// @resource          icon-link               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/link.svg?b=791ad8d
-// @resource          icon-lyrics             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/lyrics.svg?b=791ad8d
-// @resource          icon-reload             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/refresh.svg?b=791ad8d
-// @resource          icon-skip_to            https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/skip_to.svg?b=791ad8d
-// @resource          icon-spinner            https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/spinner.svg?b=791ad8d
-// @resource          img-logo                https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=791ad8d
-// @resource          img-close               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/close.png?b=791ad8d
-// @resource          img-discord             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/discord.png?b=791ad8d
-// @resource          img-github              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/github.png?b=791ad8d
-// @resource          img-greasyfork          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/greasyfork.png?b=791ad8d
-// @resource          img-openuserjs          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/openuserjs.png?b=791ad8d
-// @resource          trans-de_DE             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/de_DE.json?b=791ad8d
-// @resource          trans-en_US             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_US.json?b=791ad8d
-// @resource          trans-en_UK             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_UK.json?b=791ad8d
-// @resource          trans-es_ES             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/es_ES.json?b=791ad8d
-// @resource          trans-fr_FR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/fr_FR.json?b=791ad8d
-// @resource          trans-hi_IN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/hi_IN.json?b=791ad8d
-// @resource          trans-ja_JA             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/ja_JA.json?b=791ad8d
-// @resource          trans-pt_BR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/pt_BR.json?b=791ad8d
-// @resource          trans-zh_CN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/zh_CN.json?b=791ad8d
+// @resource          css-bundle              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/dist/BetterYTM.css?b=9fa2f5d
+// @resource          css-above_queue_btns    https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/aboveQueueBtns.css?b=9fa2f5d
+// @resource          css-anchor_improvements https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/anchorImprovements.css?b=9fa2f5d
+// @resource          css-fix_hdr             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/fixHDR.css?b=9fa2f5d
+// @resource          css-fix_spacing         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/style/fixSpacing.css?b=9fa2f5d
+// @resource          doc-changelog           https://raw.githubusercontent.com/Sv443/BetterYTM/develop/changelog.md?b=9fa2f5d
+// @resource          icon-advanced_mode      https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/plus_circle_small.svg?b=9fa2f5d
+// @resource          icon-arrow_down         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/arrow_down.svg?b=9fa2f5d
+// @resource          icon-clear_list         https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/clear_list.svg?b=9fa2f5d
+// @resource          icon-delete             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/delete.svg?b=9fa2f5d
+// @resource          icon-error              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/error.svg?b=9fa2f5d
+// @resource          icon-experimental       https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/beaker_small.svg?b=9fa2f5d
+// @resource          icon-globe              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/globe.svg?b=9fa2f5d
+// @resource          icon-help               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/help.svg?b=9fa2f5d
+// @resource          icon-image_filled       https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/image_filled.svg?b=9fa2f5d
+// @resource          icon-image              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/image.svg?b=9fa2f5d
+// @resource          icon-link               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/link.svg?b=9fa2f5d
+// @resource          icon-lyrics             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/lyrics.svg?b=9fa2f5d
+// @resource          icon-reload             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/refresh.svg?b=9fa2f5d
+// @resource          icon-skip_to            https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/skip_to.svg?b=9fa2f5d
+// @resource          icon-spinner            https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/icons/spinner.svg?b=9fa2f5d
+// @resource          img-logo                https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/logo/logo_48.png?b=9fa2f5d
+// @resource          img-close               https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/close.png?b=9fa2f5d
+// @resource          img-discord             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/discord.png?b=9fa2f5d
+// @resource          img-github              https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/github.png?b=9fa2f5d
+// @resource          img-greasyfork          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/greasyfork.png?b=9fa2f5d
+// @resource          img-openuserjs          https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/images/external/openuserjs.png?b=9fa2f5d
+// @resource          trans-de_DE             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/de_DE.json?b=9fa2f5d
+// @resource          trans-en_US             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_US.json?b=9fa2f5d
+// @resource          trans-en_UK             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/en_UK.json?b=9fa2f5d
+// @resource          trans-es_ES             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/es_ES.json?b=9fa2f5d
+// @resource          trans-fr_FR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/fr_FR.json?b=9fa2f5d
+// @resource          trans-hi_IN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/hi_IN.json?b=9fa2f5d
+// @resource          trans-ja_JA             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/ja_JA.json?b=9fa2f5d
+// @resource          trans-pt_BR             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/pt_BR.json?b=9fa2f5d
+// @resource          trans-zh_CN             https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/translations/zh_CN.json?b=9fa2f5d
 // @require           https://cdn.jsdelivr.net/npm/@sv443-network/[email protected]/dist/index.global.js
 // @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]/dist/fuse.basic.js
 // @require           https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.js
 // @require           https://cdn.jsdelivr.net/npm/[email protected]/lib/marked.umd.js
@@ -198,7 +198,7 @@ var PluginIntent;
 })(PluginIntent || (PluginIntent = {}));const modeRaw = "development";
 })(PluginIntent || (PluginIntent = {}));const modeRaw = "development";
 const branchRaw = "develop";
 const branchRaw = "develop";
 const hostRaw = "github";
 const hostRaw = "github";
-const buildNumberRaw = "791ad8d";
+const buildNumberRaw = "9fa2f5d";
 /** The mode in which the script was built (production or development) */
 /** The mode in which the script was built (production or development) */
 const mode = (modeRaw.match(/^#{{.+}}$/) ? "production" : modeRaw);
 const mode = (modeRaw.match(/^#{{.+}}$/) ? "production" : modeRaw);
 /** The branch to use in various URLs that point to the GitHub repo */
 /** The branch to use in various URLs that point to the GitHub repo */
@@ -4327,14 +4327,6 @@ function addQueueButtons(queueItem_1) {
         queueItem.classList.add("bytm-has-queue-btns");
         queueItem.classList.add("bytm-has-queue-btns");
     });
     });
 }//#region dependencies
 }//#region dependencies
-/** List of all available locale SelectOptions */
-const localeOptions = Object.entries(langMapping).reduce((a, [locale, { name }]) => {
-    return [...a, {
-            value: locale,
-            label: name,
-        }];
-}, [])
-    .sort((a, b) => a.label.localeCompare(b.label));
 /** Creates an HTML string for the given adornment properties */
 /** Creates an HTML string for the given adornment properties */
 const getAdornHtml = (className, title, resource, extraParams) => __awaiter(void 0, void 0, void 0, function* () { var _a; return `<span class="${className} bytm-adorn-icon" title="${title}" aria-label="${title}"${extraParams ? " " + extraParams : ""}>${(_a = yield resourceToHTMLString(resource)) !== null && _a !== void 0 ? _a : ""}</span>`; });
 const getAdornHtml = (className, title, resource, extraParams) => __awaiter(void 0, void 0, void 0, function* () { var _a; return `<span class="${className} bytm-adorn-icon" title="${title}" aria-label="${title}"${extraParams ? " " + extraParams : ""}>${(_a = yield resourceToHTMLString(resource)) !== null && _a !== void 0 ? _a : ""}</span>`; });
 /** Combines multiple async functions or promises that resolve with an adornment HTML string into a single string */
 /** Combines multiple async functions or promises that resolve with an adornment HTML string into a single string */
@@ -4361,6 +4353,14 @@ const options = {
         { value: "yt", label: t("site_selection_only_yt") },
         { value: "yt", label: t("site_selection_only_yt") },
         { value: "ytm", label: t("site_selection_only_ytm") },
         { value: "ytm", label: t("site_selection_only_ytm") },
     ],
     ],
+    locale: () => Object.entries(langMapping)
+        .reduce((a, [locale, { name }]) => {
+        return [...a, {
+                value: locale,
+                label: name,
+            }];
+    }, [])
+        .sort((a, b) => a.label.localeCompare(b.label)),
 };
 };
 //#region features
 //#region features
 /**
 /**
@@ -4615,7 +4615,7 @@ const featInfo = {
         min: 0,
         min: 0,
         max: 30,
         max: 30,
         step: 0.5,
         step: 0.5,
-        default: 0,
+        default: 3,
         unit: "s",
         unit: "s",
         reloadRequired: false,
         reloadRequired: false,
         enable: noop,
         enable: noop,
@@ -4805,7 +4805,7 @@ const featInfo = {
     locale: {
     locale: {
         type: "select",
         type: "select",
         category: "general",
         category: "general",
-        options: localeOptions,
+        options: options.locale,
         default: getPreferredLocale(),
         default: getPreferredLocale(),
         textAdornment: () => combineAdornments([adornments.globe, adornments.reloadRequired]),
         textAdornment: () => combineAdornments([adornments.globe, adornments.reloadRequired]),
     },
     },
@@ -4851,18 +4851,18 @@ const migrations = {
         return Object.assign(Object.assign({}, oldData), { deleteFromQueueButton: queueBtnsEnabled, lyricsQueueButton: queueBtnsEnabled });
         return Object.assign(Object.assign({}, oldData), { deleteFromQueueButton: queueBtnsEnabled, lyricsQueueButton: queueBtnsEnabled });
     },
     },
     // 2 -> 3 (v1.0)
     // 2 -> 3 (v1.0)
-    3: (oldData) => useDefaultConfig([
+    3: (oldData) => useDefaultConfig(oldData, [
         "removeShareTrackingParam", "numKeysSkipToTime",
         "removeShareTrackingParam", "numKeysSkipToTime",
         "fixSpacing", "scrollToActiveSongBtn", "logLevel",
         "fixSpacing", "scrollToActiveSongBtn", "logLevel",
-    ], oldData),
+    ]),
     // 3 -> 4 (v1.1)
     // 3 -> 4 (v1.1)
     4: (oldData) => {
     4: (oldData) => {
         var _a, _b, _c, _d;
         var _a, _b, _c, _d;
         const oldSwitchSitesHotkey = oldData.switchSitesHotkey;
         const oldSwitchSitesHotkey = oldData.switchSitesHotkey;
-        return Object.assign(Object.assign({}, useDefaultConfig([
+        return Object.assign(Object.assign({}, useDefaultConfig(oldData, [
             "rememberSongTime", "rememberSongTimeSites",
             "rememberSongTime", "rememberSongTimeSites",
             "volumeSliderScrollStep", "locale", "versionCheck",
             "volumeSliderScrollStep", "locale", "versionCheck",
-        ], oldData)), { arrowKeySkipBy: 10, switchSitesHotkey: {
+        ])), { arrowKeySkipBy: 10, switchSitesHotkey: {
                 code: (_a = oldSwitchSitesHotkey.key) !== null && _a !== void 0 ? _a : "F9",
                 code: (_a = oldSwitchSitesHotkey.key) !== null && _a !== void 0 ? _a : "F9",
                 shift: Boolean((_b = oldSwitchSitesHotkey.shift) !== null && _b !== void 0 ? _b : false),
                 shift: Boolean((_b = oldSwitchSitesHotkey.shift) !== null && _b !== void 0 ? _b : false),
                 ctrl: Boolean((_c = oldSwitchSitesHotkey.ctrl) !== null && _c !== void 0 ? _c : false),
                 ctrl: Boolean((_c = oldSwitchSitesHotkey.ctrl) !== null && _c !== void 0 ? _c : false),
@@ -4870,7 +4870,7 @@ const migrations = {
             }, listButtonsPlacement: "queueOnly" });
             }, listButtonsPlacement: "queueOnly" });
     },
     },
     // 4 -> 5 (v1.2)
     // 4 -> 5 (v1.2)
-    5: (oldData) => (Object.assign({}, useDefaultConfig([
+    5: (oldData) => (Object.assign({}, useDefaultConfig(oldData, [
         "geniUrlBase", "geniUrlToken",
         "geniUrlBase", "geniUrlToken",
         "lyricsCacheMaxSize", "lyricsCacheTTL",
         "lyricsCacheMaxSize", "lyricsCacheTTL",
         "clearLyricsCache", "advancedMode",
         "clearLyricsCache", "advancedMode",
@@ -4881,8 +4881,8 @@ const migrations = {
         "thumbnailOverlayBehavior", "thumbnailOverlayToggleBtnShown",
         "thumbnailOverlayBehavior", "thumbnailOverlayToggleBtnShown",
         "thumbnailOverlayShowIndicator", "thumbnailOverlayIndicatorOpacity",
         "thumbnailOverlayShowIndicator", "thumbnailOverlayIndicatorOpacity",
         "thumbnailOverlayImageFit", "removeShareTrackingParamSites",
         "thumbnailOverlayImageFit", "removeShareTrackingParamSites",
-        "fixHdrIssues", "clearQueueBtn",
-    ], oldData))),
+        "fixHdrIssues", "clearQueueBtn", "closeToastsTimeout",
+    ]))),
     // TODO: once advanced filtering is fully implemented, clear cache on migration to fv6
     // TODO: once advanced filtering is fully implemented, clear cache on migration to fv6
     // 5 -> 6 (v1.3)
     // 5 -> 6 (v1.3)
     // 6: (oldData: FeatureConfig) => 
     // 6: (oldData: FeatureConfig) => 
@@ -4895,7 +4895,7 @@ const defaultData = Object.keys(featInfo)
     return acc;
     return acc;
 }, {});
 }, {});
 /** Uses the default config as the base, then overwrites all values with the passed {@linkcode baseData}, then sets all passed {@linkcode resetKeys} to their default values */
 /** Uses the default config as the base, then overwrites all values with the passed {@linkcode baseData}, then sets all passed {@linkcode resetKeys} to their default values */
-function useDefaultConfig(resetKeys, baseData) {
+function useDefaultConfig(baseData, resetKeys) {
     var _a;
     var _a;
     const newData = Object.assign(Object.assign({}, defaultData), (baseData !== null && baseData !== void 0 ? baseData : {}));
     const newData = Object.assign(Object.assign({}, defaultData), (baseData !== null && baseData !== void 0 ? baseData : {}));
     for (const key of resetKeys) // @ts-ignore
     for (const key of resetKeys) // @ts-ignore
@@ -4911,21 +4911,40 @@ const bytmCfgStore = new UserUtils.DataStore({
     encodeData: (data) => canCompress ? UserUtils.compress(data, compressionFormat, "string") : data,
     encodeData: (data) => canCompress ? UserUtils.compress(data, compressionFormat, "string") : data,
     decodeData: (data) => canCompress ? UserUtils.decompress(data, compressionFormat, "string") : data,
     decodeData: (data) => canCompress ? UserUtils.decompress(data, compressionFormat, "string") : data,
 });
 });
-/** Initializes the DataStore instance and loads persistent data into memory */
+/** Initializes the DataStore instance and loads persistent data into memory. Returns a copy of the config object. */
 function initConfig() {
 function initConfig() {
     return __awaiter(this, void 0, void 0, function* () {
     return __awaiter(this, void 0, void 0, function* () {
         canCompress = yield compressionSupported();
         canCompress = yield compressionSupported();
         const oldFmtVer = Number(yield GM.getValue(`_uucfgver-${bytmCfgStore.id}`, NaN));
         const oldFmtVer = Number(yield GM.getValue(`_uucfgver-${bytmCfgStore.id}`, NaN));
-        const data = yield bytmCfgStore.loadData();
+        let data = yield bytmCfgStore.loadData();
         log(`Initialized feature config DataStore (formatVersion = ${bytmCfgStore.formatVersion})`);
         log(`Initialized feature config DataStore (formatVersion = ${bytmCfgStore.formatVersion})`);
         if (isNaN(oldFmtVer))
         if (isNaN(oldFmtVer))
             info("  !- Config data was initialized with default values");
             info("  !- Config data was initialized with default values");
-        else if (oldFmtVer !== bytmCfgStore.formatVersion)
+        else if (oldFmtVer !== bytmCfgStore.formatVersion) {
+            data = fixMissingCfgKeys(data);
+            yield bytmCfgStore.setData(data);
             info(`  !- Config data was migrated from version ${oldFmtVer} to ${bytmCfgStore.formatVersion}`);
             info(`  !- Config data was migrated from version ${oldFmtVer} to ${bytmCfgStore.formatVersion}`);
+        }
         emitInterface("bytm:configReady", getFeaturesInterface());
         emitInterface("bytm:configReady", getFeaturesInterface());
         return Object.assign({}, data);
         return Object.assign({}, data);
     });
     });
 }
 }
+/**
+ * Fixes missing keys in the passed config object with their default values and returns a copy of the fixed object.
+ * Returns a copy of the originally passed object if nothing needs to be fixed.
+ */
+function fixMissingCfgKeys(cfg) {
+    cfg = Object.assign({}, cfg);
+    const passedKeys = Object.keys(cfg);
+    const defaultKeys = Object.keys(defaultData);
+    const missingKeys = defaultKeys.filter(k => !passedKeys.includes(k));
+    if (missingKeys.length > 0) {
+        info("Fixed missing feature config keys:", missingKeys);
+        for (const key of missingKeys)
+            cfg[key] = defaultData[key];
+    }
+    return cfg;
+}
 /** Returns the current feature config from the in-memory cache as a copy */
 /** Returns the current feature config from the in-memory cache as a copy */
 function getFeatures() {
 function getFeatures() {
     return bytmCfgStore.getData();
     return bytmCfgStore.getData();
@@ -5946,7 +5965,7 @@ function registerDevMenuCommands() {
     }), "r");
     }), "r");
     GM.registerMenuCommand("Fix missing config values", () => __awaiter(this, void 0, void 0, function* () {
     GM.registerMenuCommand("Fix missing config values", () => __awaiter(this, void 0, void 0, function* () {
         const oldFeats = reserialize(getFeatures());
         const oldFeats = reserialize(getFeatures());
-        yield setFeatures(Object.assign(Object.assign({}, defaultData), getFeatures()));
+        yield setFeatures(fixMissingCfgKeys(oldFeats));
         console.log("Fixed missing config values.\nFrom:", oldFeats, "\n\nTo:", getFeatures());
         console.log("Fixed missing config values.\nFrom:", oldFeats, "\n\nTo:", getFeatures());
         if (confirm("All missing or invalid config values were set to their default values.\nReload the page now?"))
         if (confirm("All missing or invalid config values were set to their default values.\nReload the page now?"))
             location.reload();
             location.reload();

+ 31 - 11
src/config.ts

@@ -21,18 +21,18 @@ export const migrations: DataMigrationsDict = {
     };
     };
   },
   },
   // 2 -> 3 (v1.0)
   // 2 -> 3 (v1.0)
-  3: (oldData: FeatureConfig) => useDefaultConfig([
+  3: (oldData: FeatureConfig) => useDefaultConfig(oldData, [
     "removeShareTrackingParam", "numKeysSkipToTime",
     "removeShareTrackingParam", "numKeysSkipToTime",
     "fixSpacing", "scrollToActiveSongBtn", "logLevel",
     "fixSpacing", "scrollToActiveSongBtn", "logLevel",
-  ], oldData),
+  ]),
   // 3 -> 4 (v1.1)
   // 3 -> 4 (v1.1)
   4: (oldData: FeatureConfig) => {
   4: (oldData: FeatureConfig) => {
     const oldSwitchSitesHotkey = oldData.switchSitesHotkey as Record<string, unknown>;
     const oldSwitchSitesHotkey = oldData.switchSitesHotkey as Record<string, unknown>;
     return {
     return {
-      ...useDefaultConfig([
+      ...useDefaultConfig(oldData, [
         "rememberSongTime", "rememberSongTimeSites",
         "rememberSongTime", "rememberSongTimeSites",
         "volumeSliderScrollStep", "locale", "versionCheck",
         "volumeSliderScrollStep", "locale", "versionCheck",
-      ], oldData),
+      ]),
       arrowKeySkipBy: 10,
       arrowKeySkipBy: 10,
       switchSitesHotkey: {
       switchSitesHotkey: {
         code: oldSwitchSitesHotkey.key ?? "F9",
         code: oldSwitchSitesHotkey.key ?? "F9",
@@ -45,7 +45,7 @@ export const migrations: DataMigrationsDict = {
   },
   },
   // 4 -> 5 (v1.2)
   // 4 -> 5 (v1.2)
   5: (oldData: FeatureConfig) => ({
   5: (oldData: FeatureConfig) => ({
-    ...useDefaultConfig([
+    ...useDefaultConfig(oldData, [
       "geniUrlBase", "geniUrlToken",
       "geniUrlBase", "geniUrlToken",
       "lyricsCacheMaxSize", "lyricsCacheTTL",
       "lyricsCacheMaxSize", "lyricsCacheTTL",
       "clearLyricsCache", "advancedMode",
       "clearLyricsCache", "advancedMode",
@@ -56,8 +56,8 @@ export const migrations: DataMigrationsDict = {
       "thumbnailOverlayBehavior", "thumbnailOverlayToggleBtnShown",
       "thumbnailOverlayBehavior", "thumbnailOverlayToggleBtnShown",
       "thumbnailOverlayShowIndicator", "thumbnailOverlayIndicatorOpacity",
       "thumbnailOverlayShowIndicator", "thumbnailOverlayIndicatorOpacity",
       "thumbnailOverlayImageFit", "removeShareTrackingParamSites",
       "thumbnailOverlayImageFit", "removeShareTrackingParamSites",
-      "fixHdrIssues", "clearQueueBtn",
-    ], oldData),
+      "fixHdrIssues", "clearQueueBtn", "closeToastsTimeout",
+    ]),
   }),
   }),
   // TODO: once advanced filtering is fully implemented, clear cache on migration to fv6
   // TODO: once advanced filtering is fully implemented, clear cache on migration to fv6
   // 5 -> 6 (v1.3)
   // 5 -> 6 (v1.3)
@@ -72,7 +72,7 @@ export const defaultData = (Object.keys(featInfo) as (keyof typeof featInfo)[])
   }, {}) as FeatureConfig;
   }, {}) as FeatureConfig;
 
 
 /** Uses the default config as the base, then overwrites all values with the passed {@linkcode baseData}, then sets all passed {@linkcode resetKeys} to their default values */
 /** Uses the default config as the base, then overwrites all values with the passed {@linkcode baseData}, then sets all passed {@linkcode resetKeys} to their default values */
-function useDefaultConfig(resetKeys: (keyof typeof featInfo)[], baseData?: FeatureConfig): Partial<FeatureConfig> {
+function useDefaultConfig(baseData: Partial<FeatureConfig> | undefined, resetKeys: (keyof typeof featInfo)[]): FeatureConfig {
   const newData = { ...defaultData, ...(baseData ?? {}) };
   const newData = { ...defaultData, ...(baseData ?? {}) };
   for(const key of resetKeys) // @ts-ignore
   for(const key of resetKeys) // @ts-ignore
     newData[key] = featInfo?.[key]?.default as never; // typescript funny moments
     newData[key] = featInfo?.[key]?.default as never; // typescript funny moments
@@ -90,22 +90,42 @@ const bytmCfgStore = new DataStore({
   decodeData: (data) => canCompress ? decompress(data, compressionFormat, "string") : data,
   decodeData: (data) => canCompress ? decompress(data, compressionFormat, "string") : data,
 });
 });
 
 
-/** Initializes the DataStore instance and loads persistent data into memory */
+/** Initializes the DataStore instance and loads persistent data into memory. Returns a copy of the config object. */
 export async function initConfig() {
 export async function initConfig() {
   canCompress = await compressionSupported();
   canCompress = await compressionSupported();
   const oldFmtVer = Number(await GM.getValue(`_uucfgver-${bytmCfgStore.id}`, NaN));
   const oldFmtVer = Number(await GM.getValue(`_uucfgver-${bytmCfgStore.id}`, NaN));
-  const data = await bytmCfgStore.loadData();
+  let data = await bytmCfgStore.loadData();
   log(`Initialized feature config DataStore (formatVersion = ${bytmCfgStore.formatVersion})`);
   log(`Initialized feature config DataStore (formatVersion = ${bytmCfgStore.formatVersion})`);
   if(isNaN(oldFmtVer))
   if(isNaN(oldFmtVer))
     info("  !- Config data was initialized with default values");
     info("  !- Config data was initialized with default values");
-  else if(oldFmtVer !== bytmCfgStore.formatVersion)
+  else if(oldFmtVer !== bytmCfgStore.formatVersion) {
+    data = fixMissingCfgKeys(data);
+    await bytmCfgStore.setData(data);
     info(`  !- Config data was migrated from version ${oldFmtVer} to ${bytmCfgStore.formatVersion}`);
     info(`  !- Config data was migrated from version ${oldFmtVer} to ${bytmCfgStore.formatVersion}`);
+  }
 
 
   emitInterface("bytm:configReady", getFeaturesInterface());
   emitInterface("bytm:configReady", getFeaturesInterface());
 
 
   return { ...data };
   return { ...data };
 }
 }
 
 
+/**
+ * Fixes missing keys in the passed config object with their default values and returns a copy of the fixed object.  
+ * Returns a copy of the originally passed object if nothing needs to be fixed.
+ */
+export function fixMissingCfgKeys(cfg: Partial<FeatureConfig>): FeatureConfig {
+  cfg = { ...cfg };
+  const passedKeys = Object.keys(cfg);
+  const defaultKeys = Object.keys(defaultData);
+  const missingKeys = defaultKeys.filter(k => !passedKeys.includes(k));
+  if(missingKeys.length > 0) {
+    info("Fixed missing feature config keys:", missingKeys);
+    for(const key of missingKeys)
+      cfg[key as keyof FeatureConfig] = defaultData[key as keyof FeatureConfig] as never;
+  }
+  return cfg as FeatureConfig;
+}
+
 /** Returns the current feature config from the in-memory cache as a copy */
 /** Returns the current feature config from the in-memory cache as a copy */
 export function getFeatures(): FeatureConfig {
 export function getFeatures(): FeatureConfig {
   return bytmCfgStore.getData();
   return bytmCfgStore.getData();

+ 10 - 11
src/features/index.ts

@@ -21,15 +21,6 @@ type SelectOption = { value: number | string, label: string };
 
 
 //#region dependencies
 //#region dependencies
 
 
-/** List of all available locale SelectOptions */
-const localeOptions = Object.entries(langMapping).reduce((a, [locale, { name }]) => {
-  return [...a, {
-    value: locale,
-    label: name,
-  }];
-}, [] as SelectOption[])
-  .sort((a, b) => a.label.localeCompare(b.label));
-
 /** Creates an HTML string for the given adornment properties */
 /** Creates an HTML string for the given adornment properties */
 const getAdornHtml = async (className: string, title: string, resource: ResourceKey, extraParams?: string) =>
 const getAdornHtml = async (className: string, title: string, resource: ResourceKey, extraParams?: string) =>
   `<span class="${className} bytm-adorn-icon" title="${title}" aria-label="${title}"${extraParams ? " " + extraParams : ""}>${await resourceToHTMLString(resource) ?? ""}</span>`;
   `<span class="${className} bytm-adorn-icon" title="${title}" aria-label="${title}"${extraParams ? " " + extraParams : ""}>${await resourceToHTMLString(resource) ?? ""}</span>`;
@@ -66,6 +57,14 @@ const options = {
     { value: "yt", label: t("site_selection_only_yt") },
     { value: "yt", label: t("site_selection_only_yt") },
     { value: "ytm", label: t("site_selection_only_ytm") },
     { value: "ytm", label: t("site_selection_only_ytm") },
   ],
   ],
+  locale: () => Object.entries(langMapping)
+    .reduce((a, [locale, { name }]) => {
+      return [...a, {
+        value: locale,
+        label: name,
+      }];
+    }, [] as SelectOption[])
+    .sort((a, b) => a.label.localeCompare(b.label)),
 };
 };
 
 
 //#region features
 //#region features
@@ -325,7 +324,7 @@ export const featInfo = {
     min: 0,
     min: 0,
     max: 30,
     max: 30,
     step: 0.5,
     step: 0.5,
-    default: 0,
+    default: 3,
     unit: "s",
     unit: "s",
     reloadRequired: false,
     reloadRequired: false,
     enable: noop,
     enable: noop,
@@ -516,7 +515,7 @@ export const featInfo = {
   locale: {
   locale: {
     type: "select",
     type: "select",
     category: "general",
     category: "general",
-    options: localeOptions,
+    options: options.locale,
     default: getPreferredLocale(),
     default: getPreferredLocale(),
     textAdornment: () => combineAdornments([adornments.globe, adornments.reloadRequired]),
     textAdornment: () => combineAdornments([adornments.globe, adornments.reloadRequired]),
   },
   },

+ 2 - 2
src/index.ts

@@ -1,6 +1,6 @@
 import { compress, decompress, type Stringifiable } from "@sv443-network/userutils";
 import { compress, decompress, type Stringifiable } from "@sv443-network/userutils";
 import { addStyleFromResource, domLoaded, reserialize, warn } from "./utils";
 import { addStyleFromResource, domLoaded, reserialize, warn } from "./utils";
-import { clearConfig, defaultData as defaultFeatData, getFeatures, initConfig, setFeatures } from "./config";
+import { clearConfig, fixMissingCfgKeys, getFeatures, initConfig, setFeatures } from "./config";
 import { buildNumber, compressionFormat, defaultLogLevel, mode, scriptInfo } from "./constants";
 import { buildNumber, compressionFormat, defaultLogLevel, mode, scriptInfo } from "./constants";
 import { error, getDomain, info, getSessionId, log, setLogLevel, initTranslations, setLocale } from "./utils";
 import { error, getDomain, info, getSessionId, log, setLogLevel, initTranslations, setLocale } from "./utils";
 import { initSiteEvents } from "./siteEvents";
 import { initSiteEvents } from "./siteEvents";
@@ -356,7 +356,7 @@ function registerDevMenuCommands() {
 
 
   GM.registerMenuCommand("Fix missing config values", async () => {
   GM.registerMenuCommand("Fix missing config values", async () => {
     const oldFeats = reserialize(getFeatures());
     const oldFeats = reserialize(getFeatures());
-    await setFeatures({ ...defaultFeatData, ...getFeatures() });
+    await setFeatures(fixMissingCfgKeys(oldFeats));
     console.log("Fixed missing config values.\nFrom:", oldFeats, "\n\nTo:", getFeatures());
     console.log("Fixed missing config values.\nFrom:", oldFeats, "\n\nTo:", getFeatures());
     if(confirm("All missing or invalid config values were set to their default values.\nReload the page now?"))
     if(confirm("All missing or invalid config values were set to their default values.\nReload the page now?"))
       location.reload();
       location.reload();