index.ts 11 KB

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