reorganize and redesign settings

This commit is contained in:
jeffvli
2025-11-23 18:15:38 -08:00
parent 7cc5ccd2c5
commit a2926ef47e
26 changed files with 629 additions and 540 deletions
+14 -5
View File
@@ -459,7 +459,20 @@
"generalTab": "general",
"hotkeysTab": "hotkeys",
"playbackTab": "playback",
"windowTab": "window"
"windowTab": "window",
"updates": "update",
"cache": "cache",
"application": "application",
"theme": "theme",
"controls": "controls",
"sidebar": "sidebar",
"remote": "remote",
"exportImport": "export/import",
"scrobble": "scrobble",
"audio": "audio",
"lyrics": "lyrics",
"transcoding": "transcoding",
"discord": "discord"
},
"sidebar": {
"albumArtists": "$t(entity.albumArtist_other)",
@@ -614,8 +627,6 @@
"discordServeImage_description": "share cover art for {{discord}} rich presence from server itself, only available for Jellyfin and Navidrome. {{discord}} uses a bot to fetch images, so your server must be reachable from the public internet",
"discordUpdateInterval": "{{discord}} rich presence update interval",
"discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)",
"doubleClickBehavior_description": "if true, all matching tracks in a track search will be queued. otherwise, only the clicked one will be queued",
"doubleClickBehavior": "queue all searched tracks when double clicking",
"enableAutoTranslation_description": "enable translation automatically when lyrics are loaded",
"enableAutoTranslation": "enable auto translation",
"enableRemote_description": "enables the remote control server to allow other devices to control the application",
@@ -634,8 +645,6 @@
"exportImportSettings_offendingKeyError": "\"{{offendingKey}}\" is incorrect - {{reason}}",
"externalLinks_description": "enables showing external links (Last.fm, MusicBrainz) on artist/album pages",
"externalLinks": "show external links",
"floatingQueueArea_description": "display a hover icon on the right side of the screen to view the play queue",
"floatingQueueArea": "show floating queue hover area",
"followLyric_description": "scroll the lyric to the current playing position",
"followLyric": "follow current lyric",
"font_description": "sets the font to use for the application",
@@ -1,14 +1,26 @@
import { Fragment } from 'react/jsx-runtime';
import { ExportImportSettings } from '/@/renderer/features/settings/components/advanced/export-import-settings';
import { StylesSettings } from '/@/renderer/features/settings/components/advanced/styles-settings';
import { CacheSettings } from '/@/renderer/features/settings/components/window/cache-settngs';
import { UpdateSettings } from '/@/renderer/features/settings/components/window/update-settings';
import { Divider } from '/@/shared/components/divider/divider';
import { Stack } from '/@/shared/components/stack/stack';
const sections = [
{ component: UpdateSettings, key: 'update' },
{ component: ExportImportSettings, key: 'export-import' },
{ component: CacheSettings, key: 'cache' },
];
export const AdvancedTab = () => {
return (
<Stack gap="md">
<UpdateSettings />
<StylesSettings />
<ExportImportSettings />
{sections.map(({ component: Section, key }, index) => (
<Fragment key={key}>
<Section />
{index < sections.length - 1 && <Divider />}
</Fragment>
))}
</Stack>
);
};
@@ -3,7 +3,10 @@ import { t } from 'i18next';
import { useCallback } from 'react';
import { ExportImportSettingsModal } from '/@/renderer/components/export-import-settings-modal/export-import-settings-modal';
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
import { useSettingsForExport } from '/@/renderer/store';
import { Button } from '/@/shared/components/button/button';
@@ -28,26 +31,41 @@ export const ExportImportSettings = () => {
openModal({
children: <ExportImportSettingsModal />,
size: 'lg',
title: t('setting.exportImportSettings_importModalTitle').toString(),
title: t('setting.exportImportSettings_importModalTitle', {
postProcess: 'sentenceCase',
}),
});
};
const options: SettingOption[] = [
{
control: (
<>
<Button onClick={onExportSettings} size="compact-sm">
{t('setting.exportImportSettings_control_exportText', {
postProcess: 'sentenceCase',
})}
</Button>
<Button onClick={openImportModal} size="compact-sm">
{t('setting.exportImportSettings_control_importText', {
postProcess: 'sentenceCase',
})}
</Button>
</>
),
description: t('setting.exportImportSettings_control_description', {
postProcess: 'sentenceCase',
}),
title: t('setting.exportImportSettings_control_title', {
postProcess: 'sentenceCase',
}),
},
];
return (
<>
<SettingsOptions
control={
<>
<Button onClick={onExportSettings}>
{t('setting.exportImportSettings_control_exportText').toString()}
</Button>
<Button onClick={openImportModal}>
{t('setting.exportImportSettings_control_importText').toString()}
</Button>
</>
}
description={t('setting.exportImportSettings_control_description').toString()}
title={t('setting.exportImportSettings_control_title').toString()}
/>
</>
<SettingsSection
options={options}
title={t('page.setting.exportImport', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -1,15 +1,19 @@
import type { IpcRendererEvent } from 'electron';
import { t } from 'i18next';
import isElectron from 'is-electron';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import i18n, { languages } from '/@/i18n/i18n';
import { ArtistSettings } from '/@/renderer/features/settings/components/general/artist-settings';
import { HomeSettings } from '/@/renderer/features/settings/components/general/home-settings';
import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
import {
SideQueueType,
useFontSettings,
useGeneralSettings,
useSettingsStoreActions,
@@ -18,6 +22,8 @@ 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 { 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';
@@ -26,6 +32,23 @@ const ipc = isElectron() ? window.api.ipc : null;
// Electron 32+ removed file.path, use this which is exposed in preload to get real path
const webUtils = isElectron() ? window.electron.webUtils : null;
const SIDE_QUEUE_OPTIONS = [
{
label: t('setting.sidePlayQueueStyle', {
context: 'optionAttached',
postProcess: 'sentenceCase',
}),
value: 'sideQueue',
},
{
label: t('setting.sidePlayQueueStyle', {
context: 'optionDetached',
postProcess: 'sentenceCase',
}),
value: 'sideDrawerQueue',
},
];
const FONT_TYPES: Font[] = [
{
label: i18n.t('setting.fontType', {
@@ -284,7 +307,291 @@ export const ApplicationSettings = () => {
postProcess: 'sentenceCase',
}),
},
{
control: (
<Switch
defaultChecked={settings.resume}
onChange={(e) => {
localSettings?.set('resume', e.target.checked);
setSettings({
general: {
...settings,
resume: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.savePlayQueue', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.savePlayQueue', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label={t('setting.homeFeature', { postProcess: 'sentenceCase' })}
defaultChecked={settings.homeFeature}
onChange={(e) =>
setSettings({
general: {
...settings,
homeFeature: e.currentTarget.checked,
},
})
}
/>
),
description: t('setting.homeFeature', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.homeFeature', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label={t('setting.albumBackground', { postProcess: 'sentenceCase' })}
defaultChecked={settings.albumBackground}
onChange={(e) =>
setSettings({
general: {
...settings,
albumBackground: e.currentTarget.checked,
},
})
}
/>
),
description: t('setting.albumBackground', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.albumBackground', { postProcess: 'sentenceCase' }),
},
{
control: (
<Slider
defaultValue={settings.albumBackgroundBlur}
label={(e) => `${e} rem`}
max={6}
min={0}
onChangeEnd={(e) => {
setSettings({
general: {
...settings,
albumBackgroundBlur: e,
},
});
}}
step={0.5}
w={100}
/>
),
description: t('setting.albumBackgroundBlur', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !settings.albumBackground,
title: t('setting.albumBackgroundBlur', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label={t('setting.artistBackground', { postProcess: 'sentenceCase' })}
defaultChecked={settings.artistBackground}
onChange={(e) =>
setSettings({
general: {
...settings,
artistBackground: e.currentTarget.checked,
},
})
}
/>
),
description: t('setting.artistBackground', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.artistBackground', { postProcess: 'sentenceCase' }),
},
{
control: (
<Slider
defaultValue={settings.artistBackgroundBlur}
label={(e) => `${e} rem`}
max={6}
min={0}
onChangeEnd={(e) => {
setSettings({
general: {
...settings,
artistBackgroundBlur: e,
},
});
}}
step={0.5}
w={100}
/>
),
description: t('setting.artistBackgroundBlur', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !settings.artistBackground,
title: t('setting.artistBackgroundBlur', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label="Toggle using native aspect ratio"
defaultChecked={settings.nativeAspectRatio}
onChange={(e) =>
setSettings({
general: {
...settings,
nativeAspectRatio: e.currentTarget.checked,
},
})
}
/>
),
description: t('setting.imageAspectRatio', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.imageAspectRatio', { postProcess: 'sentenceCase' }),
},
{
control: (
<Select
data={SIDE_QUEUE_OPTIONS}
defaultValue={settings.sideQueueType}
onChange={(e) => {
setSettings({
general: {
...settings,
sideQueueType: e as SideQueueType,
},
});
}}
/>
),
description: t('setting.sidePlayQueueStyle', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.sidePlayQueueStyle', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
defaultChecked={settings.lastFM}
onChange={(e) => {
setSettings({
general: {
...settings,
lastFM: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.lastfm', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !settings.externalLinks,
title: t('setting.lastfm', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
defaultChecked={settings.musicBrainz}
onChange={(e) => {
setSettings({
general: {
...settings,
musicBrainz: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.musicbrainz', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !settings.externalLinks,
title: t('setting.musicbrainz', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label={t('setting.playerbarOpenDrawer', { postProcess: 'sentenceCase' })}
defaultChecked={settings.playerbarOpenDrawer}
onChange={(e) =>
setSettings({
general: {
...settings,
playerbarOpenDrawer: e.currentTarget.checked,
},
})
}
/>
),
description: t('setting.playerbarOpenDrawer', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.playerbarOpenDrawer', { postProcess: 'sentenceCase' }),
},
{
control: (
<NumberInput
defaultValue={settings.albumArtRes || undefined}
hideControls={false}
max={2500}
onBlur={(e) => {
const newVal =
e.currentTarget.value !== '0'
? Math.min(Math.max(Number(e.currentTarget.value), 175), 2500)
: null;
setSettings({ general: { ...settings, albumArtRes: newVal } });
}}
placeholder="0"
value={settings.albumArtRes ?? 0}
width={75}
/>
),
description: t('setting.playerAlbumArtResolution', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.playerAlbumArtResolution', { postProcess: 'sentenceCase' }),
},
];
return <SettingsSection options={options} />;
return (
<SettingsSection
extra={
<>
<HomeSettings />
<ArtistSettings />
</>
}
options={options}
title={t('page.setting.application', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -1,5 +1,3 @@
import { t } from 'i18next';
import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
import {
@@ -10,7 +8,6 @@ import {
BarAlign,
GenreTarget,
PlayerbarSliderType,
SideQueueType,
useGeneralSettings,
usePlayerbarSlider,
useSettingsStoreActions,
@@ -25,25 +22,6 @@ import { Text } from '/@/shared/components/text/text';
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { Play } from '/@/shared/types/types';
const localSettings = isElectron() ? window.api.localSettings : null;
const SIDE_QUEUE_OPTIONS = [
{
label: t('setting.sidePlayQueueStyle', {
context: 'optionAttached',
postProcess: 'sentenceCase',
}),
value: 'sideQueue',
},
{
label: t('setting.sidePlayQueueStyle', {
context: 'optionDetached',
postProcess: 'sentenceCase',
}),
value: 'sideDrawerQueue',
},
];
export const ControlSettings = () => {
const { t } = useTranslation();
const settings = useGeneralSettings();
@@ -55,6 +33,7 @@ export const ControlSettings = () => {
control: (
<NumberInput
defaultValue={settings.buttonSize}
hideControls={false}
max={30}
min={15}
onBlur={(e) => {
@@ -69,7 +48,6 @@ export const ControlSettings = () => {
},
});
}}
rightSection={<Text size="sm">px</Text>}
width={75}
/>
),
@@ -80,53 +58,6 @@ export const ControlSettings = () => {
isHidden: false,
title: t('setting.buttonSize', { postProcess: 'sentenceCase' }),
},
{
control: (
<NumberInput
defaultValue={settings.albumArtRes || undefined}
max={2500}
onBlur={(e) => {
const newVal =
e.currentTarget.value !== '0'
? Math.min(Math.max(Number(e.currentTarget.value), 175), 2500)
: null;
setSettings({ general: { ...settings, albumArtRes: newVal } });
}}
placeholder="0"
rightSection={<Text size="sm">px</Text>}
value={settings.albumArtRes ?? 0}
width={75}
/>
),
description: t('setting.playerAlbumArtResolution', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.playerAlbumArtResolution', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label="Toggle using native aspect ratio"
defaultChecked={settings.nativeAspectRatio}
onChange={(e) =>
setSettings({
general: {
...settings,
nativeAspectRatio: e.currentTarget.checked,
},
})
}
/>
),
description: t('setting.imageAspectRatio', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.imageAspectRatio', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
@@ -255,71 +186,6 @@ export const ControlSettings = () => {
isHidden: false,
title: t('setting.playButtonBehavior', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label="Whether double clicking a track should queue all matching songs"
defaultChecked={settings.doubleClickQueueAll}
onChange={(e) =>
setSettings({
general: {
...settings,
doubleClickQueueAll: e.currentTarget.checked,
},
})
}
/>
),
description: t('setting.doubleClickBehavior', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.doubleClickBehavior', { postProcess: 'sentenceCase' }),
},
{
control: (
<Select
data={SIDE_QUEUE_OPTIONS}
defaultValue={settings.sideQueueType}
onChange={(e) => {
setSettings({
general: {
...settings,
sideQueueType: e as SideQueueType,
},
});
}}
/>
),
description: t('setting.sidePlayQueueStyle', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.sidePlayQueueStyle', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
defaultChecked={settings.showQueueDrawerButton}
onChange={(e) => {
setSettings({
general: {
...settings,
showQueueDrawerButton: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.sidePlayQueueStyle', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.floatingQueueArea', { postProcess: 'sentenceCase' }),
},
{
control: (
<Slider
@@ -367,28 +233,7 @@ export const ControlSettings = () => {
isHidden: false,
title: t('setting.volumeWidth', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
defaultChecked={settings.resume}
onChange={(e) => {
localSettings?.set('resume', e.target.checked);
setSettings({
general: {
...settings,
resume: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.savePlayQueue', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isElectron(),
title: t('setting.savePlayQueue', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
@@ -409,48 +254,7 @@ export const ControlSettings = () => {
}),
title: t('setting.externalLinks', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
defaultChecked={settings.lastFM}
onChange={(e) => {
setSettings({
general: {
...settings,
lastFM: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.lastfm', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !settings.externalLinks,
title: t('setting.lastfm', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
defaultChecked={settings.musicBrainz}
onChange={(e) => {
setSettings({
general: {
...settings,
musicBrainz: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.musicbrainz', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !settings.externalLinks,
title: t('setting.musicbrainz', { postProcess: 'sentenceCase' }),
},
{
control: (
<Select
@@ -486,146 +290,6 @@ export const ControlSettings = () => {
isHidden: false,
title: t('setting.genreBehavior', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label={t('setting.homeFeature', { postProcess: 'sentenceCase' })}
defaultChecked={settings.homeFeature}
onChange={(e) =>
setSettings({
general: {
...settings,
homeFeature: e.currentTarget.checked,
},
})
}
/>
),
description: t('setting.homeFeature', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.homeFeature', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label={t('setting.albumBackground', { postProcess: 'sentenceCase' })}
defaultChecked={settings.albumBackground}
onChange={(e) =>
setSettings({
general: {
...settings,
albumBackground: e.currentTarget.checked,
},
})
}
/>
),
description: t('setting.albumBackground', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.albumBackground', { postProcess: 'sentenceCase' }),
},
{
control: (
<Slider
defaultValue={settings.albumBackgroundBlur}
label={(e) => `${e} rem`}
max={6}
min={0}
onChangeEnd={(e) => {
setSettings({
general: {
...settings,
albumBackgroundBlur: e,
},
});
}}
step={0.5}
w={100}
/>
),
description: t('setting.albumBackgroundBlur', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !settings.albumBackground,
title: t('setting.albumBackgroundBlur', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label={t('setting.artistBackground', { postProcess: 'sentenceCase' })}
defaultChecked={settings.artistBackground}
onChange={(e) =>
setSettings({
general: {
...settings,
artistBackground: e.currentTarget.checked,
},
})
}
/>
),
description: t('setting.artistBackground', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.artistBackground', { postProcess: 'sentenceCase' }),
},
{
control: (
<Slider
defaultValue={settings.artistBackgroundBlur}
label={(e) => `${e} rem`}
max={6}
min={0}
onChangeEnd={(e) => {
setSettings({
general: {
...settings,
artistBackgroundBlur: e,
},
});
}}
step={0.5}
w={100}
/>
),
description: t('setting.artistBackgroundBlur', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !settings.artistBackground,
title: t('setting.artistBackgroundBlur', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
aria-label={t('setting.playerbarOpenDrawer', { postProcess: 'sentenceCase' })}
defaultChecked={settings.playerbarOpenDrawer}
onChange={(e) =>
setSettings({
general: {
...settings,
playerbarOpenDrawer: e.currentTarget.checked,
},
})
}
/>
),
description: t('setting.playerbarOpenDrawer', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.playerbarOpenDrawer', { postProcess: 'sentenceCase' }),
},
{
control: (
<SegmentedControl
@@ -824,5 +488,10 @@ export const ControlSettings = () => {
: []),
];
return <SettingsSection options={controlOptions} />;
return (
<SettingsSection
options={controlOptions}
title={t('page.setting.controls', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -8,7 +8,6 @@ import { SettingsOptions } from '/@/renderer/features/settings/components/settin
import { useSettingSearchContext } from '/@/renderer/features/settings/context/search-context';
import { SortableItem } from '/@/renderer/store';
import { Button } from '/@/shared/components/button/button';
import { Divider } from '/@/shared/components/divider/divider';
export type DraggableItemsProps<K, T> = {
description: string;
@@ -122,7 +121,6 @@ export const DraggableItems = <K extends string, T extends SortableItem<K>>({
))}
</Reorder.Group>
)}
<Divider />
</>
);
};
@@ -1,28 +1,34 @@
import isElectron from 'is-electron';
import { Fragment } from 'react/jsx-runtime';
import { ApplicationSettings } from '/@/renderer/features/settings/components/general/application-settings';
import { ArtistSettings } from '/@/renderer/features/settings/components/general/artist-settings';
import { ControlSettings } from '/@/renderer/features/settings/components/general/control-settings';
import { HomeSettings } from '/@/renderer/features/settings/components/general/home-settings';
import { LyricSettings } from '/@/renderer/features/settings/components/general/lyric-settings';
import { RemoteSettings } from '/@/renderer/features/settings/components/general/remote-settings';
import { SidebarReorder } from '/@/renderer/features/settings/components/general/sidebar-reorder';
import { ScrobbleSettings } from '/@/renderer/features/settings/components/general/scrobble-settings';
import { SidebarSettings } from '/@/renderer/features/settings/components/general/sidebar-settings';
import { ThemeSettings } from '/@/renderer/features/settings/components/general/theme-settings';
import { CacheSettings } from '/@/renderer/features/settings/components/window/cache-settngs';
import { Divider } from '/@/shared/components/divider/divider';
import { Stack } from '/@/shared/components/stack/stack';
const sections = [
{ component: ApplicationSettings, key: 'application' },
{ component: ThemeSettings, key: 'theme' },
{ component: ControlSettings, key: 'control' },
{ component: SidebarSettings, key: 'sidebar' },
{ component: RemoteSettings, key: 'remote' },
{ component: ScrobbleSettings, key: 'scrobble' },
{ component: LyricSettings, key: 'lyrics' },
];
export const GeneralTab = () => {
return (
<Stack gap="md">
<ApplicationSettings />
<ThemeSettings />
<ControlSettings />
<HomeSettings />
<ArtistSettings />
<SidebarReorder />
<SidebarSettings />
{isElectron() && <RemoteSettings />}
<CacheSettings />
{sections.map(({ component: Section, key }, index) => (
<Fragment key={key}>
<Section />
{index < sections.length - 1 && <Divider />}
</Fragment>
))}
</Stack>
);
};
@@ -238,5 +238,10 @@ export const LyricSettings = () => {
},
];
return <SettingsSection divider={false} options={lyricOptions} />;
return (
<SettingsSection
options={lyricOptions}
title={t('page.setting.lyrics', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -155,5 +155,10 @@ export const RemoteSettings = () => {
},
];
return <SettingsSection options={controlOptions} />;
return (
<SettingsSection
options={controlOptions}
title={t('page.setting.remote', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -144,5 +144,10 @@ export const ScrobbleSettings = () => {
},
];
return <SettingsSection options={scrobbleOptions} />;
return (
<SettingsSection
options={scrobbleOptions}
title={t('page.setting.scrobble', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -1,6 +1,7 @@
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { SidebarReorder } from '/@/renderer/features/settings/components/general/sidebar-reorder';
import {
SettingOption,
SettingsSection,
@@ -60,5 +61,11 @@ export const SidebarSettings = () => {
},
];
return <SettingsSection options={options} />;
return (
<SettingsSection
extra={<SidebarReorder />}
options={options}
title={t('page.setting.sidebar', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -1,6 +1,7 @@
import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
import { StylesSettings } from '/@/renderer/features/settings/components/advanced/styles-settings';
import {
SettingOption,
SettingsSection,
@@ -163,5 +164,11 @@ export const ThemeSettings = () => {
},
];
return <SettingsSection options={themeOptions} />;
return (
<SettingsSection
extra={<StylesSettings />}
options={themeOptions}
title={t('page.setting.theme', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -7,6 +7,10 @@ import styles from './hotkeys-manager-settings.module.css';
import i18n from '/@/i18n/i18n';
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
import { useSettingSearchContext } from '/@/renderer/features/settings/context/search-context';
import { BindingActions, useHotkeySettings, useSettingsStoreActions } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
@@ -150,12 +154,12 @@ export const HotkeyManagerSettings = () => {
20,
);
const handleSetHotkey = useCallback(debouncedSetHotkey, [
bindings,
globalMediaHotkeys,
setSettings,
debouncedSetHotkey,
]);
const handleSetHotkey = useCallback(
(binding: BindingActions, e: KeyboardEvent<HTMLInputElement>) => {
debouncedSetHotkey(binding, e);
},
[debouncedSetHotkey],
);
const handleSetGlobalHotkey = useCallback(
(binding: BindingActions, e: ChangeEvent<HTMLInputElement>) => {
@@ -232,85 +236,97 @@ export const HotkeyManagerSettings = () => {
});
}, [bindings, keyword]);
const options: SettingOption[] = [];
return (
<>
<SettingsOptions
control={<></>}
description={t('setting.applicationHotkeys', {
context: 'description',
postProcess: 'sentenceCase',
})}
title={t('setting.applicationHotkeys', { postProcess: 'sentenceCase' })}
/>
<div className={styles.container}>
{filteredBindings.map((binding) => (
<Group key={`hotkey-${binding}`} wrap="nowrap">
<TextInput
readOnly
style={{ userSelect: 'none' }}
value={BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]}
/>
<TextInput
id={`hotkey-${binding}`}
leftSection={<Icon icon="keyboard" />}
onBlur={() => setSelected(null)}
onChange={() => {}}
onKeyDownCapture={(e) => {
if (selected !== (binding as BindingActions)) return;
handleSetHotkey(binding as BindingActions, e);
}}
readOnly
rightSection={
<ActionIcon
icon="edit"
onClick={() => {
setSelected(binding as BindingActions);
document.getElementById(`hotkey-${binding}`)?.focus();
}}
variant="transparent"
<SettingsSection
extra={
<>
<SettingsOptions
control={<></>}
description={t('setting.applicationHotkeys', {
context: 'description',
postProcess: 'sentenceCase',
})}
title={t('setting.applicationHotkeys', { postProcess: 'sentenceCase' })}
/>
<div className={styles.container}>
{filteredBindings.map((binding) => (
<Group key={`hotkey-${binding}`} wrap="nowrap">
<TextInput
readOnly
style={{ userSelect: 'none' }}
value={BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]}
/>
}
style={{
opacity: selected === (binding as BindingActions) ? 0.8 : 1,
outline: duplicateHotkeyMap.includes(
bindings[binding as keyof typeof BINDINGS_MAP].hotkey!,
)
? '1px dashed red'
: undefined,
}}
value={bindings[binding as keyof typeof BINDINGS_MAP].hotkey}
/>
{isElectron() && (
<Checkbox
checked={bindings[binding as keyof typeof BINDINGS_MAP].isGlobal}
disabled={
bindings[binding as keyof typeof BINDINGS_MAP].hotkey === ''
}
onChange={(e) =>
handleSetGlobalHotkey(binding as BindingActions, e)
}
size="md"
style={{
opacity: bindings[binding as keyof typeof BINDINGS_MAP]
.allowGlobal
? 1
: 0,
}}
/>
)}
{bindings[binding as keyof typeof BINDINGS_MAP].hotkey && (
<ActionIcon
icon="x"
iconProps={{
color: 'error',
}}
onClick={() => handleClearHotkey(binding as BindingActions)}
variant="transparent"
/>
)}
</Group>
))}
</div>
</>
<TextInput
id={`hotkey-${binding}`}
leftSection={<Icon icon="keyboard" />}
onBlur={() => setSelected(null)}
onChange={() => {}}
onKeyDownCapture={(e) => {
if (selected !== (binding as BindingActions)) return;
handleSetHotkey(binding as BindingActions, e);
}}
readOnly
rightSection={
<ActionIcon
icon="edit"
onClick={() => {
setSelected(binding as BindingActions);
document
.getElementById(`hotkey-${binding}`)
?.focus();
}}
variant="transparent"
/>
}
style={{
opacity: selected === (binding as BindingActions) ? 0.8 : 1,
outline: duplicateHotkeyMap.includes(
bindings[binding as keyof typeof BINDINGS_MAP].hotkey!,
)
? '1px dashed red'
: undefined,
}}
value={bindings[binding as keyof typeof BINDINGS_MAP].hotkey}
/>
{isElectron() && (
<Checkbox
checked={
bindings[binding as keyof typeof BINDINGS_MAP].isGlobal
}
disabled={
bindings[binding as keyof typeof BINDINGS_MAP]
.hotkey === ''
}
onChange={(e) =>
handleSetGlobalHotkey(binding as BindingActions, e)
}
size="md"
style={{
opacity: bindings[binding as keyof typeof BINDINGS_MAP]
.allowGlobal
? 1
: 0,
}}
/>
)}
{bindings[binding as keyof typeof BINDINGS_MAP].hotkey && (
<ActionIcon
icon="x"
iconProps={{
color: 'error',
}}
onClick={() => handleClearHotkey(binding as BindingActions)}
variant="transparent"
/>
)}
</Group>
))}
</div>
</>
}
options={options}
/>
);
};
@@ -1,14 +1,27 @@
import isElectron from 'is-electron';
import { Fragment } from 'react/jsx-runtime';
import { HotkeyManagerSettings } from '/@/renderer/features/settings/components/hotkeys/hotkey-manager-settings';
import { MediaSessionSettings } from '/@/renderer/features/settings/components/hotkeys/media-session-settings';
import { WindowHotkeySettings } from '/@/renderer/features/settings/components/hotkeys/window-hotkey-settings';
import { Divider } from '/@/shared/components/divider/divider';
import { Stack } from '/@/shared/components/stack/stack';
const sections = [
{ component: WindowHotkeySettings, hidden: !isElectron(), key: 'window' },
{ component: MediaSessionSettings, key: 'media-session' },
{ component: HotkeyManagerSettings, key: 'hotkey-manager' },
];
export const HotkeysTab = () => {
return (
<Stack gap="md">
{isElectron() && <WindowHotkeySettings />}
<HotkeyManagerSettings />
{sections.map(({ component: Section, hidden, key }, index) => (
<Fragment key={key}>
{!hidden && <Section />}
{index < sections.length - 1 && <Divider />}
</Fragment>
))}
</Stack>
);
};
@@ -44,5 +44,5 @@ export const MediaSessionSettings = () => {
},
];
return <SettingsSection divider options={mediaSessionOptions} />;
return <SettingsSection options={mediaSessionOptions} />;
};
@@ -20,7 +20,7 @@ const getAudioDevice = async () => {
return (devices || []).filter((dev: MediaDeviceInfo) => dev.kind === 'audiooutput');
};
export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) => {
export const AudioSettings = () => {
const { t } = useTranslation();
const settings = usePlaybackSettings();
const { setSettings } = useSettingsStoreActions();
@@ -137,5 +137,10 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
},
];
return <SettingsSection divider={!hasFancyAudio} options={audioOptions} />;
return (
<SettingsSection
options={audioOptions}
title={t('page.setting.audio', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -2,11 +2,9 @@ import isElectron from 'is-electron';
import { lazy, Suspense, useMemo } from 'react';
import { AudioSettings } from '/@/renderer/features/settings/components/playback/audio-settings';
import { LyricSettings } from '/@/renderer/features/settings/components/playback/lyric-settings';
import { MediaSessionSettings } from '/@/renderer/features/settings/components/playback/media-session-settings';
import { ScrobbleSettings } from '/@/renderer/features/settings/components/playback/scrobble-settings';
import { TranscodeSettings } from '/@/renderer/features/settings/components/playback/transcode-settings';
import { useSettingsStore } from '/@/renderer/store';
import { Divider } from '/@/shared/components/divider/divider';
import { Stack } from '/@/shared/components/stack/stack';
import { PlayerType } from '/@/shared/types/types';
@@ -29,12 +27,10 @@ export const PlaybackTab = () => {
return (
<Stack gap="md">
<AudioSettings hasFancyAudio={hasFancyAudio} />
<AudioSettings />
<Suspense fallback={<></>}>{hasFancyAudio && <MpvSettings />}</Suspense>
<Divider />
<TranscodeSettings />
<MediaSessionSettings />
<ScrobbleSettings />
<LyricSettings />
</Stack>
);
};
@@ -86,5 +86,10 @@ export const TranscodeSettings = () => {
},
];
return <SettingsSection divider options={transcodeOptions} />;
return (
<SettingsSection
options={transcodeOptions}
title={t('page.setting.transcoding', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -1,41 +1,15 @@
import isElectron from 'is-electron';
import { lazy } from 'react';
import { useTranslation } from 'react-i18next';
import { AdvancedTab } from '/@/renderer/features/settings/components/advanced/advanced-tab';
import { GeneralTab } from '/@/renderer/features/settings/components/general/general-tab';
import { HotkeysTab } from '/@/renderer/features/settings/components/hotkeys/hotkeys-tab';
import { PlaybackTab } from '/@/renderer/features/settings/components/playback/playback-tab';
import { WindowTab } from '/@/renderer/features/settings/components/window/window-tab';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import { Tabs } from '/@/shared/components/tabs/tabs';
const GeneralTab = lazy(() =>
import('/@/renderer/features/settings/components/general/general-tab').then((module) => ({
default: module.GeneralTab,
})),
);
const PlaybackTab = lazy(() =>
import('/@/renderer/features/settings/components/playback/playback-tab').then((module) => ({
default: module.PlaybackTab,
})),
);
const ApplicationTab = lazy(() =>
import('/@/renderer/features/settings/components/window/window-tab').then((module) => ({
default: module.WindowTab,
})),
);
const HotkeysTab = lazy(() =>
import('/@/renderer/features/settings/components/hotkeys/hotkeys-tab').then((module) => ({
default: module.HotkeysTab,
})),
);
const AdvancedTab = lazy(() =>
import('/@/renderer/features/settings/components/advanced/advanced-tab').then((module) => ({
default: module.AdvancedTab,
})),
);
export const SettingsContent = () => {
const { t } = useTranslation();
const currentTab = useSettingsStore((state) => state.tab);
@@ -81,7 +55,7 @@ export const SettingsContent = () => {
</Tabs.Panel>
{isElectron() && (
<Tabs.Panel value="window">
<ApplicationTab />
<WindowTab />
</Tabs.Panel>
)}
<Tabs.Panel value="advanced">
@@ -2,7 +2,8 @@ import { ReactNode } from 'react';
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
import { useSettingSearchContext } from '/@/renderer/features/settings/context/search-context';
import { Divider } from '/@/shared/components/divider/divider';
import { Stack } from '/@/shared/components/stack/stack';
import { TextTitle } from '/@/shared/components/text-title/text-title';
export type SettingOption = {
control: ReactNode;
@@ -13,11 +14,12 @@ export type SettingOption = {
};
interface SettingsSectionProps {
divider?: boolean;
extra?: ReactNode;
options: SettingOption[];
title?: ReactNode;
}
export const SettingsSection = ({ divider, options }: SettingsSectionProps) => {
export const SettingsSection = ({ extra, options, title }: SettingsSectionProps) => {
const keyword = useSettingSearchContext();
const hasKeyword = keyword !== '';
@@ -27,10 +29,17 @@ export const SettingsSection = ({ divider, options }: SettingsSectionProps) => {
return (
<>
{values.map((option) => (
<SettingsOptions key={`option-${option.title}`} {...option} />
))}
{divider !== false && values.length > 0 && <Divider />}
{title && (
<TextTitle fw={600} order={4}>
{title}
</TextTitle>
)}
<Stack gap="xl" px="xl">
{values.map((option) => (
<SettingsOptions key={`option-${option.title}`} {...option} />
))}
{extra}
</Stack>
</>
);
};
@@ -94,5 +94,10 @@ export const CacheSettings = () => {
},
];
return <SettingsSection divider={false} options={options} />;
return (
<SettingsSection
options={options}
title={t('page.setting.cache', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -268,5 +268,10 @@ export const DiscordSettings = () => {
},
];
return <SettingsSection options={discordOptions} />;
return (
<SettingsSection
options={discordOptions}
title={t('page.setting.discord', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -85,5 +85,10 @@ export const UpdateSettings = () => {
},
];
return <SettingsSection divider={true} options={updateOptions} />;
return (
<SettingsSection
options={updateOptions}
title={t('page.setting.updates', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -233,5 +233,10 @@ export const WindowSettings = () => {
},
];
return <SettingsSection options={windowOptions} />;
return (
<SettingsSection
options={windowOptions}
title={t('page.setting.application', { postProcess: 'sentenceCase' })}
/>
);
};
@@ -1,22 +1,29 @@
import isElectron from 'is-electron';
import { Fragment } from 'react/jsx-runtime';
import { DiscordSettings } from '/@/renderer/features/settings/components/window/discord-settings';
import { PasswordSettings } from '/@/renderer/features/settings/components/window/password-settings';
import { WindowSettings } from '/@/renderer/features/settings/components/window/window-settings';
import { Divider } from '/@/shared/components/divider/divider';
import { Stack } from '/@/shared/components/stack/stack';
const utils = isElectron() ? window.api.utils : null;
const sections = [
{ component: WindowSettings, key: 'window' },
{ component: DiscordSettings, key: 'discord' },
{ component: PasswordSettings, hidden: !utils?.isLinux(), key: 'password' },
];
export const WindowTab = () => {
return (
<Stack gap="md">
<WindowSettings />
<DiscordSettings />
{utils?.isLinux() && (
<>
<PasswordSettings />
</>
)}
{sections.map(({ component: Section, hidden, key }, index) => (
<Fragment key={key}>
{!hidden && <Section />}
{index < sections.length - 1 && <Divider />}
</Fragment>
))}
</Stack>
);
};
-4
View File
@@ -229,7 +229,6 @@ const GeneralSettingsSchema = z.object({
artistItems: z.array(SortableItemSchema(ArtistItemSchema)),
buttonSize: z.number(),
disabledContextMenu: z.record(z.string(), z.boolean()),
doubleClickQueueAll: z.boolean(),
externalLinks: z.boolean(),
followSystemTheme: z.boolean(),
genreTarget: GenreTargetSchema,
@@ -245,7 +244,6 @@ const GeneralSettingsSchema = z.object({
playerbarOpenDrawer: z.boolean(),
playerbarSlider: PlayerbarSliderSchema,
resume: z.boolean(),
showQueueDrawerButton: z.boolean(),
sidebarCollapsedNavigation: z.boolean(),
sidebarCollapseShared: z.boolean(),
sidebarItems: z.array(SidebarItemTypeSchema),
@@ -600,7 +598,6 @@ const initialState: SettingsState = {
artistItems,
buttonSize: 15,
disabledContextMenu: {},
doubleClickQueueAll: true,
externalLinks: true,
followSystemTheme: false,
genreTarget: GenreTarget.TRACK,
@@ -622,7 +619,6 @@ const initialState: SettingsState = {
type: PlayerbarSliderType.WAVEFORM,
},
resume: true,
showQueueDrawerButton: false,
sidebarCollapsedNavigation: true,
sidebarCollapseShared: false,
sidebarItems,