config.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  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, FeatureKey } 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 (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([
  24. "removeShareTrackingParam", "numKeysSkipToTime",
  25. "fixSpacing", "scrollToActiveSongBtn", "logLevel",
  26. ], oldData),
  27. // 3 -> 4 (v1.1)
  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 (v1.2)
  46. 5: (oldData: FeatureConfig) => ({
  47. ...useDefaultConfig([
  48. "geniUrlBase", "geniUrlToken",
  49. "lyricsCacheMaxSize", "lyricsCacheTTL",
  50. "clearLyricsCache", "advancedMode",
  51. "checkVersionNow", "advancedLyricsFilter",
  52. "rememberSongTimeDuration", "rememberSongTimeReduction",
  53. "rememberSongTimeMinPlayTime", "volumeSharedBetweenTabs",
  54. "setInitialTabVolume", "initialTabVolumeLevel",
  55. "thumbnailOverlayBehavior", "thumbnailOverlayToggleBtnShown",
  56. "thumbnailOverlayShowIndicator", "thumbnailOverlayIndicatorOpacity",
  57. "thumbnailOverlayImageFit", "removeShareTrackingParamSites",
  58. ], oldData),
  59. }),
  60. // TODO: once advanced filtering is fully implemented, clear cache on migration to fv6
  61. // 5 -> 6 (v1.3)
  62. // 6: (oldData: FeatureConfig) =>
  63. } as const satisfies DataMigrationsDict;
  64. export const defaultData = (Object.keys(featInfo) as (keyof typeof featInfo)[])
  65. .reduce<Partial<FeatureConfig>>((acc, key) => {
  66. // @ts-ignore
  67. acc[key] = featInfo?.[key]?.default as unknown as undefined;
  68. return acc;
  69. }, {}) as FeatureConfig;
  70. /** 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 */
  71. function useDefaultConfig(resetKeys: (keyof typeof featInfo)[], baseData?: FeatureConfig): Partial<FeatureConfig> {
  72. const newData = { ...defaultData, ...(baseData ?? {}) };
  73. for(const key of resetKeys) // @ts-ignore
  74. newData[key] = featInfo?.[key]?.default as never; // typescript funny moments
  75. return newData;
  76. }
  77. let canCompress = true;
  78. const bytmCfgStore = new DataStore({
  79. id: "bytm-config",
  80. formatVersion,
  81. defaultData,
  82. migrations,
  83. encodeData: (data) => canCompress ? compress(data, compressionFormat, "string") : data,
  84. decodeData: (data) => canCompress ? decompress(data, compressionFormat, "string") : data,
  85. });
  86. /** Initializes the DataStore instance and loads persistent data into memory */
  87. export async function initConfig() {
  88. canCompress = await compressionSupported();
  89. const oldFmtVer = Number(await GM.getValue(`_uucfgver-${bytmCfgStore.id}`, NaN));
  90. const data = await bytmCfgStore.loadData();
  91. log(`Initialized feature config DataStore (formatVersion = ${bytmCfgStore.formatVersion})`);
  92. if(isNaN(oldFmtVer))
  93. info(" !- Config data was initialized with default values");
  94. else if(oldFmtVer !== bytmCfgStore.formatVersion)
  95. info(` !- Config data was migrated from version ${oldFmtVer} to ${bytmCfgStore.formatVersion}`);
  96. emitInterface("bytm:configReady", getFeaturesInterface());
  97. return { ...data };
  98. }
  99. /** Returns the current feature config from the in-memory cache as a copy */
  100. export function getFeatures(): FeatureConfig {
  101. return bytmCfgStore.getData();
  102. }
  103. /** Returns the value of the feature with the given key from the in-memory cache, as a copy */
  104. export function getFeature<TKey extends FeatureKey>(key: TKey): FeatureConfig[TKey] {
  105. return bytmCfgStore.getData()[key];
  106. }
  107. /** Saves the feature config synchronously to the in-memory cache and asynchronously to the persistent storage */
  108. export function setFeatures(featureConf: FeatureConfig) {
  109. const res = bytmCfgStore.setData(featureConf);
  110. emitSiteEvent("configChanged", bytmCfgStore.getData());
  111. info("Saved new feature config:", featureConf);
  112. return res;
  113. }
  114. /** Saves the default feature config synchronously to the in-memory cache and asynchronously to persistent storage */
  115. export function setDefaultFeatures() {
  116. const res = bytmCfgStore.saveDefaultData();
  117. emitSiteEvent("configChanged", bytmCfgStore.getData());
  118. info("Reset feature config to its default values");
  119. return res;
  120. }
  121. /** 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 */
  122. export async function clearConfig() {
  123. await bytmCfgStore.deleteData();
  124. info("Deleted config from persistent storage");
  125. }