import { t } from 'i18next'; import isElectron from 'is-electron'; import { memo, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import i18n, { languages } from '/@/i18n/i18n'; import { ImageResolutionSettings } from '/@/renderer/features/settings/components/general/art-resolution-settings'; import { ArtistReleaseTypeSettings, ArtistSettings, } from '/@/renderer/features/settings/components/general/artist-settings'; import { FullscreenPlayerSettings } from '/@/renderer/features/settings/components/general/fullscreen-player-settings'; import { HomeSettings } from '/@/renderer/features/settings/components/general/home-settings'; import { PathSettings } from '/@/renderer/features/settings/components/general/path-settings'; import { SettingOption, SettingsSection, } from '/@/renderer/features/settings/components/settings-section'; import { HomeFeatureStyle, SideQueueLayout, SideQueueType, useFontSettings, useGeneralSettings, useSettingsStoreActions, } from '/@/renderer/store/settings.store'; import { type Font, FONT_OPTIONS } from '/@/renderer/types/fonts'; import { FileInput } from '/@/shared/components/file-input/file-input'; import { NumberInput } from '/@/shared/components/number-input/number-input'; import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control'; import { Select } from '/@/shared/components/select/select'; import { Slider } from '/@/shared/components/slider/slider'; import { Switch } from '/@/shared/components/switch/switch'; import { toast } from '/@/shared/components/toast/toast'; import { FontType } from '/@/shared/types/types'; const localSettings = isElectron() ? window.api.localSettings : null; const ipc = isElectron() ? window.api.ipc : null; const utils = isElectron() ? window.api.utils : null; // Electron 32+ removed file.path, use this which is exposed in preload to get real path const getPathForFile = isElectron() ? window.api.getPathForFile : null; const HOME_FEATURE_STYLE_OPTIONS = [ { label: t('setting.homeFeatureStyle', { context: 'optionSingle', }), value: HomeFeatureStyle.SINGLE, }, { label: t('setting.homeFeatureStyle', { context: 'optionMultiple', }), value: HomeFeatureStyle.MULTIPLE, }, ]; const SIDE_QUEUE_OPTIONS = [ { label: t('setting.sidePlayQueueStyle', { context: 'optionAttached', }), value: 'sideQueue', }, { label: t('setting.sidePlayQueueStyle', { context: 'optionDetached', }), value: 'sideDrawerQueue', }, ]; const SIDE_QUEUE_LAYOUT_OPTIONS = [ { label: t('setting.sidePlayQueueLayout', { context: 'optionHorizontal', }), value: 'horizontal', }, { label: t('setting.sidePlayQueueLayout', { context: 'optionVertical', }), value: 'vertical', }, ]; const FONT_TYPES: Font[] = [ { label: i18n.t('setting.fontType', { context: 'optionBuiltIn', }), value: FontType.BUILT_IN, }, ]; if (window.queryLocalFonts) { FONT_TYPES.push({ label: i18n.t('setting.fontType', { context: 'optionSystem' }), value: FontType.SYSTEM, }); } if (isElectron()) { FONT_TYPES.push({ label: i18n.t('setting.fontType', { context: 'optionCustom' }), value: FontType.CUSTOM, }); } export const ApplicationSettings = memo(() => { const { t } = useTranslation(); const settings = useGeneralSettings(); const fontSettings = useFontSettings(); const { setSettings } = useSettingsStoreActions(); const [localFonts, setLocalFonts] = useState([]); // const fontList = useMemo(() => { // if (fontSettings.custom) { // return fontSettings.custom.split(/(\\|\/)/g).pop()!; // } // return ''; // }, [fontSettings.custom]); const onFontError = useCallback( (file: string) => { toast.error({ message: `${file} is not a valid font file`, }); setSettings({ font: { ...fontSettings, custom: null, }, }); }, [fontSettings, setSettings], ); useEffect(() => { if (localSettings) { localSettings.fontError(onFontError); return () => { ipc?.removeAllListeners('custom-font-error'); }; } return () => {}; }, [onFontError]); useEffect(() => { const getFonts = async () => { if ( fontSettings.type === FontType.SYSTEM && localFonts.length === 0 && window.queryLocalFonts ) { try { // WARNING (Oct 17 2023): while this query is valid for chromium-based // browsers, it is still experimental, and so Typescript will complain const status = await navigator.permissions.query({ name: 'local-fonts' as any, }); if (status.state === 'denied') { throw new Error(t('error.localFontAccessDenied')); } const data = await window.queryLocalFonts(); setLocalFonts( data.map((font) => ({ label: font.fullName, value: font.postscriptName, })), ); } catch (error) { console.error('Failed to get local fonts', error); toast.error({ message: t('error.systemFontError'), }); setSettings({ font: { ...fontSettings, type: FontType.BUILT_IN, }, }); } } }; getFonts(); }, [fontSettings, localFonts, setSettings, t]); const handleChangeLanguage = (e: null | string) => { if (!e) return; setSettings({ general: { ...settings, language: e, }, }); }; const options: SettingOption[] = [ { control: ( { if (!e) return; setSettings({ font: { ...fontSettings, type: e as FontType, }, }); }} value={fontSettings.type} /> ), description: t('setting.fontType', { context: 'description', }), isHidden: FONT_TYPES.length === 1, title: t('setting.fontType'), }, { control: ( { if (!e) return; setSettings({ font: { ...fontSettings, system: e, }, }); }} searchable value={fontSettings.system} w={300} /> ), description: t('setting.font', { context: 'description' }), isHidden: !localFonts || fontSettings.type !== FontType.SYSTEM, title: t('setting.font'), }, { control: ( { const custom = e ? getPathForFile?.(e) || null : null; await localSettings?.setSync('local_font_path', custom); setSettings({ font: { ...fontSettings, custom, }, }); }} w={300} /> ), description: t('setting.customFontPath', { context: 'description', }), isHidden: !isElectron() || fontSettings.type !== FontType.CUSTOM, title: t('setting.customFontPath'), }, { control: ( { if (!e) return; const newVal = e.currentTarget.value ? Math.min(Math.max(Number(e.currentTarget.value), 50), 300) : settings.zoomFactor; setSettings({ general: { ...settings, zoomFactor: newVal, }, }); localSettings!.setZoomFactor(newVal); }} value={settings.zoomFactor} /> ), description: t('setting.zoom', { context: 'description', }), isHidden: !isElectron(), title: t('setting.zoom'), }, { control: ( { localSettings?.set('resume', e.target.checked); setSettings({ general: { ...settings, resume: e.currentTarget.checked, }, }); }} /> ), description: t('setting.savePlayQueue', { context: 'description', }), isHidden: !isElectron(), title: t('setting.savePlayQueue'), }, { control: ( setSettings({ general: { ...settings, homeFeature: e.currentTarget.checked, }, }) } /> ), description: t('setting.homeFeature', { context: 'description', }), isHidden: false, title: t('setting.homeFeature'), }, { control: ( setSettings({ general: { ...settings, homeFeatureStyle: e as HomeFeatureStyle, }, }) } /> ), description: t('setting.homeFeatureStyle', { context: 'description', }), isHidden: false, title: t('setting.homeFeatureStyle'), }, { control: ( setSettings({ general: { ...settings, albumBackground: e.currentTarget.checked, }, }) } /> ), description: t('setting.albumBackground', { context: 'description', }), isHidden: false, title: t('setting.albumBackground'), }, { control: ( `${e} rem`} max={6} min={0} onChangeEnd={(e) => { setSettings({ general: { ...settings, albumBackgroundBlur: e, }, }); }} step={0.5} w={100} /> ), description: t('setting.albumBackgroundBlur', { context: 'description', }), isHidden: !settings.albumBackground, title: t('setting.albumBackgroundBlur'), }, { control: ( setSettings({ general: { ...settings, artistBackground: e.currentTarget.checked, }, }) } /> ), description: t('setting.artistBackground', { context: 'description', }), isHidden: false, title: t('setting.artistBackground'), }, { control: ( `${e} rem`} max={6} min={0} onChangeEnd={(e) => { setSettings({ general: { ...settings, artistBackgroundBlur: e, }, }); }} step={0.5} w={100} /> ), description: t('setting.artistBackgroundBlur', { context: 'description', }), isHidden: !settings.artistBackground, title: t('setting.artistBackgroundBlur'), }, { control: ( setSettings({ general: { ...settings, nativeAspectRatio: e.currentTarget.checked, }, }) } /> ), description: t('setting.imageAspectRatio', { context: 'description', }), isHidden: false, title: t('setting.imageAspectRatio'), }, { control: (