mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 10:23:33 +00:00
Add files
This commit is contained in:
commit
e87c814068
266 changed files with 63938 additions and 0 deletions
258
src/renderer/features/settings/components/general-tab.tsx
Normal file
258
src/renderer/features/settings/components/general-tab.tsx
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
import { Divider, Stack } from '@mantine/core';
|
||||
import { Select, Switch } from '/@/renderer/components';
|
||||
import isElectron from 'is-electron';
|
||||
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
|
||||
import { THEME_DATA } from '/@/renderer/hooks';
|
||||
import {
|
||||
useGeneralSettings,
|
||||
useSettingsStoreActions,
|
||||
SideQueueType,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { AppTheme } from '/@/renderer/themes/types';
|
||||
|
||||
const FONT_OPTIONS = [
|
||||
{ label: 'AnekTamil', value: 'AnekTamil' },
|
||||
{ label: 'Archivo', value: 'Archivo' },
|
||||
{ label: 'Circular STD', value: 'Circular STD' },
|
||||
{ label: 'Didact Gothic', value: 'Didact Gothic' },
|
||||
{ label: 'DM Sans', value: 'DM Sans' },
|
||||
{ label: 'Encode Sans', value: 'Encode Sans' },
|
||||
{ label: 'Epilogue', value: 'Epilogue' },
|
||||
{ label: 'Gotham', value: 'Gotham' },
|
||||
{ label: 'Inconsolata', value: 'Inconsolata' },
|
||||
{ label: 'Inter', value: 'Inter' },
|
||||
{ label: 'JetBrains Mono', value: 'JetBrainsMono' },
|
||||
{ label: 'Manrope', value: 'Manrope' },
|
||||
{ label: 'Montserrat', value: 'Montserrat' },
|
||||
{ label: 'Oxygen', value: 'Oxygen' },
|
||||
{ label: 'Poppins', value: 'Poppins' },
|
||||
{ label: 'Raleway', value: 'Raleway' },
|
||||
{ label: 'Roboto', value: 'Roboto' },
|
||||
{ label: 'Sora', value: 'Sora' },
|
||||
{ label: 'Work Sans', value: 'Work Sans' },
|
||||
];
|
||||
|
||||
const SIDE_QUEUE_OPTIONS = [
|
||||
{ label: 'Fixed', value: 'sideQueue' },
|
||||
{ label: 'Floating', value: 'sideDrawerQueue' },
|
||||
];
|
||||
|
||||
export const GeneralTab = () => {
|
||||
const settings = useGeneralSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const options = [
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
disabled
|
||||
data={['Windows', 'macOS']}
|
||||
defaultValue="Windows"
|
||||
/>
|
||||
),
|
||||
description: 'Adjust the style of the titlebar',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Titlebar style',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
disabled
|
||||
data={[]}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the application language',
|
||||
isHidden: false,
|
||||
title: 'Language',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={FONT_OPTIONS}
|
||||
defaultValue={settings.fontContent}
|
||||
onChange={(e) => {
|
||||
if (!e) return;
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
fontContent: e,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the application content font',
|
||||
isHidden: false,
|
||||
title: 'Font (Content)',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={FONT_OPTIONS}
|
||||
defaultValue={settings.fontHeader}
|
||||
onChange={(e) => {
|
||||
if (!e) return;
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
fontHeader: e,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the application header font',
|
||||
isHidden: false,
|
||||
title: 'Font (Header)',
|
||||
},
|
||||
];
|
||||
|
||||
const themeOptions = [
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
defaultChecked={settings.followSystemTheme}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
followSystemTheme: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Follows the system-defined light or dark preference',
|
||||
isHidden: false,
|
||||
title: 'Use system theme',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={THEME_DATA}
|
||||
defaultValue={settings.theme}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
theme: e as AppTheme,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the default theme',
|
||||
isHidden: settings.followSystemTheme,
|
||||
title: 'Theme',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={THEME_DATA}
|
||||
defaultValue={settings.themeDark}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
themeDark: e as AppTheme,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the dark theme',
|
||||
isHidden: !settings.followSystemTheme,
|
||||
title: 'Theme (dark)',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={THEME_DATA}
|
||||
defaultValue={settings.themeLight}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
themeLight: e as AppTheme,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the light theme',
|
||||
isHidden: !settings.followSystemTheme,
|
||||
title: 'Theme (light)',
|
||||
},
|
||||
];
|
||||
|
||||
const layoutOptions = [
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={SIDE_QUEUE_OPTIONS}
|
||||
defaultValue={settings.sideQueueType}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
sideQueueType: e as SideQueueType,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'The style of the sidebar play queue',
|
||||
isHidden: false,
|
||||
title: 'Side play queue style',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
defaultChecked={settings.showQueueDrawerButton}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
showQueueDrawerButton: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Display a hover icon on the right side of the application view the play queue',
|
||||
isHidden: false,
|
||||
title: 'Show floating queue hover area',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack spacing="xl">
|
||||
{options
|
||||
.filter((o) => !o.isHidden)
|
||||
.map((option) => (
|
||||
<SettingsOptions
|
||||
key={`general-${option.title}`}
|
||||
{...option}
|
||||
/>
|
||||
))}
|
||||
<Divider />
|
||||
{themeOptions
|
||||
.filter((o) => !o.isHidden)
|
||||
.map((option) => (
|
||||
<SettingsOptions
|
||||
key={`general-${option.title}`}
|
||||
{...option}
|
||||
/>
|
||||
))}
|
||||
<Divider />
|
||||
{layoutOptions
|
||||
.filter((o) => !o.isHidden)
|
||||
.map((option) => (
|
||||
<SettingsOptions
|
||||
key={`general-${option.title}`}
|
||||
{...option}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
383
src/renderer/features/settings/components/playback-tab.tsx
Normal file
383
src/renderer/features/settings/components/playback-tab.tsx
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Divider, Group, SelectItem, Stack } from '@mantine/core';
|
||||
import {
|
||||
FileInput,
|
||||
NumberInput,
|
||||
SegmentedControl,
|
||||
Select,
|
||||
Slider,
|
||||
Switch,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
toast,
|
||||
Text,
|
||||
} from '/@/renderer/components';
|
||||
import isElectron from 'is-electron';
|
||||
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
|
||||
import { useCurrentStatus, usePlayerStore } from '/@/renderer/store';
|
||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { PlaybackType, PlayerStatus, PlaybackStyle, CrossfadeStyle, Play } from '/@/renderer/types';
|
||||
|
||||
const localSettings = window.electron.localSettings;
|
||||
const mpvPlayer = window.electron.mpvPlayer;
|
||||
|
||||
const getAudioDevice = async () => {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return (devices || []).filter((dev: MediaDeviceInfo) => dev.kind === 'audiooutput');
|
||||
};
|
||||
|
||||
export const PlaybackTab = () => {
|
||||
const settings = useSettingsStore((state) => state.player);
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const status = useCurrentStatus();
|
||||
const [audioDevices, setAudioDevices] = useState<SelectItem[]>([]);
|
||||
const [mpvPath, setMpvPath] = useState('');
|
||||
const [mpvParameters, setMpvParameters] = useState('');
|
||||
|
||||
const handleSetMpvPath = (e: File) => {
|
||||
localSettings.set('mpv_path', e.path);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getMpvPath = async () => {
|
||||
if (!isElectron()) return setMpvPath('');
|
||||
const mpvPath = (await localSettings.get('mpv_path')) as string;
|
||||
return setMpvPath(mpvPath);
|
||||
};
|
||||
|
||||
const getMpvParameters = async () => {
|
||||
if (!isElectron()) return setMpvPath('');
|
||||
const mpvParametersFromSettings = (await localSettings.get('mpv_parameters')) as string[];
|
||||
const mpvParameters = mpvParametersFromSettings?.join('\n');
|
||||
return setMpvParameters(mpvParameters);
|
||||
};
|
||||
|
||||
getMpvPath();
|
||||
getMpvParameters();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const getAudioDevices = () => {
|
||||
getAudioDevice()
|
||||
.then((dev) => setAudioDevices(dev.map((d) => ({ label: d.label, value: d.deviceId }))))
|
||||
.catch(() => toast.error({ message: 'Error fetching audio devices' }));
|
||||
};
|
||||
|
||||
getAudioDevices();
|
||||
}, []);
|
||||
|
||||
const playerOptions = [
|
||||
{
|
||||
control: (
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{
|
||||
disabled: !isElectron(),
|
||||
label: 'Mpv',
|
||||
value: PlaybackType.LOCAL,
|
||||
},
|
||||
{ label: 'Web', value: PlaybackType.WEB },
|
||||
]}
|
||||
defaultValue={settings.type}
|
||||
disabled={status === PlayerStatus.PLAYING}
|
||||
onChange={(e) => {
|
||||
setSettings({ player: { ...settings, type: e as PlaybackType } });
|
||||
if (isElectron() && e === PlaybackType.LOCAL) {
|
||||
const queueData = usePlayerStore.getState().actions.getPlayerData();
|
||||
mpvPlayer.setQueue(queueData);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'The audio player to use for playback',
|
||||
isHidden: !isElectron(),
|
||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||
title: 'Audio player',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<FileInput
|
||||
placeholder={mpvPath}
|
||||
size="sm"
|
||||
width={225}
|
||||
onChange={handleSetMpvPath}
|
||||
/>
|
||||
),
|
||||
description: 'The location of your mpv executable',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
note: 'Restart required',
|
||||
title: 'Mpv executable path',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Stack spacing="xs">
|
||||
<Textarea
|
||||
autosize
|
||||
defaultValue={mpvParameters}
|
||||
minRows={4}
|
||||
placeholder={'--gapless-playback=yes\n--prefetch-playlist=yes'}
|
||||
width={225}
|
||||
onBlur={(e) => {
|
||||
if (isElectron()) {
|
||||
localSettings.set('mpv_parameters', e.currentTarget.value.split('\n'));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
),
|
||||
description: (
|
||||
<Text
|
||||
$noSelect
|
||||
$secondary
|
||||
size="sm"
|
||||
>
|
||||
Options to pass to the player{' '}
|
||||
<a
|
||||
href="https://mpv.io/manual/stable/#audio"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
https://mpv.io/manual/stable/#audio
|
||||
</a>
|
||||
</Text>
|
||||
),
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
note: 'Restart required',
|
||||
title: 'Mpv parameters',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
clearable
|
||||
data={audioDevices}
|
||||
defaultValue={settings.audioDeviceId}
|
||||
disabled={settings.type !== PlaybackType.WEB}
|
||||
onChange={(e) => setSettings({ player: { ...settings, audioDeviceId: e } })}
|
||||
/>
|
||||
),
|
||||
description: 'The audio device to use for playback (web player only)',
|
||||
isHidden: !isElectron() || settings.type !== PlaybackType.WEB,
|
||||
title: 'Audio device',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: 'Normal', value: PlaybackStyle.GAPLESS },
|
||||
{ label: 'Crossfade', value: PlaybackStyle.CROSSFADE },
|
||||
]}
|
||||
defaultValue={settings.style}
|
||||
disabled={settings.type !== PlaybackType.WEB || status === PlayerStatus.PLAYING}
|
||||
onChange={(e) => setSettings({ player: { ...settings, style: e as PlaybackStyle } })}
|
||||
/>
|
||||
),
|
||||
description: 'Adjust the playback style (web player only)',
|
||||
isHidden: settings.type !== PlaybackType.WEB,
|
||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||
title: 'Playback style',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Slider
|
||||
defaultValue={settings.crossfadeDuration}
|
||||
disabled={
|
||||
settings.type !== PlaybackType.WEB ||
|
||||
settings.style !== PlaybackStyle.CROSSFADE ||
|
||||
status === PlayerStatus.PLAYING
|
||||
}
|
||||
max={15}
|
||||
min={0}
|
||||
w={100}
|
||||
onChangeEnd={(e) => setSettings({ player: { ...settings, crossfadeDuration: e } })}
|
||||
/>
|
||||
),
|
||||
description: 'Adjust the crossfade duration (web player only)',
|
||||
isHidden: settings.type !== PlaybackType.WEB,
|
||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||
title: 'Crossfade Duration',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{ label: 'Linear', value: CrossfadeStyle.LINEAR },
|
||||
{ label: 'Constant Power', value: CrossfadeStyle.CONSTANT_POWER },
|
||||
{
|
||||
label: 'Constant Power (Slow cut)',
|
||||
value: CrossfadeStyle.CONSTANT_POWER_SLOW_CUT,
|
||||
},
|
||||
{
|
||||
label: 'Constant Power (Slow fade)',
|
||||
value: CrossfadeStyle.CONSTANT_POWER_SLOW_FADE,
|
||||
},
|
||||
{ label: 'Dipped', value: CrossfadeStyle.DIPPED },
|
||||
{ label: 'Equal Power', value: CrossfadeStyle.EQUALPOWER },
|
||||
]}
|
||||
defaultValue={settings.crossfadeStyle}
|
||||
disabled={
|
||||
settings.type !== PlaybackType.WEB ||
|
||||
settings.style !== PlaybackStyle.CROSSFADE ||
|
||||
status === PlayerStatus.PLAYING
|
||||
}
|
||||
width={200}
|
||||
onChange={(e) => {
|
||||
if (!e) return;
|
||||
setSettings({
|
||||
player: { ...settings, crossfadeStyle: e as CrossfadeStyle },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Change the crossfade algorithm (web player only)',
|
||||
isHidden: settings.type !== PlaybackType.WEB,
|
||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||
title: 'Crossfade Style',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Toggle global media hotkeys"
|
||||
defaultChecked={settings.globalMediaHotkeys}
|
||||
disabled={!isElectron()}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
player: {
|
||||
...settings,
|
||||
globalMediaHotkeys: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
localSettings.set('global_media_hotkeys', e.currentTarget.checked);
|
||||
|
||||
if (e.currentTarget.checked) {
|
||||
localSettings.enableMediaKeys();
|
||||
} else {
|
||||
localSettings.disableMediaKeys();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Enable or disable the usage of your system media hotkeys to control the audio player (desktop only)',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Global media hotkeys',
|
||||
},
|
||||
];
|
||||
|
||||
const otherOptions = [
|
||||
{
|
||||
control: (
|
||||
<SegmentedControl
|
||||
data={[
|
||||
{ label: 'Now', value: Play.NOW },
|
||||
{ label: 'Next', value: Play.NEXT },
|
||||
{ label: 'Last', value: Play.LAST },
|
||||
]}
|
||||
defaultValue={settings.playButtonBehavior}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
player: {
|
||||
...settings,
|
||||
playButtonBehavior: e as Play,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
description: 'The default behavior of the play button when adding songs to the queue',
|
||||
isHidden: false,
|
||||
title: 'Play button behavior',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Toggle skip buttons"
|
||||
defaultChecked={settings.skipButtons?.enabled}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
player: {
|
||||
...settings,
|
||||
skipButtons: {
|
||||
...settings.skipButtons,
|
||||
enabled: e.currentTarget.checked,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
description: 'Show or hide the skip buttons on the playerbar',
|
||||
isHidden: false,
|
||||
title: 'Show skip buttons',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Group>
|
||||
<Tooltip label="Backward">
|
||||
<NumberInput
|
||||
defaultValue={settings.skipButtons.skipBackwardSeconds}
|
||||
min={0}
|
||||
width={75}
|
||||
onBlur={(e) =>
|
||||
setSettings({
|
||||
player: {
|
||||
...settings,
|
||||
skipButtons: {
|
||||
...settings.skipButtons,
|
||||
skipBackwardSeconds: e.currentTarget.value
|
||||
? Number(e.currentTarget.value)
|
||||
: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="Forward">
|
||||
<NumberInput
|
||||
defaultValue={settings.skipButtons.skipForwardSeconds}
|
||||
min={0}
|
||||
width={75}
|
||||
onBlur={(e) =>
|
||||
setSettings({
|
||||
player: {
|
||||
...settings,
|
||||
skipButtons: {
|
||||
...settings.skipButtons,
|
||||
skipForwardSeconds: e.currentTarget.value ? Number(e.currentTarget.value) : 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
),
|
||||
description:
|
||||
'The number (in seconds) to skip forward or backward when using the skip buttons',
|
||||
isHidden: false,
|
||||
title: 'Skip duration',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack spacing="xl">
|
||||
{playerOptions
|
||||
.filter((o) => !o.isHidden)
|
||||
.map((option) => (
|
||||
<SettingsOptions
|
||||
key={`playback-${option.title}`}
|
||||
{...option}
|
||||
/>
|
||||
))}
|
||||
<Divider />
|
||||
{otherOptions
|
||||
.filter((o) => !o.isHidden)
|
||||
.map((option) => (
|
||||
<SettingsOptions
|
||||
key={`playerbar-${option.title}`}
|
||||
{...option}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import React from 'react';
|
||||
import { Group, Stack } from '@mantine/core';
|
||||
import { RiInformationLine } from 'react-icons/ri';
|
||||
import { Text, Tooltip } from '/@/renderer/components';
|
||||
|
||||
interface SettingsOptionProps {
|
||||
control: React.ReactNode;
|
||||
description?: React.ReactNode | string;
|
||||
note?: string;
|
||||
title: React.ReactNode | string;
|
||||
}
|
||||
|
||||
export const SettingsOptions = ({ title, description, control, note }: SettingsOptionProps) => {
|
||||
return (
|
||||
<>
|
||||
<Group
|
||||
noWrap
|
||||
position="apart"
|
||||
sx={{ alignItems: 'center' }}
|
||||
>
|
||||
<Stack
|
||||
spacing="xs"
|
||||
sx={{
|
||||
alignSelf: 'flex-start',
|
||||
display: 'flex',
|
||||
maxWidth: '50%',
|
||||
}}
|
||||
>
|
||||
<Group>
|
||||
<Text
|
||||
$noSelect
|
||||
size="sm"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
{note && (
|
||||
<Tooltip
|
||||
label={note}
|
||||
openDelay={0}
|
||||
>
|
||||
<Group>
|
||||
<RiInformationLine size={15} />
|
||||
</Group>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
{React.isValidElement(description) ? (
|
||||
description
|
||||
) : (
|
||||
<Text
|
||||
$noSelect
|
||||
$secondary
|
||||
size="sm"
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
<Group position="right">{control}</Group>
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
SettingsOptions.defaultProps = {
|
||||
description: undefined,
|
||||
note: undefined,
|
||||
};
|
||||
72
src/renderer/features/settings/components/settings.tsx
Normal file
72
src/renderer/features/settings/components/settings.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { Box } from '@mantine/core';
|
||||
import { Tabs } from '/@/renderer/components';
|
||||
import type { Variants } from 'framer-motion';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { GeneralTab } from '/@/renderer/features/settings/components/general-tab';
|
||||
import { PlaybackTab } from '/@/renderer/features/settings/components/playback-tab';
|
||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
|
||||
export const Settings = () => {
|
||||
const currentTab = useSettingsStore((state) => state.tab);
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const tabVariants: Variants = {
|
||||
in: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
},
|
||||
x: 0,
|
||||
},
|
||||
out: {
|
||||
opacity: 0,
|
||||
x: 50,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
m={5}
|
||||
sx={{ height: '800px', maxHeight: '50vh', overflowX: 'hidden' }}
|
||||
>
|
||||
<AnimatePresence initial={false}>
|
||||
<Tabs
|
||||
keepMounted={false}
|
||||
orientation="vertical"
|
||||
styles={{
|
||||
tab: {
|
||||
fontSize: '1.1rem',
|
||||
padding: '0.5rem 1rem',
|
||||
},
|
||||
}}
|
||||
value={currentTab}
|
||||
variant="pills"
|
||||
onTabChange={(e) => e && setSettings({ tab: e })}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="general">General</Tabs.Tab>
|
||||
<Tabs.Tab value="playback">Playback</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="general">
|
||||
<motion.div
|
||||
animate="in"
|
||||
initial="out"
|
||||
variants={tabVariants}
|
||||
>
|
||||
<GeneralTab />
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="playback">
|
||||
<motion.div
|
||||
animate="in"
|
||||
initial="out"
|
||||
variants={tabVariants}
|
||||
>
|
||||
<PlaybackTab />
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue