index.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. import { debounce } from "@sv443-network/userutils";
  2. import { getPreferredLocale, resourceToHTMLString, t, tp } from "../utils";
  3. import langMapping from "../../assets/locales.json" assert { type: "json" };
  4. import { remSongMinPlayTime } from "./behavior";
  5. import { clearLyricsCache, getLyricsCache } from "./lyricsCache";
  6. import { doVersionCheck } from "./versionCheck";
  7. import { mode } from "../constants";
  8. import { getFeatures } from "../config";
  9. import { FeatureInfo } from "../types";
  10. export * from "./layout";
  11. export * from "./behavior";
  12. export * from "./input";
  13. export * from "./lyrics";
  14. export * from "./lyricsCache";
  15. export * from "./songLists";
  16. export * from "./versionCheck";
  17. type SelectOption = { value: number | string, label: string };
  18. //#MARKER feature dependencies
  19. const localeOptions = Object.entries(langMapping).reduce((a, [locale, { name }]) => {
  20. return [...a, {
  21. value: locale,
  22. label: name,
  23. }];
  24. }, [] as SelectOption[])
  25. .sort((a, b) => a.label.localeCompare(b.label));
  26. //#MARKER features
  27. /**
  28. * Contains all possible features with their default values and other configuration.
  29. *
  30. * **Required props:**
  31. * | Property | Description |
  32. * | :-- | :-- |
  33. * | `type` | type of the feature configuration element - use autocomplete or check `FeatureTypeProps` in `src/types.ts` |
  34. * | `category` | category of the feature - use autocomplete or check `FeatureCategory` in `src/types.ts` |
  35. * | `default` | default value of the feature - type of the value depends on the given `type` |
  36. * | `enable(value: any)` | function that will be called when the feature is enabled / initialized for the first time |
  37. *
  38. * **Optional props:**
  39. * | Property | Description |
  40. * | :-- | :-- |
  41. * | `disable(newValue: any)` | for type `toggle` only - function that will be called when the feature is disabled - can be a synchronous or asynchronous function |
  42. * | `change(prevValue: any, newValue: any)` | for types `number`, `select`, `slider` and `hotkey` only - function that will be called when the value is changed |
  43. * | `click: () => void` | for type `button` only - function that will be called when the button is clicked |
  44. * | `helpText(): string / () => string` | function that returns an HTML string or the literal string itself that will be the help text for this feature - writing as function is useful for pluralizing or inserting values into the translation at runtime - if not set, translation with key `feature_helptext_featureKey` will be used instead, if available |
  45. * | `textAdornment(): string / Promise<string>` | function that returns an HTML string that will be appended to the text in the config menu as an adornment element - TODO: to be replaced in the big menu rework |
  46. * | `advanced` | if true, the feature will only be shown if the advanced mode feature has been turned on |
  47. * | `hidden` | if true, the feature will not be shown in the settings - default is undefined (false) |
  48. * | `min` | Only if type is `number` or `slider` - Overwrites the default of the `min` property of the HTML input element |
  49. * | `max` | Only if type is `number` or `slider` - Overwrites the default of the `max` property of the HTML input element |
  50. * | `step` | Only if type is `number` or `slider` - Overwrites the default of the `step` property of the HTML input element |
  51. * | `unit: string / (val: number) => string` | Only if type is `number` or `slider` - The unit text that is displayed next to the input element, i.e. "px" |
  52. *
  53. * **Notes:**
  54. * - If no `disable()` or `change()` function is present, the page needs to be reloaded for the changes to take effect
  55. */
  56. export const featInfo = {
  57. //#SECTION layout
  58. removeUpgradeTab: {
  59. type: "toggle",
  60. category: "layout",
  61. default: true,
  62. enable: noopTODO,
  63. },
  64. volumeSliderLabel: {
  65. type: "toggle",
  66. category: "layout",
  67. default: true,
  68. enable: noopTODO,
  69. disable: noopTODO,
  70. },
  71. volumeSliderSize: {
  72. type: "number",
  73. category: "layout",
  74. min: 50,
  75. max: 500,
  76. step: 5,
  77. default: 150,
  78. unit: "px",
  79. enable: noopTODO,
  80. change: noopTODO,
  81. },
  82. volumeSliderStep: {
  83. type: "slider",
  84. category: "layout",
  85. min: 1,
  86. max: 25,
  87. default: 2,
  88. unit: "%",
  89. enable: noopTODO,
  90. change: noopTODO,
  91. },
  92. volumeSliderScrollStep: {
  93. type: "slider",
  94. category: "layout",
  95. min: 1,
  96. max: 25,
  97. default: 10,
  98. unit: "%",
  99. enable: noopTODO,
  100. change: noopTODO,
  101. },
  102. watermarkEnabled: {
  103. type: "toggle",
  104. category: "layout",
  105. default: true,
  106. enable: noopTODO,
  107. disable: noopTODO,
  108. },
  109. removeShareTrackingParam: {
  110. type: "toggle",
  111. category: "layout",
  112. default: true,
  113. enable: noopTODO,
  114. disable: noopTODO,
  115. },
  116. fixSpacing: {
  117. type: "toggle",
  118. category: "layout",
  119. default: true,
  120. enable: noopTODO,
  121. disable: noopTODO,
  122. },
  123. scrollToActiveSongBtn: {
  124. type: "toggle",
  125. category: "layout",
  126. default: true,
  127. enable: noopTODO,
  128. disable: noopTODO,
  129. },
  130. //#SECTION song lists
  131. lyricsQueueButton: {
  132. type: "toggle",
  133. category: "songLists",
  134. default: true,
  135. enable: noopTODO,
  136. disable: noopTODO,
  137. },
  138. deleteFromQueueButton: {
  139. type: "toggle",
  140. category: "songLists",
  141. default: true,
  142. enable: noopTODO,
  143. disable: noopTODO,
  144. },
  145. listButtonsPlacement: {
  146. type: "select",
  147. category: "songLists",
  148. options: () => [
  149. { value: "queueOnly", label: t("list_button_placement_queue_only") },
  150. { value: "everywhere", label: t("list_button_placement_everywhere") },
  151. ],
  152. default: "everywhere",
  153. enable: noopTODO,
  154. disable: noopTODO,
  155. },
  156. //#SECTION behavior
  157. disableBeforeUnloadPopup: {
  158. type: "toggle",
  159. category: "behavior",
  160. default: false,
  161. enable: noopTODO,
  162. },
  163. closeToastsTimeout: {
  164. type: "number",
  165. category: "behavior",
  166. min: 0,
  167. max: 30,
  168. step: 0.5,
  169. default: 0,
  170. unit: "s",
  171. enable: noopTODO,
  172. change: noopTODO,
  173. },
  174. rememberSongTime: {
  175. type: "toggle",
  176. category: "behavior",
  177. default: true,
  178. enable: noopTODO,
  179. disable: noopTODO, // TODO: feasible?
  180. helpText: () => tp("feature_helptext_rememberSongTime", remSongMinPlayTime, remSongMinPlayTime)
  181. },
  182. rememberSongTimeSites: {
  183. type: "select",
  184. category: "behavior",
  185. options: () => [
  186. { value: "all", label: t("remember_song_time_sites_all") },
  187. { value: "yt", label: t("remember_song_time_sites_yt") },
  188. { value: "ytm", label: t("remember_song_time_sites_ytm") },
  189. ],
  190. default: "ytm",
  191. enable: noopTODO,
  192. change: noopTODO,
  193. },
  194. lockVolume: {
  195. type: "toggle",
  196. category: "behavior",
  197. default: false,
  198. enable: () => noopTODO,
  199. disable: () => noopTODO,
  200. },
  201. lockVolumeLevel: {
  202. type: "slider",
  203. category: "behavior",
  204. min: 0,
  205. max: 100,
  206. step: 1,
  207. default: 100,
  208. unit: "%",
  209. enable: noop,
  210. change: () => noopTODO,
  211. },
  212. //#SECTION input
  213. arrowKeySupport: {
  214. type: "toggle",
  215. category: "input",
  216. default: true,
  217. enable: noopTODO,
  218. disable: noopTODO,
  219. },
  220. arrowKeySkipBy: {
  221. type: "number",
  222. category: "input",
  223. min: 0.5,
  224. max: 60,
  225. step: 0.5,
  226. default: 5,
  227. enable: noopTODO,
  228. change: noopTODO,
  229. },
  230. switchBetweenSites: {
  231. type: "toggle",
  232. category: "input",
  233. default: true,
  234. enable: noopTODO,
  235. disable: noopTODO,
  236. },
  237. switchSitesHotkey: {
  238. type: "hotkey",
  239. category: "input",
  240. default: {
  241. code: "F9",
  242. shift: false,
  243. ctrl: false,
  244. alt: false,
  245. },
  246. enable: noopTODO,
  247. change: noopTODO,
  248. },
  249. anchorImprovements: {
  250. type: "toggle",
  251. category: "input",
  252. default: true,
  253. enable: noopTODO,
  254. disable: noopTODO,
  255. },
  256. numKeysSkipToTime: {
  257. type: "toggle",
  258. category: "input",
  259. default: true,
  260. enable: noopTODO,
  261. disable: noopTODO,
  262. },
  263. //#SECTION lyrics
  264. geniusLyrics: {
  265. type: "toggle",
  266. category: "lyrics",
  267. default: true,
  268. enable: noopTODO,
  269. disable: noopTODO,
  270. },
  271. geniUrlBase: {
  272. type: "text",
  273. category: "lyrics",
  274. default: "https://api.sv443.net/geniurl",
  275. normalize: (val: string) => val.trim().replace(/\/+$/, ""),
  276. advanced: true,
  277. // TODO: to be reworked or removed in the big menu rework
  278. textAdornment: getAdvancedModeAdornment,
  279. },
  280. geniUrlToken: {
  281. type: "text",
  282. valueHidden: true,
  283. category: "lyrics",
  284. default: "",
  285. normalize: (val: string) => val.trim(),
  286. advanced: true,
  287. // TODO: to be reworked or removed in the big menu rework
  288. textAdornment: getAdvancedModeAdornment,
  289. },
  290. lyricsCacheMaxSize: {
  291. type: "slider",
  292. category: "lyrics",
  293. default: 500,
  294. min: 50,
  295. max: 2000,
  296. step: 50,
  297. unit: (val: number) => tp("unit_entries", val),
  298. enable: noopTODO,
  299. change: noopTODO,
  300. advanced: true,
  301. // TODO: to be reworked or removed in the big menu rework
  302. textAdornment: getAdvancedModeAdornment,
  303. },
  304. lyricsCacheTTL: {
  305. type: "slider",
  306. category: "lyrics",
  307. default: 21,
  308. min: 1,
  309. max: 100,
  310. step: 1,
  311. unit: (val: number) => tp("unit_days", val),
  312. enable: noopTODO,
  313. change: noopTODO,
  314. advanced: true,
  315. // TODO: to be reworked or removed in the big menu rework
  316. textAdornment: getAdvancedModeAdornment,
  317. },
  318. clearLyricsCache: {
  319. type: "button",
  320. category: "lyrics",
  321. default: undefined,
  322. click() {
  323. const entries = getLyricsCache().length;
  324. if(confirm(tp("lyrics_clear_cache_confirm_prompt", entries, entries))) {
  325. clearLyricsCache();
  326. alert(t("lyrics_clear_cache_success"));
  327. }
  328. },
  329. advanced: true,
  330. // TODO: to be reworked or removed in the big menu rework
  331. textAdornment: getAdvancedModeAdornment,
  332. },
  333. lyricsFuzzyFilter: {
  334. type: "toggle",
  335. category: "lyrics",
  336. default: false,
  337. enable: noopTODO,
  338. disable: noopTODO,
  339. // TODO: use dialog here?
  340. change: () => confirm(t("lyrics_cache_changed_clear_confirm")) && clearLyricsCache(),
  341. advanced: true,
  342. // TODO: to be reworked or removed in the big menu rework
  343. textAdornment: getAdvancedModeAdornment,
  344. },
  345. //#SECTION general
  346. locale: {
  347. type: "select",
  348. category: "general",
  349. options: localeOptions,
  350. default: getPreferredLocale(),
  351. enable: noopTODO,
  352. // TODO: to be reworked or removed in the big menu rework
  353. textAdornment: async () => await resourceToHTMLString("icon-globe") ?? "",
  354. },
  355. versionCheck: {
  356. type: "toggle",
  357. category: "general",
  358. default: true,
  359. enable: noopTODO,
  360. disable: noopTODO,
  361. },
  362. checkVersionNow: {
  363. type: "button",
  364. category: "general",
  365. default: undefined,
  366. click: debounce(() => doVersionCheck(true), 750),
  367. },
  368. logLevel: {
  369. type: "select",
  370. category: "general",
  371. options: () => [
  372. { value: 0, label: t("log_level_debug") },
  373. { value: 1, label: t("log_level_info") },
  374. ],
  375. default: 1,
  376. enable: noopTODO,
  377. },
  378. advancedMode: {
  379. type: "toggle",
  380. category: "general",
  381. default: mode === "development",
  382. enable: noopTODO,
  383. disable: noopTODO,
  384. // TODO: to be reworked or removed in the big menu rework
  385. textAdornment: () => getFeatures().advancedMode ? getAdvancedModeAdornment() : undefined,
  386. },
  387. } as const satisfies FeatureInfo;
  388. async function getAdvancedModeAdornment() {
  389. return `<span class="bytm-advanced-mode-icon" title="${t("advanced_mode")}">${await resourceToHTMLString("icon-advanced_mode") ?? ""}</span>`;
  390. }
  391. function noop() {
  392. void 0;
  393. }
  394. function noopTODO() {
  395. void 0;
  396. }