index.ts 25 KB

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