mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
reorganize and redesign settings
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+6
-1
@@ -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' })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+6
-1
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user