index.ts 26 KB

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