index.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819
  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. //#region cat:input
  458. arrowKeySupport: {
  459. type: "toggle",
  460. category: "input",
  461. default: true,
  462. reloadRequired: false,
  463. enable: noop,
  464. },
  465. arrowKeySkipBy: {
  466. type: "slider",
  467. category: "input",
  468. min: 0.5,
  469. max: 30,
  470. step: 0.5,
  471. default: 5,
  472. unit: "s",
  473. reloadRequired: false,
  474. enable: noop,
  475. },
  476. switchBetweenSites: {
  477. type: "toggle",
  478. category: "input",
  479. default: true,
  480. reloadRequired: false,
  481. enable: noop,
  482. },
  483. switchSitesHotkey: {
  484. type: "hotkey",
  485. category: "input",
  486. default: {
  487. code: "F9",
  488. shift: false,
  489. ctrl: false,
  490. alt: false,
  491. },
  492. reloadRequired: false,
  493. enable: noop,
  494. },
  495. anchorImprovements: {
  496. type: "toggle",
  497. category: "input",
  498. default: true,
  499. textAdornment: adornments.reload,
  500. },
  501. numKeysSkipToTime: {
  502. type: "toggle",
  503. category: "input",
  504. default: true,
  505. reloadRequired: false,
  506. enable: noop,
  507. },
  508. autoLikeChannels: {
  509. type: "toggle",
  510. category: "input",
  511. default: true,
  512. textAdornment: adornments.reload,
  513. },
  514. autoLikeChannelToggleBtn: {
  515. type: "toggle",
  516. category: "input",
  517. default: true,
  518. reloadRequired: false,
  519. enable: noop,
  520. advanced: true,
  521. textAdornment: adornments.advanced,
  522. },
  523. // TODO(v2.2):
  524. // autoLikePlayerBarToggleBtn: {
  525. // type: "toggle",
  526. // category: "input",
  527. // default: false,
  528. // textAdornment: adornments.reload,
  529. // },
  530. autoLikeTimeout: {
  531. type: "slider",
  532. category: "input",
  533. min: 3,
  534. max: 30,
  535. step: 0.5,
  536. default: 5,
  537. unit: "s",
  538. advanced: true,
  539. reloadRequired: false,
  540. enable: noop,
  541. textAdornment: adornments.advanced,
  542. },
  543. autoLikeShowToast: {
  544. type: "toggle",
  545. category: "input",
  546. default: true,
  547. reloadRequired: false,
  548. advanced: true,
  549. enable: noop,
  550. textAdornment: adornments.advanced,
  551. },
  552. autoLikeOpenMgmtDialog: {
  553. type: "button",
  554. category: "input",
  555. click: () => getAutoLikeDialog().then(d => d.open()),
  556. },
  557. //#region cat:lyrics
  558. geniusLyrics: {
  559. type: "toggle",
  560. category: "lyrics",
  561. default: true,
  562. textAdornment: adornments.reload,
  563. },
  564. errorOnLyricsNotFound: {
  565. type: "toggle",
  566. category: "lyrics",
  567. default: false,
  568. reloadRequired: false,
  569. enable: noop,
  570. },
  571. geniUrlBase: {
  572. type: "text",
  573. category: "lyrics",
  574. default: "https://api.sv443.net/geniurl",
  575. normalize: (val: string) => val.trim().replace(/\/+$/, ""),
  576. advanced: true,
  577. textAdornment: adornments.advanced,
  578. reloadRequired: false,
  579. enable: noop,
  580. },
  581. geniUrlToken: {
  582. type: "text",
  583. valueHidden: true,
  584. category: "lyrics",
  585. default: "",
  586. normalize: (val: string) => val.trim(),
  587. advanced: true,
  588. textAdornment: adornments.advanced,
  589. reloadRequired: false,
  590. enable: noop,
  591. },
  592. lyricsCacheMaxSize: {
  593. type: "slider",
  594. category: "lyrics",
  595. default: 2000,
  596. min: 100,
  597. max: 10000,
  598. step: 100,
  599. unit: (val: number) => ` ${tp("unit_entries", val)}`,
  600. renderValue: renderLongNumberValue,
  601. advanced: true,
  602. textAdornment: adornments.advanced,
  603. reloadRequired: false,
  604. enable: noop,
  605. },
  606. lyricsCacheTTL: {
  607. type: "slider",
  608. category: "lyrics",
  609. default: 21,
  610. min: 1,
  611. max: 100,
  612. step: 1,
  613. unit: (val: number) => " " + tp("unit_days", val),
  614. advanced: true,
  615. textAdornment: adornments.advanced,
  616. reloadRequired: false,
  617. enable: noop,
  618. },
  619. clearLyricsCache: {
  620. type: "button",
  621. category: "lyrics",
  622. async click() {
  623. const entries = getLyricsCache().length;
  624. const formattedEntries = entries.toLocaleString(getLocale(), { style: "decimal", maximumFractionDigits: 0 });
  625. if(await showPrompt({ type: "confirm", message: tp("lyrics_clear_cache_confirm_prompt", entries, formattedEntries) })) {
  626. await clearLyricsCache();
  627. await showPrompt({ type: "alert", message: t("lyrics_clear_cache_success") });
  628. }
  629. },
  630. advanced: true,
  631. textAdornment: adornments.advanced,
  632. },
  633. // advancedLyricsFilter: {
  634. // type: "toggle",
  635. // category: "lyrics",
  636. // default: false,
  637. // change: () => setTimeout(async () => await showPrompt({ type: "confirm", message: t("lyrics_cache_changed_clear_confirm") }) && clearLyricsCache(), 200),
  638. // advanced: true,
  639. // textAdornment: adornments.experimental,
  640. // reloadRequired: false,
  641. // enable: noop,
  642. // },
  643. //#region cat:integrations
  644. disableDarkReaderSites: {
  645. type: "select",
  646. category: "integrations",
  647. options: options.siteSelectionOrNone,
  648. default: "all",
  649. advanced: true,
  650. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  651. },
  652. sponsorBlockIntegration: {
  653. type: "toggle",
  654. category: "integrations",
  655. default: true,
  656. advanced: true,
  657. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  658. },
  659. themeSongIntegration: {
  660. type: "toggle",
  661. category: "integrations",
  662. default: false,
  663. textAdornment: adornments.reload,
  664. },
  665. themeSongLightness: {
  666. type: "select",
  667. category: "integrations",
  668. options: options.colorLightness,
  669. default: "darker",
  670. textAdornment: adornments.reload,
  671. },
  672. //#region cat:plugins
  673. openPluginList: {
  674. type: "button",
  675. category: "plugins",
  676. default: undefined,
  677. click: () => getPluginListDialog().then(d => d.open()),
  678. },
  679. initTimeout: {
  680. type: "number",
  681. category: "plugins",
  682. min: 3,
  683. max: 30,
  684. default: 8,
  685. step: 0.1,
  686. unit: "s",
  687. advanced: true,
  688. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  689. },
  690. //#region cat:general
  691. locale: {
  692. type: "select",
  693. category: "general",
  694. options: options.locale,
  695. default: getPreferredLocale(),
  696. textAdornment: () => combineAdornments([adornments.globe, adornments.reload]),
  697. },
  698. localeFallback: {
  699. type: "toggle",
  700. category: "general",
  701. default: true,
  702. advanced: true,
  703. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  704. },
  705. versionCheck: {
  706. type: "toggle",
  707. category: "general",
  708. default: true,
  709. textAdornment: adornments.reload,
  710. },
  711. checkVersionNow: {
  712. type: "button",
  713. category: "general",
  714. click: () => doVersionCheck(true),
  715. },
  716. numbersFormat: {
  717. type: "select",
  718. category: "general",
  719. options: () => [
  720. { value: "long", label: `${formatNumber(12_345_678, "long")} (${t("votes_format_long")})` },
  721. { value: "short", label: `${formatNumber(12_345_678, "short")} (${t("votes_format_short")})` },
  722. ],
  723. default: "short",
  724. reloadRequired: false,
  725. enable: noop,
  726. },
  727. toastDuration: {
  728. type: "slider",
  729. category: "general",
  730. min: 0,
  731. max: 15,
  732. default: 4,
  733. step: 0.5,
  734. unit: "s",
  735. reloadRequired: false,
  736. advanced: true,
  737. textAdornment: adornments.advanced,
  738. enable: noop,
  739. change: () => showIconToast({
  740. message: t("example_toast"),
  741. iconSrc: getResourceUrl(`img-logo${mode === "development" ? "_dev" : ""}`),
  742. }),
  743. },
  744. showToastOnGenericError: {
  745. type: "toggle",
  746. category: "general",
  747. default: true,
  748. advanced: true,
  749. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  750. },
  751. resetConfig: {
  752. type: "button",
  753. category: "general",
  754. click: promptResetConfig,
  755. textAdornment: adornments.reload,
  756. },
  757. resetEverything: {
  758. type: "button",
  759. category: "general",
  760. click: async () => {
  761. if(await showPrompt({
  762. type: "confirm",
  763. message: t("reset_everything_confirm"),
  764. })) {
  765. await getStoreSerializer().resetStoresData();
  766. await reloadTab();
  767. }
  768. },
  769. advanced: true,
  770. textAdornment: () => combineAdornments([adornments.advanced, adornments.reload]),
  771. },
  772. logLevel: {
  773. type: "select",
  774. category: "general",
  775. options: () => [
  776. { value: LogLevel.Debug, label: t("log_level_debug") },
  777. { value: LogLevel.Info, label: t("log_level_info") },
  778. ],
  779. default: LogLevel.Info,
  780. textAdornment: adornments.reload,
  781. },
  782. advancedMode: {
  783. type: "toggle",
  784. category: "general",
  785. default: false,
  786. textAdornment: () => getFeature("advancedMode") ? adornments.advanced() : undefined,
  787. change: (_key, prevValue, newValue) =>
  788. prevValue !== newValue &&
  789. emitSiteEvent("recreateCfgMenu"),
  790. },
  791. } as const satisfies FeatureInfo;