index.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  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. * | `hidden` | if true, the feature will not be shown in the settings - default is undefined (false) |
  47. * | `min` | Only if type is `number` or `slider` - Overwrites the default of the `min` property of the HTML input element |
  48. * | `max` | Only if type is `number` or `slider` - Overwrites the default of the `max` property of the HTML input element |
  49. * | `step` | Only if type is `number` or `slider` - Overwrites the default of the `step` property of the HTML input element |
  50. * | `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" |
  51. *
  52. * **Notes:**
  53. * - If no `disable()` or `change()` function is present, the page needs to be reloaded for the changes to take effect
  54. */
  55. export const featInfo = {
  56. //#SECTION layout
  57. removeUpgradeTab: {
  58. type: "toggle",
  59. category: "layout",
  60. default: true,
  61. enable: noopTODO,
  62. },
  63. volumeSliderLabel: {
  64. type: "toggle",
  65. category: "layout",
  66. default: true,
  67. enable: noopTODO,
  68. disable: noopTODO,
  69. },
  70. volumeSliderSize: {
  71. type: "number",
  72. category: "layout",
  73. min: 50,
  74. max: 500,
  75. step: 5,
  76. default: 150,
  77. unit: "px",
  78. enable: noopTODO,
  79. change: noopTODO,
  80. },
  81. volumeSliderStep: {
  82. type: "slider",
  83. category: "layout",
  84. min: 1,
  85. max: 25,
  86. default: 2,
  87. unit: "%",
  88. enable: noopTODO,
  89. change: noopTODO,
  90. },
  91. volumeSliderScrollStep: {
  92. type: "slider",
  93. category: "layout",
  94. min: 1,
  95. max: 25,
  96. default: 10,
  97. unit: "%",
  98. enable: noopTODO,
  99. change: noopTODO,
  100. },
  101. watermarkEnabled: {
  102. type: "toggle",
  103. category: "layout",
  104. default: true,
  105. enable: noopTODO,
  106. disable: noopTODO,
  107. },
  108. removeShareTrackingParam: {
  109. type: "toggle",
  110. category: "layout",
  111. default: true,
  112. enable: noopTODO,
  113. disable: noopTODO,
  114. },
  115. fixSpacing: {
  116. type: "toggle",
  117. category: "layout",
  118. default: true,
  119. enable: noopTODO,
  120. disable: noopTODO,
  121. },
  122. scrollToActiveSongBtn: {
  123. type: "toggle",
  124. category: "layout",
  125. default: true,
  126. enable: noopTODO,
  127. disable: noopTODO,
  128. },
  129. //#SECTION song lists
  130. lyricsQueueButton: {
  131. type: "toggle",
  132. category: "songLists",
  133. default: true,
  134. enable: noopTODO,
  135. disable: noopTODO,
  136. },
  137. deleteFromQueueButton: {
  138. type: "toggle",
  139. category: "songLists",
  140. default: true,
  141. enable: noopTODO,
  142. disable: noopTODO,
  143. },
  144. listButtonsPlacement: {
  145. type: "select",
  146. category: "songLists",
  147. options: () => [
  148. { value: "queueOnly", label: t("list_button_placement_queue_only") },
  149. { value: "everywhere", label: t("list_button_placement_everywhere") },
  150. ],
  151. default: "everywhere",
  152. enable: noopTODO,
  153. disable: noopTODO,
  154. },
  155. //#SECTION behavior
  156. disableBeforeUnloadPopup: {
  157. type: "toggle",
  158. category: "behavior",
  159. default: false,
  160. enable: noopTODO,
  161. },
  162. closeToastsTimeout: {
  163. type: "number",
  164. category: "behavior",
  165. min: 0,
  166. max: 30,
  167. step: 0.5,
  168. default: 0,
  169. unit: "s",
  170. enable: noopTODO,
  171. change: noopTODO,
  172. },
  173. rememberSongTime: {
  174. type: "toggle",
  175. category: "behavior",
  176. default: true,
  177. enable: noopTODO,
  178. disable: noopTODO, // TODO: feasible?
  179. helpText: () => tp("feature_helptext_rememberSongTime", remSongMinPlayTime, remSongMinPlayTime)
  180. },
  181. rememberSongTimeSites: {
  182. type: "select",
  183. category: "behavior",
  184. options: () => [
  185. { value: "all", label: t("remember_song_time_sites_all") },
  186. { value: "yt", label: t("remember_song_time_sites_yt") },
  187. { value: "ytm", label: t("remember_song_time_sites_ytm") },
  188. ],
  189. default: "ytm",
  190. enable: noopTODO,
  191. change: noopTODO,
  192. },
  193. lockVolume: {
  194. type: "toggle",
  195. category: "behavior",
  196. default: false,
  197. enable: () => noopTODO,
  198. disable: () => noopTODO,
  199. },
  200. lockVolumeLevel: {
  201. type: "slider",
  202. category: "behavior",
  203. min: 0,
  204. max: 100,
  205. step: 1,
  206. default: 100,
  207. unit: "%",
  208. enable: noop,
  209. change: () => noopTODO,
  210. },
  211. //#SECTION input
  212. arrowKeySupport: {
  213. type: "toggle",
  214. category: "input",
  215. default: true,
  216. enable: noopTODO,
  217. disable: noopTODO,
  218. },
  219. arrowKeySkipBy: {
  220. type: "number",
  221. category: "input",
  222. min: 0.5,
  223. max: 60,
  224. step: 0.5,
  225. default: 5,
  226. enable: noopTODO,
  227. change: noopTODO,
  228. },
  229. switchBetweenSites: {
  230. type: "toggle",
  231. category: "input",
  232. default: true,
  233. enable: noopTODO,
  234. disable: noopTODO,
  235. },
  236. switchSitesHotkey: {
  237. type: "hotkey",
  238. category: "input",
  239. default: {
  240. code: "F9",
  241. shift: false,
  242. ctrl: false,
  243. alt: false,
  244. },
  245. enable: noopTODO,
  246. change: noopTODO,
  247. },
  248. anchorImprovements: {
  249. type: "toggle",
  250. category: "input",
  251. default: true,
  252. enable: noopTODO,
  253. disable: noopTODO,
  254. },
  255. numKeysSkipToTime: {
  256. type: "toggle",
  257. category: "input",
  258. default: true,
  259. enable: noopTODO,
  260. disable: noopTODO,
  261. },
  262. //#SECTION lyrics
  263. geniusLyrics: {
  264. type: "toggle",
  265. category: "lyrics",
  266. default: true,
  267. enable: noopTODO,
  268. disable: noopTODO,
  269. },
  270. geniUrlBase: {
  271. type: "text",
  272. category: "lyrics",
  273. default: "https://api.sv443.net/geniurl",
  274. normalize: (val: string) => val.trim().replace(/\/+$/, ""),
  275. advanced: true,
  276. // TODO: to be reworked or removed in the big menu rework
  277. textAdornment: getAdvancedModeAdornment,
  278. },
  279. geniUrlToken: {
  280. type: "text",
  281. valueHidden: true,
  282. category: "lyrics",
  283. default: "",
  284. normalize: (val: string) => val.trim(),
  285. advanced: true,
  286. // TODO: to be reworked or removed in the big menu rework
  287. textAdornment: getAdvancedModeAdornment,
  288. },
  289. lyricsCacheMaxSize: {
  290. type: "slider",
  291. category: "lyrics",
  292. default: 500,
  293. min: 50,
  294. max: 2000,
  295. step: 50,
  296. unit: (val: number) => tp("unit_entries", val),
  297. enable: noopTODO,
  298. change: noopTODO,
  299. advanced: true,
  300. // TODO: to be reworked or removed in the big menu rework
  301. textAdornment: getAdvancedModeAdornment,
  302. },
  303. lyricsCacheTTL: {
  304. type: "slider",
  305. category: "lyrics",
  306. default: 21,
  307. min: 3,
  308. max: 100,
  309. step: 1,
  310. unit: (val: number) => tp("unit_days", val),
  311. enable: noopTODO,
  312. change: noopTODO,
  313. advanced: true,
  314. // TODO: to be reworked or removed in the big menu rework
  315. textAdornment: getAdvancedModeAdornment,
  316. },
  317. clearLyricsCache: {
  318. type: "button",
  319. category: "lyrics",
  320. default: undefined,
  321. click() {
  322. const entries = getLyricsCache().length;
  323. if(confirm(tp("lyrics_clear_cache_confirm_prompt", entries, entries))) {
  324. clearLyricsCache();
  325. alert(t("lyrics_clear_cache_success"));
  326. }
  327. },
  328. advanced: true,
  329. // TODO: to be reworked or removed in the big menu rework
  330. textAdornment: getAdvancedModeAdornment,
  331. },
  332. //#SECTION general
  333. locale: {
  334. type: "select",
  335. category: "general",
  336. options: localeOptions,
  337. default: getPreferredLocale(),
  338. enable: noopTODO,
  339. // TODO: to be reworked or removed in the big menu rework
  340. textAdornment: async () => await resourceToHTMLString("icon-globe") ?? "",
  341. },
  342. versionCheck: {
  343. type: "toggle",
  344. category: "general",
  345. default: true,
  346. enable: noopTODO,
  347. disable: noopTODO,
  348. },
  349. checkVersionNow: {
  350. type: "button",
  351. category: "general",
  352. default: undefined,
  353. click: debounce(() => doVersionCheck(true), 750),
  354. },
  355. logLevel: {
  356. type: "select",
  357. category: "general",
  358. options: () => [
  359. { value: 0, label: t("log_level_debug") },
  360. { value: 1, label: t("log_level_info") },
  361. ],
  362. default: 1,
  363. enable: noopTODO,
  364. },
  365. advancedMode: {
  366. type: "toggle",
  367. category: "general",
  368. default: mode === "development",
  369. enable: noopTODO,
  370. disable: noopTODO,
  371. // TODO: to be reworked or removed in the big menu rework
  372. textAdornment: () => getFeatures().advancedMode ? getAdvancedModeAdornment() : undefined,
  373. },
  374. } as const satisfies FeatureInfo;
  375. async function getAdvancedModeAdornment() {
  376. return `<span class="bytm-advanced-mode-icon" title="${t("advanced_mode")}">${await resourceToHTMLString("icon-advanced_mode") ?? ""}</span>`;
  377. }
  378. function noop() {
  379. void 0;
  380. }
  381. function noopTODO() {
  382. void 0;
  383. }