config.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { DataStore, compress, type DataMigrationsDict, decompress } from "@sv443-network/userutils";
  2. import { featInfo } from "./features/index";
  3. import { compressionSupported, error, info, log } from "./utils";
  4. import { emitSiteEvent } from "./siteEvents";
  5. import { compressionFormat } from "./constants";
  6. import { emitInterface } from "./interface";
  7. import type { FeatureConfig, FeatureKey } from "./types";
  8. /** If this number is incremented, the features object data will be migrated to the new format */
  9. export const formatVersion = 5;
  10. /** Config data format migration dictionary */
  11. export const migrations: DataMigrationsDict = {
  12. // 1 -> 2 (v1.0)
  13. 2: (oldData: Record<string, unknown>) => {
  14. const queueBtnsEnabled = Boolean(oldData.queueButtons);
  15. delete oldData.queueButtons;
  16. return {
  17. ...oldData,
  18. deleteFromQueueButton: queueBtnsEnabled,
  19. lyricsQueueButton: queueBtnsEnabled,
  20. };
  21. },
  22. // 2 -> 3 (v1.0)
  23. 3: (oldData: FeatureConfig) => useDefaultConfig(oldData, [
  24. "removeShareTrackingParam", "numKeysSkipToTime",
  25. "fixSpacing", "scrollToActiveSongBtn", "logLevel",
  26. ]),
  27. // 3 -> 4 (v1.1)
  28. 4: (oldData: FeatureConfig) => {
  29. const oldSwitchSitesHotkey = oldData.switchSitesHotkey as Record<string, unknown>;
  30. return {
  31. ...useDefaultConfig(oldData, [
  32. "rememberSongTime", "rememberSongTimeSites",
  33. "volumeSliderScrollStep", "locale", "versionCheck",
  34. ]),
  35. arrowKeySkipBy: 10,
  36. switchSitesHotkey: {
  37. code: oldSwitchSitesHotkey.key ?? "F9",
  38. shift: Boolean(oldSwitchSitesHotkey.shift ?? false),
  39. ctrl: Boolean(oldSwitchSitesHotkey.ctrl ?? false),
  40. alt: Boolean(oldSwitchSitesHotkey.meta ?? false),
  41. },
  42. listButtonsPlacement: "queueOnly",
  43. };
  44. },
  45. // 4 -> 5 (v2.0)
  46. 5: (oldData: FeatureConfig) => useDefaultConfig(oldData, [
  47. "localeFallback", "geniUrlBase", "geniUrlToken",
  48. "lyricsCacheMaxSize", "lyricsCacheTTL",
  49. "clearLyricsCache", "advancedMode",
  50. "checkVersionNow", "advancedLyricsFilter",
  51. "rememberSongTimeDuration", "rememberSongTimeReduction",
  52. "rememberSongTimeMinPlayTime", "volumeSharedBetweenTabs",
  53. "setInitialTabVolume", "initialTabVolumeLevel",
  54. "thumbnailOverlayBehavior", "thumbnailOverlayToggleBtnShown",
  55. "thumbnailOverlayShowIndicator", "thumbnailOverlayIndicatorOpacity",
  56. "thumbnailOverlayImageFit", "removeShareTrackingParamSites",
  57. "fixHdrIssues", "clearQueueBtn",
  58. "closeToastsTimeout", "disableDarkReaderSites",
  59. ]),
  60. // 5 -> 6 (v2.1)
  61. 6: (oldData: FeatureConfig) => useDefaultConfig(oldData, [
  62. "autoLikeChannels", "openAutoLikeChannelsDialog",
  63. ]),
  64. // TODO: once advanced filtering is fully implemented, clear cache on migration to fv6
  65. // 6 -> 7 (v2.x)
  66. // 7: (oldData: FeatureConfig) =>
  67. } as const satisfies DataMigrationsDict;
  68. export const defaultData = (Object.keys(featInfo) as (keyof typeof featInfo)[])
  69. .reduce<Partial<FeatureConfig>>((acc, key) => {
  70. // @ts-ignore
  71. acc[key] = featInfo?.[key]?.default as unknown as undefined;
  72. return acc;
  73. }, {}) as FeatureConfig;
  74. /** 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 */
  75. function useDefaultConfig(baseData: Partial<FeatureConfig> | undefined, resetKeys: (keyof typeof featInfo)[]): FeatureConfig {
  76. const newData = { ...defaultData, ...(baseData ?? {}) };
  77. for(const key of resetKeys) // @ts-ignore
  78. newData[key] = featInfo?.[key]?.default as never; // typescript funny moments
  79. return newData;
  80. }
  81. let canCompress = true;
  82. const cfgDataStore = new DataStore({
  83. id: "bytm-config",
  84. formatVersion,
  85. defaultData,
  86. migrations,
  87. encodeData: (data) => canCompress ? compress(data, compressionFormat, "string") : data,
  88. decodeData: (data) => canCompress ? decompress(data, compressionFormat, "string") : data,
  89. });
  90. /** Initializes the DataStore instance and loads persistent data into memory. Returns a copy of the config object. */
  91. export async function initConfig() {
  92. canCompress = await compressionSupported();
  93. const oldFmtVer = Number(await GM.getValue(`_uucfgver-${cfgDataStore.id}`, NaN));
  94. let data = await cfgDataStore.loadData();
  95. log(`Initialized feature config DataStore (formatVersion = ${cfgDataStore.formatVersion})`);
  96. if(isNaN(oldFmtVer))
  97. info(" !- Config data was initialized with default values");
  98. else if(oldFmtVer !== cfgDataStore.formatVersion) {
  99. try {
  100. await cfgDataStore.setData(data = fixMissingCfgKeys(data));
  101. info(` !- Config data was migrated from version ${oldFmtVer} to ${cfgDataStore.formatVersion}`);
  102. }
  103. catch(err) {
  104. error(" !- Config data migration failed, falling back to default data:", err);
  105. await cfgDataStore.setData(data = cfgDataStore.defaultData);
  106. }
  107. }
  108. emitInterface("bytm:configReady");
  109. return { ...data };
  110. }
  111. /**
  112. * Fixes missing keys in the passed config object with their default values and returns a copy of the fixed object.
  113. * Returns a copy of the originally passed object if nothing needs to be fixed.
  114. */
  115. export function fixMissingCfgKeys(cfg: Partial<FeatureConfig>): FeatureConfig {
  116. cfg = { ...cfg };
  117. const passedKeys = Object.keys(cfg);
  118. const defaultKeys = Object.keys(defaultData);
  119. const missingKeys = defaultKeys.filter(k => !passedKeys.includes(k));
  120. if(missingKeys.length > 0) {
  121. info("Fixed missing feature config keys:", missingKeys);
  122. for(const key of missingKeys)
  123. cfg[key as keyof FeatureConfig] = defaultData[key as keyof FeatureConfig] as never;
  124. }
  125. return cfg as FeatureConfig;
  126. }
  127. /** Returns the current feature config from the in-memory cache as a copy */
  128. export function getFeatures(): FeatureConfig {
  129. return cfgDataStore.getData();
  130. }
  131. /** Returns the value of the feature with the given key from the in-memory cache, as a copy */
  132. export function getFeature<TKey extends FeatureKey>(key: TKey): FeatureConfig[TKey] {
  133. return cfgDataStore.getData()[key];
  134. }
  135. /** Saves the feature config synchronously to the in-memory cache and asynchronously to the persistent storage */
  136. export function setFeatures(featureConf: FeatureConfig) {
  137. const res = cfgDataStore.setData(featureConf);
  138. emitSiteEvent("configChanged", cfgDataStore.getData());
  139. info("Saved new feature config:", featureConf);
  140. return res;
  141. }
  142. /** Saves the default feature config synchronously to the in-memory cache and asynchronously to persistent storage */
  143. export function setDefaultFeatures() {
  144. const res = cfgDataStore.saveDefaultData();
  145. emitSiteEvent("configChanged", cfgDataStore.getData());
  146. info("Reset feature config to its default values");
  147. return res;
  148. }
  149. /** Clears the feature config from the persistent storage - since the cache will be out of whack, this should only be run before a site re-/unload */
  150. export async function clearConfig() {
  151. await cfgDataStore.deleteData();
  152. info("Deleted config from persistent storage");
  153. }