index.ts 25 KB

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