index.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833
  1. import { consumeStringGen, type StringGen } from "@sv443-network/userutils";
  2. import { formatNumber, getLocale, getPreferredLocale, getResourceUrl, reloadTab, resourceAsString, t, tp } from "../utils/index.js";
  3. import { clearLyricsCache, getLyricsCache } from "./lyricsCache.js";
  4. import { doVersionCheck } from "./versionCheck.js";
  5. import { getFeature, promptResetConfig } from "../config.js";
  6. import { FeatureInfo, LogLevel, type ColorLightnessPref, type ResourceKey, type SiteSelection, type SiteSelectionOrNone } from "../types.js";
  7. import { emitSiteEvent } from "../siteEvents.js";
  8. import langMapping from "../../assets/locales.json" with { type: "json" };
  9. import { showIconToast } from "../components/toast.js";
  10. import { mode } from "../constants.js";
  11. import { getStoreSerializer } from "../serializer.js";
  12. import { getAutoLikeDialog } from "../dialogs/autoLike.js";
  13. import { showPrompt } from "../dialogs/prompt.js";
  14. import { getPluginListDialog } from "../dialogs/pluginList.js";
  15. //#region re-exports
  16. export * from "./layout.js";
  17. export * from "./behavior.js";
  18. export * from "./input.js";
  19. export * from "./integrations.js";
  20. export * from "./lyrics.js";
  21. export * from "./lyricsCache.js";
  22. export * from "./songLists.js";
  23. export * from "./versionCheck.js";
  24. export * from "./volume.js";
  25. //#region misc
  26. function noop() {
  27. void 0;
  28. }
  29. void [noop];
  30. //#region adornments
  31. type AdornmentFunc =
  32. | ((...args: any[]) => Promise<string | undefined>)
  33. | Promise<string | undefined>;
  34. /** Creates an HTML string for the given adornment properties */
  35. const getAdornHtml = async (className: string, title: StringGen | undefined, resource: ResourceKey, extraAttributes?: StringGen) => {
  36. title = title ? await consumeStringGen(title) : undefined;
  37. extraAttributes = extraAttributes ? await consumeStringGen(extraAttributes) : undefined;
  38. return `<span class="${className} bytm-adorn-icon" ${title ? `title="${title}" aria-label="${title}"` : ""}${extraAttributes ? ` ${extraAttributes}` : ""}>${await resourceAsString(resource) ?? ""}</span>`;
  39. };
  40. /** Combines multiple async functions or promises that resolve with an adornment HTML string into a single string */
  41. const combineAdornments = (
  42. adornments: Array<AdornmentFunc>
  43. ) => new Promise<string>(
  44. async (resolve) => {
  45. const sortedAdornments = adornments.sort((a, b) => {
  46. const aIndex = adornmentOrder.get(a) ? adornmentOrder.get(a)! : -1;
  47. const bIndex = adornmentOrder.has(b) ? adornmentOrder.get(b)! : -1;
  48. return aIndex - bIndex;
  49. });
  50. const html = [] as string[];
  51. for(const adornment of sortedAdornments) {
  52. const val = typeof adornment === "function"
  53. ? await adornment()
  54. : await adornment;
  55. val && html.push(val);
  56. }
  57. resolve(html.join(""));
  58. }
  59. );
  60. /** Decoration elements that can be added next to the label */
  61. const adornments = {
  62. advanced: async () => getAdornHtml("bytm-advanced-mode-icon", t("advanced_mode"), "icon-advanced_mode"),
  63. experimental: async () => getAdornHtml("bytm-experimental-icon", t("experimental_feature"), "icon-experimental"),
  64. globe: async () => getAdornHtml("bytm-locale-icon", undefined, "icon-globe_small"),
  65. alert: async (title: StringGen) => getAdornHtml("bytm-warning-icon", title, "icon-error", "role=\"alert\""),
  66. reload: async () => getFeature("advancedMode") ? getAdornHtml("bytm-reload-icon", t("feature_requires_reload"), "icon-reload") : undefined,
  67. } satisfies Record<string, AdornmentFunc>;
  68. /** Order of adornment elements in the {@linkcode combineAdornments()} function */
  69. const adornmentOrder = new Map<AdornmentFunc, number>();
  70. adornmentOrder.set(adornments.alert, 0);
  71. adornmentOrder.set(adornments.experimental, 1);
  72. adornmentOrder.set(adornments.globe, 2);
  73. adornmentOrder.set(adornments.reload, 3);
  74. adornmentOrder.set(adornments.advanced, 4);
  75. //#region select options
  76. interface SelectOption<TValue = number | string> {
  77. value: TValue;
  78. label: string;
  79. }
  80. /** Common options for config items of type "select" */
  81. const options = {
  82. siteSelection: (): SelectOption<SiteSelection>[] => [
  83. { value: "all", label: t("site_selection_both_sites") },
  84. { value: "yt", label: t("site_selection_only_yt") },
  85. { value: "ytm", label: t("site_selection_only_ytm") },
  86. ],
  87. siteSelectionOrNone: (): SelectOption<SiteSelectionOrNone>[] => [
  88. { value: "all", label: t("site_selection_both_sites") },
  89. { value: "yt", label: t("site_selection_only_yt") },
  90. { value: "ytm", label: t("site_selection_only_ytm") },
  91. { value: "none", label: t("site_selection_none") },
  92. ],
  93. locale: () => Object.entries(langMapping)
  94. .reduce((a, [locale, { name }]) => {
  95. return [...a, {
  96. value: locale,
  97. label: name,
  98. }];
  99. }, [] as SelectOption[])
  100. .sort((a, b) => a.label.localeCompare(b.label)),
  101. colorLightness: (): SelectOption<ColorLightnessPref>[] => [
  102. { value: "darker", label: t("color_lightness_darker") },
  103. { value: "normal", label: t("color_lightness_normal") },
  104. { value: "lighter", label: t("color_lightness_lighter") },
  105. ],
  106. };
  107. //#region rendering
  108. /** Renders a long number with a thousands separator */
  109. function renderLongNumberValue(val: string, maximumFractionDigits = 0) {
  110. return Number(val).toLocaleString(
  111. getLocale().replace(/_/g, "-"),
  112. {
  113. style: "decimal",
  114. maximumFractionDigits,
  115. }
  116. );
  117. }
  118. //#region features
  119. /**
  120. * Contains all possible features with their default values and other configuration.
  121. *
  122. * **Required props:**
  123. * <!------------------------------------------------------------------------------------------------------------------------------------------------------------------>
  124. * | Property | Description |
  125. * | :----------------------------- | :------------------------------------------------------------------------------------------------------------------------------- |
  126. * | `type: string` | Type of the feature configuration element - use autocomplete or check `FeatureTypeProps` in `src/types.ts` |
  127. * | `category: string` | Category of the feature - use autocomplete or check `FeatureCategory` in `src/types.ts` |
  128. * | `default: unknown` | Default value of the feature - type of the value depends on the given `type` |
  129. * | `enable(value: unknown): void` | (required if reloadRequired = false) - function that will be called when the feature is enabled / initialized for the first time |
  130. * <!------------------------------------------------------------------------------------------------------------------------------------------------------------------>
  131. *
  132. *
  133. * **Optional props:**
  134. * <!------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------->
  135. * | Property | Description |
  136. * | :----------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------|
  137. * | `disable(newValue: unknown): void` | For type `toggle` only - function that will be called when the feature is disabled - can be a synchronous or asynchronous function |
  138. * | `change(key: string, prevValue: unknown, newValue: unknown): void` | For types `number`, `select`, `slider` and `hotkey` only - function that will be called when the value is changed |
  139. * | `click(): void` | For type `button` only - function that will be called when the button is clicked |
  140. * | `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 |
  141. * | `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 |
  142. * | `unit: string \| (val: number) => string` | For types `number` or `slider` only - The unit text that is displayed next to the input element, i.e. " px" - a leading space need to be added too! |
  143. * | `min: number` | For types `number` or `slider` only - Overwrites the default of the `min` property of the HTML input element |
  144. * | `max: number` | For types `number` or `slider` only - Overwrites the default of the `max` property of the HTML input element |
  145. * | `step: number` | For types `number` or `slider` only - Overwrites the default of the `step` property of the HTML input element |
  146. * | `options: SelectOption[] \| () => SelectOption[]` | For type `select` only - function that returns an array of objects with `value` and `label` properties |
  147. * | `reloadRequired: boolean` | If true (default), the page needs to be reloaded for the changes to take effect - if false, `enable()` needs to be provided |
  148. * | `advanced: boolean` | If true, the feature will only be shown if the advanced mode feature has been turned on |
  149. * | `hidden: boolean` | If true, the feature will not be shown in the settings - default is undefined (false) |
  150. * | `valueHidden: boolean` | If true, the value of the feature will be hidden in the settings and via the plugin interface - default is undefined (false) |
  151. * | `normalize(val: unknown): unknown` | Function that will be called to normalize the value before it is saved - useful for trimming strings or other simple operations |
  152. * | `renderValue(val: string): string` | If provided, is used to render the value's label in the config menu |
  153. * <!------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------->
  154. *
  155. * TODO: go through all features and set as many as possible to reloadRequired = false
  156. */
  157. export const featInfo = {
  158. //#region cat:layout
  159. watermarkEnabled: {
  160. type: "toggle",
  161. category: "layout",
  162. default: true,
  163. textAdornment: adornments.reload,
  164. },
  165. removeShareTrackingParam: {
  166. type: "toggle",
  167. category: "layout",
  168. default: true,
  169. textAdornment: adornments.reload,
  170. },
  171. removeShareTrackingParamSites: {
  172. type: "select",
  173. category: "layout",
  174. options: options.siteSelection,
  175. default: "all",
  176. advanced: true,
  177. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  178. },
  179. fixSpacing: {
  180. type: "toggle",
  181. category: "layout",
  182. default: true,
  183. advanced: true,
  184. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  185. },
  186. thumbnailOverlayBehavior: {
  187. type: "select",
  188. category: "layout",
  189. options: () => [
  190. { value: "songsOnly", label: t("thumbnail_overlay_behavior_songs_only") },
  191. { value: "videosOnly", label: t("thumbnail_overlay_behavior_videos_only") },
  192. { value: "always", label: t("thumbnail_overlay_behavior_always") },
  193. { value: "never", label: t("thumbnail_overlay_behavior_never") },
  194. ],
  195. default: "songsOnly",
  196. reloadRequired: false,
  197. enable: noop,
  198. },
  199. thumbnailOverlayToggleBtnShown: {
  200. type: "toggle",
  201. category: "layout",
  202. default: true,
  203. textAdornment: adornments.reload,
  204. },
  205. thumbnailOverlayShowIndicator: {
  206. type: "toggle",
  207. category: "layout",
  208. default: true,
  209. textAdornment: adornments.reload,
  210. },
  211. thumbnailOverlayIndicatorOpacity: {
  212. type: "slider",
  213. category: "layout",
  214. min: 5,
  215. max: 100,
  216. step: 5,
  217. default: 40,
  218. unit: "%",
  219. advanced: true,
  220. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  221. },
  222. thumbnailOverlayImageFit: {
  223. type: "select",
  224. category: "layout",
  225. options: () => [
  226. { value: "cover", label: t("thumbnail_overlay_image_fit_crop") },
  227. { value: "contain", label: t("thumbnail_overlay_image_fit_full") },
  228. { value: "fill", label: t("thumbnail_overlay_image_fit_stretch") },
  229. ],
  230. default: "cover",
  231. advanced: true,
  232. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  233. },
  234. hideCursorOnIdle: {
  235. type: "toggle",
  236. category: "layout",
  237. default: true,
  238. reloadRequired: false,
  239. enable: noop,
  240. },
  241. hideCursorOnIdleDelay: {
  242. type: "slider",
  243. category: "layout",
  244. min: 0.5,
  245. max: 10,
  246. step: 0.25,
  247. default: 2,
  248. unit: "s",
  249. advanced: true,
  250. textAdornment: adornments.advanced,
  251. reloadRequired: false,
  252. enable: noop,
  253. },
  254. fixHdrIssues: {
  255. type: "toggle",
  256. category: "layout",
  257. default: true,
  258. advanced: true,
  259. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  260. },
  261. showVotes: {
  262. type: "toggle",
  263. category: "layout",
  264. default: true,
  265. textAdornment: adornments.reload,
  266. },
  267. // archived idea for future version
  268. // (shows a bar under the like/dislike buttons that shows the ratio of likes to dislikes)
  269. // showVoteRatio: {
  270. // type: "select",
  271. // category: "layout",
  272. // options: () => [
  273. // { value: "disabled", label: t("vote_ratio_disabled") },
  274. // { value: "greenRed", label: t("vote_ratio_green_red") },
  275. // { value: "blueGray", label: t("vote_ratio_blue_gray") },
  276. // ],
  277. // default: "disabled",
  278. // textAdornment: adornments.reload,
  279. // },
  280. //#region cat:volume
  281. volumeSliderLabel: {
  282. type: "toggle",
  283. category: "volume",
  284. default: true,
  285. textAdornment: adornments.reload,
  286. },
  287. volumeSliderSize: {
  288. type: "number",
  289. category: "volume",
  290. min: 50,
  291. max: 500,
  292. step: 5,
  293. default: 150,
  294. unit: "px",
  295. textAdornment: adornments.reload,
  296. },
  297. volumeSliderStep: {
  298. type: "slider",
  299. category: "volume",
  300. min: 1,
  301. max: 25,
  302. default: 2,
  303. unit: "%",
  304. textAdornment: adornments.reload,
  305. },
  306. volumeSliderScrollStep: {
  307. type: "slider",
  308. category: "volume",
  309. min: 1,
  310. max: 25,
  311. default: 4,
  312. unit: "%",
  313. textAdornment: adornments.reload,
  314. },
  315. volumeSharedBetweenTabs: {
  316. type: "toggle",
  317. category: "volume",
  318. default: false,
  319. textAdornment: adornments.reload,
  320. },
  321. setInitialTabVolume: {
  322. type: "toggle",
  323. category: "volume",
  324. default: false,
  325. textAdornment: () => getFeature("volumeSharedBetweenTabs")
  326. ? combineAdornments([adornments.alert(t("feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible").replace(/"/g, "'")), adornments.reload])
  327. : adornments.reload(),
  328. },
  329. initialTabVolumeLevel: {
  330. type: "slider",
  331. category: "volume",
  332. min: 0,
  333. max: 100,
  334. step: 1,
  335. default: 100,
  336. unit: "%",
  337. textAdornment: () => getFeature("volumeSharedBetweenTabs")
  338. ? combineAdornments([adornments.alert(t("feature_warning_setInitialTabVolume_volumeSharedBetweenTabs_incompatible").replace(/"/g, "'")), adornments.reload])
  339. : adornments.reload(),
  340. reloadRequired: false,
  341. enable: noop,
  342. },
  343. //#region cat:song lists
  344. lyricsQueueButton: {
  345. type: "toggle",
  346. category: "songLists",
  347. default: true,
  348. textAdornment: adornments.reload,
  349. },
  350. deleteFromQueueButton: {
  351. type: "toggle",
  352. category: "songLists",
  353. default: true,
  354. textAdornment: adornments.reload,
  355. },
  356. listButtonsPlacement: {
  357. type: "select",
  358. category: "songLists",
  359. options: () => [
  360. { value: "queueOnly", label: t("list_button_placement_queue_only") },
  361. { value: "everywhere", label: t("list_button_placement_everywhere") },
  362. ],
  363. default: "everywhere",
  364. advanced: true,
  365. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  366. },
  367. scrollToActiveSongBtn: {
  368. type: "toggle",
  369. category: "songLists",
  370. default: true,
  371. textAdornment: adornments.reload,
  372. },
  373. clearQueueBtn: {
  374. type: "toggle",
  375. category: "songLists",
  376. default: true,
  377. textAdornment: adornments.reload,
  378. },
  379. aboveQueueBtnsSticky: {
  380. type: "toggle",
  381. category: "songLists",
  382. default: true,
  383. advanced: true,
  384. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  385. },
  386. //#region cat:behavior
  387. disableBeforeUnloadPopup: {
  388. type: "toggle",
  389. category: "behavior",
  390. default: false,
  391. textAdornment: adornments.reload,
  392. },
  393. closeToastsTimeout: {
  394. type: "number",
  395. category: "behavior",
  396. min: 0,
  397. max: 30,
  398. step: 0.5,
  399. default: 3,
  400. unit: "s",
  401. reloadRequired: false,
  402. enable: noop,
  403. },
  404. rememberSongTime: {
  405. type: "toggle",
  406. category: "behavior",
  407. default: true,
  408. helpText: () => tp("feature_helptext_rememberSongTime", getFeature("rememberSongTimeMinPlayTime"), getFeature("rememberSongTimeMinPlayTime")),
  409. textAdornment: adornments.reload,
  410. },
  411. rememberSongTimeSites: {
  412. type: "select",
  413. category: "behavior",
  414. options: options.siteSelection,
  415. default: "all",
  416. textAdornment: adornments.reload,
  417. },
  418. rememberSongTimeDuration: {
  419. type: "number",
  420. category: "behavior",
  421. min: 1,
  422. max: 60 * 60 * 24 * 7,
  423. step: 1,
  424. default: 60,
  425. unit: "s",
  426. advanced: true,
  427. textAdornment: adornments.advanced,
  428. reloadRequired: false,
  429. enable: noop,
  430. },
  431. rememberSongTimeReduction: {
  432. type: "number",
  433. category: "behavior",
  434. min: 0,
  435. max: 30,
  436. step: 0.05,
  437. default: 0.2,
  438. unit: "s",
  439. advanced: true,
  440. textAdornment: adornments.advanced,
  441. reloadRequired: false,
  442. enable: noop,
  443. },
  444. rememberSongTimeMinPlayTime: {
  445. type: "slider",
  446. category: "behavior",
  447. min: 3,
  448. max: 30,
  449. step: 0.5,
  450. default: 10,
  451. unit: "s",
  452. advanced: true,
  453. textAdornment: adornments.advanced,
  454. reloadRequired: false,
  455. enable: noop,
  456. },
  457. autoScrollToActiveSongMode: {
  458. type: "select",
  459. category: "behavior",
  460. options: () => [
  461. { value: "never", label: t("auto_scroll_to_active_song_mode_never") },
  462. { value: "initialPageLoad", label: t("auto_scroll_to_active_song_mode_initial_page_load") },
  463. { value: "videoChangeAll", label: t("auto_scroll_to_active_song_mode_video_change_all") },
  464. { value: "videoChangeManual", label: t("auto_scroll_to_active_song_mode_video_change_manual") },
  465. { value: "videoChangeAuto", label: t("auto_scroll_to_active_song_mode_video_change_auto") },
  466. ],
  467. default: "videoChangeManual",
  468. reloadRequired: false,
  469. enable: noop,
  470. },
  471. //#region cat:input
  472. arrowKeySupport: {
  473. type: "toggle",
  474. category: "input",
  475. default: true,
  476. reloadRequired: false,
  477. enable: noop,
  478. },
  479. arrowKeySkipBy: {
  480. type: "slider",
  481. category: "input",
  482. min: 0.5,
  483. max: 30,
  484. step: 0.5,
  485. default: 5,
  486. unit: "s",
  487. reloadRequired: false,
  488. enable: noop,
  489. },
  490. switchBetweenSites: {
  491. type: "toggle",
  492. category: "input",
  493. default: true,
  494. reloadRequired: false,
  495. enable: noop,
  496. },
  497. switchSitesHotkey: {
  498. type: "hotkey",
  499. category: "input",
  500. default: {
  501. code: "F9",
  502. shift: false,
  503. ctrl: false,
  504. alt: false,
  505. },
  506. reloadRequired: false,
  507. enable: noop,
  508. },
  509. anchorImprovements: {
  510. type: "toggle",
  511. category: "input",
  512. default: true,
  513. textAdornment: adornments.reload,
  514. },
  515. numKeysSkipToTime: {
  516. type: "toggle",
  517. category: "input",
  518. default: true,
  519. reloadRequired: false,
  520. enable: noop,
  521. },
  522. autoLikeChannels: {
  523. type: "toggle",
  524. category: "input",
  525. default: true,
  526. textAdornment: adornments.reload,
  527. },
  528. autoLikeChannelToggleBtn: {
  529. type: "toggle",
  530. category: "input",
  531. default: true,
  532. reloadRequired: false,
  533. enable: noop,
  534. advanced: true,
  535. textAdornment: adornments.advanced,
  536. },
  537. // TODO(v2.2):
  538. // autoLikePlayerBarToggleBtn: {
  539. // type: "toggle",
  540. // category: "input",
  541. // default: false,
  542. // textAdornment: adornments.reload,
  543. // },
  544. autoLikeTimeout: {
  545. type: "slider",
  546. category: "input",
  547. min: 3,
  548. max: 30,
  549. step: 0.5,
  550. default: 5,
  551. unit: "s",
  552. advanced: true,
  553. reloadRequired: false,
  554. enable: noop,
  555. textAdornment: adornments.advanced,
  556. },
  557. autoLikeShowToast: {
  558. type: "toggle",
  559. category: "input",
  560. default: true,
  561. reloadRequired: false,
  562. advanced: true,
  563. enable: noop,
  564. textAdornment: adornments.advanced,
  565. },
  566. autoLikeOpenMgmtDialog: {
  567. type: "button",
  568. category: "input",
  569. click: () => getAutoLikeDialog().then(d => d.open()),
  570. },
  571. //#region cat:lyrics
  572. geniusLyrics: {
  573. type: "toggle",
  574. category: "lyrics",
  575. default: true,
  576. textAdornment: adornments.reload,
  577. },
  578. errorOnLyricsNotFound: {
  579. type: "toggle",
  580. category: "lyrics",
  581. default: false,
  582. reloadRequired: false,
  583. enable: noop,
  584. },
  585. geniUrlBase: {
  586. type: "text",
  587. category: "lyrics",
  588. default: "https://api.sv443.net/geniurl",
  589. normalize: (val: string) => val.trim().replace(/\/+$/, ""),
  590. advanced: true,
  591. textAdornment: adornments.advanced,
  592. reloadRequired: false,
  593. enable: noop,
  594. },
  595. geniUrlToken: {
  596. type: "text",
  597. valueHidden: true,
  598. category: "lyrics",
  599. default: "",
  600. normalize: (val: string) => val.trim(),
  601. advanced: true,
  602. textAdornment: adornments.advanced,
  603. reloadRequired: false,
  604. enable: noop,
  605. },
  606. lyricsCacheMaxSize: {
  607. type: "slider",
  608. category: "lyrics",
  609. default: 2000,
  610. min: 100,
  611. max: 10000,
  612. step: 100,
  613. unit: (val: number) => ` ${tp("unit_entries", val)}`,
  614. renderValue: renderLongNumberValue,
  615. advanced: true,
  616. textAdornment: adornments.advanced,
  617. reloadRequired: false,
  618. enable: noop,
  619. },
  620. lyricsCacheTTL: {
  621. type: "slider",
  622. category: "lyrics",
  623. default: 21,
  624. min: 1,
  625. max: 100,
  626. step: 1,
  627. unit: (val: number) => " " + tp("unit_days", val),
  628. advanced: true,
  629. textAdornment: adornments.advanced,
  630. reloadRequired: false,
  631. enable: noop,
  632. },
  633. clearLyricsCache: {
  634. type: "button",
  635. category: "lyrics",
  636. async click() {
  637. const entries = getLyricsCache().length;
  638. const formattedEntries = entries.toLocaleString(getLocale(), { style: "decimal", maximumFractionDigits: 0 });
  639. if(await showPrompt({ type: "confirm", message: tp("lyrics_clear_cache_confirm_prompt", entries, formattedEntries) })) {
  640. await clearLyricsCache();
  641. await showPrompt({ type: "alert", message: t("lyrics_clear_cache_success") });
  642. }
  643. },
  644. advanced: true,
  645. textAdornment: adornments.advanced,
  646. },
  647. // advancedLyricsFilter: {
  648. // type: "toggle",
  649. // category: "lyrics",
  650. // default: false,
  651. // change: () => setTimeout(async () => await showPrompt({ type: "confirm", message: t("lyrics_cache_changed_clear_confirm") }) && clearLyricsCache(), 200),
  652. // advanced: true,
  653. // textAdornment: adornments.experimental,
  654. // reloadRequired: false,
  655. // enable: noop,
  656. // },
  657. //#region cat:integrations
  658. disableDarkReaderSites: {
  659. type: "select",
  660. category: "integrations",
  661. options: options.siteSelectionOrNone,
  662. default: "all",
  663. advanced: true,
  664. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  665. },
  666. sponsorBlockIntegration: {
  667. type: "toggle",
  668. category: "integrations",
  669. default: true,
  670. advanced: true,
  671. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  672. },
  673. themeSongIntegration: {
  674. type: "toggle",
  675. category: "integrations",
  676. default: false,
  677. textAdornment: adornments.reload,
  678. },
  679. themeSongLightness: {
  680. type: "select",
  681. category: "integrations",
  682. options: options.colorLightness,
  683. default: "darker",
  684. textAdornment: adornments.reload,
  685. },
  686. //#region cat:plugins
  687. openPluginList: {
  688. type: "button",
  689. category: "plugins",
  690. default: undefined,
  691. click: () => getPluginListDialog().then(d => d.open()),
  692. },
  693. initTimeout: {
  694. type: "number",
  695. category: "plugins",
  696. min: 3,
  697. max: 30,
  698. default: 8,
  699. step: 0.1,
  700. unit: "s",
  701. advanced: true,
  702. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  703. },
  704. //#region cat:general
  705. locale: {
  706. type: "select",
  707. category: "general",
  708. options: options.locale,
  709. default: getPreferredLocale(),
  710. textAdornment: () => combineAdornments([adornments.globe, adornments.reload]),
  711. },
  712. localeFallback: {
  713. type: "toggle",
  714. category: "general",
  715. default: true,
  716. advanced: true,
  717. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  718. },
  719. versionCheck: {
  720. type: "toggle",
  721. category: "general",
  722. default: true,
  723. textAdornment: adornments.reload,
  724. },
  725. checkVersionNow: {
  726. type: "button",
  727. category: "general",
  728. click: () => doVersionCheck(true),
  729. },
  730. numbersFormat: {
  731. type: "select",
  732. category: "general",
  733. options: () => [
  734. { value: "long", label: `${formatNumber(12_345_678, "long")} (${t("votes_format_long")})` },
  735. { value: "short", label: `${formatNumber(12_345_678, "short")} (${t("votes_format_short")})` },
  736. ],
  737. default: "short",
  738. reloadRequired: false,
  739. enable: noop,
  740. },
  741. toastDuration: {
  742. type: "slider",
  743. category: "general",
  744. min: 0,
  745. max: 15,
  746. default: 4,
  747. step: 0.5,
  748. unit: "s",
  749. reloadRequired: false,
  750. advanced: true,
  751. textAdornment: adornments.advanced,
  752. enable: noop,
  753. change: () => showIconToast({
  754. message: t("example_toast"),
  755. iconSrc: getResourceUrl(`img-logo${mode === "development" ? "_dev" : ""}`),
  756. }),
  757. },
  758. showToastOnGenericError: {
  759. type: "toggle",
  760. category: "general",
  761. default: true,
  762. advanced: true,
  763. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  764. },
  765. resetConfig: {
  766. type: "button",
  767. category: "general",
  768. click: promptResetConfig,
  769. textAdornment: adornments.reload,
  770. },
  771. resetEverything: {
  772. type: "button",
  773. category: "general",
  774. click: async () => {
  775. if(await showPrompt({
  776. type: "confirm",
  777. message: t("reset_everything_confirm"),
  778. })) {
  779. await getStoreSerializer().resetStoresData();
  780. await reloadTab();
  781. }
  782. },
  783. advanced: true,
  784. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  785. },
  786. logLevel: {
  787. type: "select",
  788. category: "general",
  789. options: () => [
  790. { value: LogLevel.Debug, label: t("log_level_debug") },
  791. { value: LogLevel.Info, label: t("log_level_info") },
  792. ],
  793. default: LogLevel.Info,
  794. textAdornment: adornments.reload,
  795. },
  796. advancedMode: {
  797. type: "toggle",
  798. category: "general",
  799. default: false,
  800. textAdornment: () => getFeature("advancedMode") ? adornments.advanced() : undefined,
  801. change: (_key, prevValue, newValue) =>
  802. prevValue !== newValue &&
  803. emitSiteEvent("recreateCfgMenu"),
  804. },
  805. } as const satisfies FeatureInfo;