config.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import { DataStore, compress, type DataMigrationsDict, decompress } from "@sv443-network/userutils";
  2. import { featInfo } from "./features/index";
  3. import { compressionSupported, info, log } from "./utils";
  4. import { emitSiteEvent } from "./siteEvents";
  5. import { compressionFormat } from "./constants";
  6. import type { FeatureConfig } from "./types";
  7. import { emitInterface, getFeaturesInterface } from "./interface";
  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
  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
  23. 3: (oldData: FeatureConfig) => useDefaultConfig([
  24. "removeShareTrackingParam", "numKeysSkipToTime",
  25. "fixSpacing", "scrollToActiveSongBtn", "logLevel",
  26. ], oldData),
  27. // 3 -> 4
  28. 4: (oldData: FeatureConfig) => {
  29. const oldSwitchSitesHotkey = oldData.switchSitesHotkey as Record<string, unknown>;
  30. return {
  31. ...useDefaultConfig([
  32. "rememberSongTime", "rememberSongTimeSites",
  33. "volumeSliderScrollStep", "locale", "versionCheck",
  34. ], oldData),
  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
  46. 5: (oldData: FeatureConfig) => useDefaultConfig([
  47. "geniUrlBase", "geniUrlToken",
  48. "lyricsCacheMaxSize", "lyricsCacheTTL",
  49. "clearLyricsCache", "advancedMode",
  50. "checkVersionNow", "advancedLyricsFilter",
  51. "rememberSongTimeDuration", "rememberSongTimeReduction",
  52. "rememberSongTimeMinPlayTime", "volumeSharedBetweenTabs",
  53. "setInitialTabVolume", "initialTabVolumeLevel",
  54. ], oldData),
  55. } as const satisfies DataMigrationsDict;
  56. // TODO: once advanced filtering is fully implemented, clear cache on migration (to v6)
  57. /** Uses the passed {@linkcode oldData} as the base (if given) and sets all passed {@linkcode keys} to their feature default - returns a copy of the object */
  58. function useDefaultConfig(keys: (keyof typeof featInfo)[], oldData?: FeatureConfig): Partial<FeatureConfig> {
  59. const newData = { ...(oldData ?? {}) };
  60. for(const key of keys)
  61. newData[key as keyof typeof featInfo] = getFeatureDefault(key as keyof typeof featInfo) as unknown as never;
  62. return newData;
  63. }
  64. /** Returns the default value for the given feature key */
  65. function getFeatureDefault<TKey extends keyof typeof featInfo>(key: TKey): typeof featInfo[TKey]["default"] {
  66. return featInfo[key].default;
  67. }
  68. export const defaultData = (Object.keys(featInfo) as (keyof typeof featInfo)[])
  69. .reduce<Partial<FeatureConfig>>((acc, key) => {
  70. acc[key] = featInfo[key].default as unknown as undefined;
  71. return acc;
  72. }, {}) as FeatureConfig;
  73. let canCompress = true;
  74. const bytmCfgStore = new DataStore({
  75. id: "bytm-config",
  76. formatVersion,
  77. defaultData,
  78. migrations,
  79. encodeData: (data) => canCompress ? compress(data, compressionFormat, "string") : data,
  80. decodeData: (data) => canCompress ? decompress(data, compressionFormat, "string") : data,
  81. });
  82. /** Initializes the DataStore instance and loads persistent data into memory */
  83. export async function initConfig() {
  84. canCompress = await compressionSupported();
  85. const oldFmtVer = Number(await GM.getValue(`_uucfgver-${bytmCfgStore.id}`, NaN));
  86. const data = await bytmCfgStore.loadData();
  87. log(`Initialized DataStore (format version = ${bytmCfgStore.formatVersion})`);
  88. if(isNaN(oldFmtVer))
  89. info("Config data initialized with default values");
  90. else if(oldFmtVer !== bytmCfgStore.formatVersion)
  91. info(`Config data migrated from version ${oldFmtVer} to ${bytmCfgStore.formatVersion}`);
  92. emitInterface("bytm:configReady", getFeaturesInterface());
  93. return { ...data };
  94. }
  95. /** Returns the current feature config from the in-memory cache as a copy */
  96. export function getFeatures() {
  97. return bytmCfgStore.getData();
  98. }
  99. /** Saves the feature config synchronously to the in-memory cache and asynchronously to the persistent storage */
  100. export function saveFeatures(featureConf: FeatureConfig) {
  101. const res = bytmCfgStore.setData(featureConf);
  102. emitSiteEvent("configChanged", bytmCfgStore.getData());
  103. info("Saved new feature config:", featureConf);
  104. return res;
  105. }
  106. /** Saves the default feature config synchronously to the in-memory cache and asynchronously to persistent storage */
  107. export function setDefaultFeatures() {
  108. const res = bytmCfgStore.saveDefaultData();
  109. emitSiteEvent("configChanged", bytmCfgStore.getData());
  110. info("Reset feature config to its default values");
  111. return res;
  112. }
  113. /** 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 */
  114. export async function clearConfig() {
  115. await bytmCfgStore.deleteData();
  116. info("Deleted config from persistent storage");
  117. }