mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 10:23:33 +00:00
Lint all files
This commit is contained in:
parent
22af76b4d6
commit
30e52ebb54
334 changed files with 76519 additions and 75932 deletions
|
|
@ -1,87 +1,87 @@
|
|||
import isElectron from 'is-electron';
|
||||
import { NumberInput, Select } from '/@/renderer/components';
|
||||
import {
|
||||
SettingsSection,
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
SettingOption,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
|
||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
|
||||
const FONT_OPTIONS = [
|
||||
{ label: 'Archivo', value: 'Archivo' },
|
||||
{ label: 'Fredoka', value: 'Fredoka' },
|
||||
{ label: 'League Spartan', value: 'League Spartan' },
|
||||
{ label: 'Lexend', value: 'Lexend' },
|
||||
{ label: 'Poppins', value: 'Poppins' },
|
||||
{ label: 'Raleway', value: 'Raleway' },
|
||||
{ label: 'Sora', value: 'Sora' },
|
||||
{ label: 'Work Sans', value: 'Work Sans' },
|
||||
{ label: 'Archivo', value: 'Archivo' },
|
||||
{ label: 'Fredoka', value: 'Fredoka' },
|
||||
{ label: 'League Spartan', value: 'League Spartan' },
|
||||
{ label: 'Lexend', value: 'Lexend' },
|
||||
{ label: 'Poppins', value: 'Poppins' },
|
||||
{ label: 'Raleway', value: 'Raleway' },
|
||||
{ label: 'Sora', value: 'Sora' },
|
||||
{ label: 'Work Sans', value: 'Work Sans' },
|
||||
];
|
||||
|
||||
export const ApplicationSettings = () => {
|
||||
const settings = useGeneralSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const settings = useGeneralSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const options: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
disabled
|
||||
data={[]}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the application language',
|
||||
isHidden: false,
|
||||
title: 'Language',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
searchable
|
||||
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: (
|
||||
<NumberInput
|
||||
max={300}
|
||||
min={50}
|
||||
value={settings.zoomFactor}
|
||||
onBlur={(e) => {
|
||||
if (!e) return;
|
||||
const newVal = e.currentTarget.value
|
||||
? Math.min(Math.max(Number(e.currentTarget.value), 50), 300)
|
||||
: settings.zoomFactor;
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
zoomFactor: newVal,
|
||||
},
|
||||
});
|
||||
localSettings.setZoomFactor(newVal);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the application zoom factor in percent',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Zoom factor',
|
||||
},
|
||||
];
|
||||
const options: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
disabled
|
||||
data={[]}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the application language',
|
||||
isHidden: false,
|
||||
title: 'Language',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
searchable
|
||||
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: (
|
||||
<NumberInput
|
||||
max={300}
|
||||
min={50}
|
||||
value={settings.zoomFactor}
|
||||
onBlur={(e) => {
|
||||
if (!e) return;
|
||||
const newVal = e.currentTarget.value
|
||||
? Math.min(Math.max(Number(e.currentTarget.value), 50), 300)
|
||||
: settings.zoomFactor;
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
zoomFactor: newVal,
|
||||
},
|
||||
});
|
||||
localSettings.setZoomFactor(newVal);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the application zoom factor in percent',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Zoom factor',
|
||||
},
|
||||
];
|
||||
|
||||
return <SettingsSection options={options} />;
|
||||
return <SettingsSection options={options} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,196 +3,199 @@ import { Group } from '@mantine/core';
|
|||
import { Select, Tooltip, NumberInput, Switch, Slider } from '/@/renderer/components';
|
||||
import { SettingsSection } from '/@/renderer/features/settings/components/settings-section';
|
||||
import {
|
||||
SideQueueType,
|
||||
useGeneralSettings,
|
||||
useSettingsStoreActions,
|
||||
SideQueueType,
|
||||
useGeneralSettings,
|
||||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { Play } from '/@/renderer/types';
|
||||
|
||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
|
||||
const SIDE_QUEUE_OPTIONS = [
|
||||
{ label: 'Fixed', value: 'sideQueue' },
|
||||
{ label: 'Floating', value: 'sideDrawerQueue' },
|
||||
{ label: 'Fixed', value: 'sideQueue' },
|
||||
{ label: 'Floating', value: 'sideDrawerQueue' },
|
||||
];
|
||||
|
||||
export const ControlSettings = () => {
|
||||
const settings = useGeneralSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const settings = useGeneralSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const controlOptions = [
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Toggle skip buttons"
|
||||
defaultChecked={settings.skipButtons?.enabled}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
general: {
|
||||
...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({
|
||||
general: {
|
||||
...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({
|
||||
general: {
|
||||
...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',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{ label: 'Now', value: Play.NOW },
|
||||
{ label: 'Next', value: Play.NEXT },
|
||||
{ label: 'Last', value: Play.LAST },
|
||||
]}
|
||||
defaultValue={settings.playButtonBehavior}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
general: {
|
||||
...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: (
|
||||
<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',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Slider
|
||||
defaultValue={settings.volumeWheelStep}
|
||||
max={20}
|
||||
min={1}
|
||||
w={100}
|
||||
onChangeEnd={(e) => {
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
volumeWheelStep: e,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'The amount of volume to change when scrolling the mouse wheel on the volume slider',
|
||||
isHidden: false,
|
||||
title: 'Volume wheel step',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
defaultChecked={settings.resume}
|
||||
onChange={(e) => {
|
||||
localSettings?.set('resume', e.target.checked);
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
resume: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'When exiting, save the current play queue and restore it when reopening',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Save play queue',
|
||||
},
|
||||
];
|
||||
const controlOptions = [
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Toggle skip buttons"
|
||||
defaultChecked={settings.skipButtons?.enabled}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
general: {
|
||||
...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({
|
||||
general: {
|
||||
...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({
|
||||
general: {
|
||||
...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',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{ label: 'Now', value: Play.NOW },
|
||||
{ label: 'Next', value: Play.NEXT },
|
||||
{ label: 'Last', value: Play.LAST },
|
||||
]}
|
||||
defaultValue={settings.playButtonBehavior}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
general: {
|
||||
...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: (
|
||||
<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',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Slider
|
||||
defaultValue={settings.volumeWheelStep}
|
||||
max={20}
|
||||
min={1}
|
||||
w={100}
|
||||
onChangeEnd={(e) => {
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
volumeWheelStep: e,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'The amount of volume to change when scrolling the mouse wheel on the volume slider',
|
||||
isHidden: false,
|
||||
title: 'Volume wheel step',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
defaultChecked={settings.resume}
|
||||
onChange={(e) => {
|
||||
localSettings?.set('resume', e.target.checked);
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
resume: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'When exiting, save the current play queue and restore it when reopening',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Save play queue',
|
||||
},
|
||||
];
|
||||
|
||||
return <SettingsSection options={controlOptions} />;
|
||||
return <SettingsSection options={controlOptions} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@ import { SidebarSettings } from '/@/renderer/features/settings/components/genera
|
|||
import { ThemeSettings } from '/@/renderer/features/settings/components/general/theme-settings';
|
||||
|
||||
export const GeneralTab = () => {
|
||||
return (
|
||||
<Stack spacing="md">
|
||||
<ApplicationSettings />
|
||||
<Divider />
|
||||
<ThemeSettings />
|
||||
<Divider />
|
||||
<ControlSettings />
|
||||
<Divider />
|
||||
<SidebarSettings />
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack spacing="md">
|
||||
<ApplicationSettings />
|
||||
<Divider />
|
||||
<ThemeSettings />
|
||||
<Divider />
|
||||
<ControlSettings />
|
||||
<Divider />
|
||||
<SidebarSettings />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,126 +8,126 @@ import { useSettingsStoreActions, useGeneralSettings } from '../../../../store/s
|
|||
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
|
||||
|
||||
const DragHandle = ({ dragControls }: any) => {
|
||||
return (
|
||||
<MdDragIndicator
|
||||
color="white"
|
||||
style={{ cursor: 'grab' }}
|
||||
onPointerDown={(event) => dragControls.start(event)}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<MdDragIndicator
|
||||
color="white"
|
||||
style={{ cursor: 'grab' }}
|
||||
onPointerDown={(event) => dragControls.start(event)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface SidebarItem {
|
||||
disabled: boolean;
|
||||
id: string;
|
||||
disabled: boolean;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface DraggableSidebarItemProps {
|
||||
handleChangeDisabled: (id: string, e: boolean) => void;
|
||||
item: SidebarItem;
|
||||
handleChangeDisabled: (id: string, e: boolean) => void;
|
||||
item: SidebarItem;
|
||||
}
|
||||
|
||||
const DraggableSidebarItem = ({ item, handleChangeDisabled }: DraggableSidebarItemProps) => {
|
||||
const dragControls = useDragControls();
|
||||
const dragControls = useDragControls();
|
||||
|
||||
return (
|
||||
<Reorder.Item
|
||||
as="div"
|
||||
dragControls={dragControls}
|
||||
dragListener={false}
|
||||
value={item}
|
||||
>
|
||||
<Group
|
||||
noWrap
|
||||
h="3rem"
|
||||
style={{ boxShadow: '0 1px 3px rgba(0,0,0,.1)' }}
|
||||
>
|
||||
<Checkbox
|
||||
checked={!item.disabled}
|
||||
onChange={(e) => handleChangeDisabled(item.id, e.target.checked)}
|
||||
/>
|
||||
<DragHandle dragControls={dragControls} />
|
||||
{item.id}
|
||||
</Group>
|
||||
</Reorder.Item>
|
||||
);
|
||||
return (
|
||||
<Reorder.Item
|
||||
as="div"
|
||||
dragControls={dragControls}
|
||||
dragListener={false}
|
||||
value={item}
|
||||
>
|
||||
<Group
|
||||
noWrap
|
||||
h="3rem"
|
||||
style={{ boxShadow: '0 1px 3px rgba(0,0,0,.1)' }}
|
||||
>
|
||||
<Checkbox
|
||||
checked={!item.disabled}
|
||||
onChange={(e) => handleChangeDisabled(item.id, e.target.checked)}
|
||||
/>
|
||||
<DragHandle dragControls={dragControls} />
|
||||
{item.id}
|
||||
</Group>
|
||||
</Reorder.Item>
|
||||
);
|
||||
};
|
||||
|
||||
export const SidebarSettings = () => {
|
||||
const settings = useGeneralSettings();
|
||||
const { setSidebarItems, setSettings } = useSettingsStoreActions();
|
||||
const settings = useGeneralSettings();
|
||||
const { setSidebarItems, setSettings } = useSettingsStoreActions();
|
||||
|
||||
const [localSidebarItems, setLocalSidebarItems] = useState(settings.sidebarItems);
|
||||
const [localSidebarItems, setLocalSidebarItems] = useState(settings.sidebarItems);
|
||||
|
||||
const handleSave = () => {
|
||||
setSidebarItems(localSidebarItems);
|
||||
};
|
||||
const handleSave = () => {
|
||||
setSidebarItems(localSidebarItems);
|
||||
};
|
||||
|
||||
const handleChangeDisabled = useCallback((id: string, e: boolean) => {
|
||||
setLocalSidebarItems((items) =>
|
||||
items.map((item) => {
|
||||
if (item.id === id) {
|
||||
return {
|
||||
...item,
|
||||
disabled: !e,
|
||||
};
|
||||
}
|
||||
const handleChangeDisabled = useCallback((id: string, e: boolean) => {
|
||||
setLocalSidebarItems((items) =>
|
||||
items.map((item) => {
|
||||
if (item.id === id) {
|
||||
return {
|
||||
...item,
|
||||
disabled: !e,
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
}),
|
||||
return item;
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleSetSidebarPlaylistList = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
sidebarPlaylistList: e.target.checked,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const isSaveButtonDisabled = isEqual(settings.sidebarItems, localSidebarItems);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsOptions
|
||||
control={
|
||||
<Switch
|
||||
checked={settings.sidebarPlaylistList}
|
||||
onChange={handleSetSidebarPlaylistList}
|
||||
/>
|
||||
}
|
||||
description="Show playlist list in sidebar"
|
||||
title="Sidebar playlist list"
|
||||
/>
|
||||
<SettingsOptions
|
||||
control={
|
||||
<Button
|
||||
compact
|
||||
disabled={isSaveButtonDisabled}
|
||||
variant="filled"
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save sidebar configuration
|
||||
</Button>
|
||||
}
|
||||
description="Select the items and order in which they appear in the sidebar"
|
||||
title="Sidebar configuration"
|
||||
/>
|
||||
<Reorder.Group
|
||||
axis="y"
|
||||
values={localSidebarItems}
|
||||
onReorder={setLocalSidebarItems}
|
||||
>
|
||||
{localSidebarItems.map((item) => (
|
||||
<DraggableSidebarItem
|
||||
key={item.id}
|
||||
handleChangeDisabled={handleChangeDisabled}
|
||||
item={item}
|
||||
/>
|
||||
))}
|
||||
</Reorder.Group>
|
||||
</>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleSetSidebarPlaylistList = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
sidebarPlaylistList: e.target.checked,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const isSaveButtonDisabled = isEqual(settings.sidebarItems, localSidebarItems);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsOptions
|
||||
control={
|
||||
<Switch
|
||||
checked={settings.sidebarPlaylistList}
|
||||
onChange={handleSetSidebarPlaylistList}
|
||||
/>
|
||||
}
|
||||
description="Show playlist list in sidebar"
|
||||
title="Sidebar playlist list"
|
||||
/>
|
||||
<SettingsOptions
|
||||
control={
|
||||
<Button
|
||||
compact
|
||||
disabled={isSaveButtonDisabled}
|
||||
variant="filled"
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save sidebar configuration
|
||||
</Button>
|
||||
}
|
||||
description="Select the items and order in which they appear in the sidebar"
|
||||
title="Sidebar configuration"
|
||||
/>
|
||||
<Reorder.Group
|
||||
axis="y"
|
||||
values={localSidebarItems}
|
||||
onReorder={setLocalSidebarItems}
|
||||
>
|
||||
{localSidebarItems.map((item) => (
|
||||
<DraggableSidebarItem
|
||||
key={item.id}
|
||||
handleChangeDisabled={handleChangeDisabled}
|
||||
item={item}
|
||||
/>
|
||||
))}
|
||||
</Reorder.Group>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,93 +1,93 @@
|
|||
import { Switch, Select } from '/@/renderer/components';
|
||||
import {
|
||||
SettingsSection,
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
SettingOption,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { THEME_DATA } from '/@/renderer/hooks';
|
||||
import { useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { AppTheme } from '/@/renderer/themes/types';
|
||||
|
||||
export const ThemeSettings = () => {
|
||||
const settings = useGeneralSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const settings = useGeneralSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const themeOptions: SettingOption[] = [
|
||||
{
|
||||
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 themeOptions: SettingOption[] = [
|
||||
{
|
||||
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)',
|
||||
},
|
||||
];
|
||||
|
||||
return <SettingsSection options={themeOptions} />;
|
||||
return <SettingsSection options={themeOptions} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,222 +11,231 @@ import { SettingsOptions } from '/@/renderer/features/settings/components/settin
|
|||
const ipc = isElectron() ? window.electron.ipc : null;
|
||||
|
||||
const BINDINGS_MAP: Record<BindingActions, string> = {
|
||||
globalSearch: 'Global search',
|
||||
localSearch: 'In-page search',
|
||||
next: 'Next track',
|
||||
pause: 'Pause',
|
||||
play: 'Play',
|
||||
playPause: 'Play / Pause',
|
||||
previous: 'Previous track',
|
||||
skipBackward: 'Skip backward',
|
||||
skipForward: 'Skip forward',
|
||||
stop: 'Stop',
|
||||
toggleFullscreenPlayer: 'Toggle fullscreen player',
|
||||
toggleQueue: 'Toggle queue',
|
||||
toggleRepeat: 'Toggle repeat',
|
||||
toggleShuffle: 'Toggle shuffle',
|
||||
volumeDown: 'Volume down',
|
||||
volumeMute: 'Volume mute',
|
||||
volumeUp: 'Volume up',
|
||||
zoomIn: 'Zoom in',
|
||||
zoomOut: 'Zoom out',
|
||||
globalSearch: 'Global search',
|
||||
localSearch: 'In-page search',
|
||||
next: 'Next track',
|
||||
pause: 'Pause',
|
||||
play: 'Play',
|
||||
playPause: 'Play / Pause',
|
||||
previous: 'Previous track',
|
||||
skipBackward: 'Skip backward',
|
||||
skipForward: 'Skip forward',
|
||||
stop: 'Stop',
|
||||
toggleFullscreenPlayer: 'Toggle fullscreen player',
|
||||
toggleQueue: 'Toggle queue',
|
||||
toggleRepeat: 'Toggle repeat',
|
||||
toggleShuffle: 'Toggle shuffle',
|
||||
volumeDown: 'Volume down',
|
||||
volumeMute: 'Volume mute',
|
||||
volumeUp: 'Volume up',
|
||||
zoomIn: 'Zoom in',
|
||||
zoomOut: 'Zoom out',
|
||||
};
|
||||
|
||||
const HotkeysContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
button {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const HotkeyManagerSettings = () => {
|
||||
const { bindings, globalMediaHotkeys } = useHotkeySettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const [selected, setSelected] = useState<BindingActions | null>(null);
|
||||
const { bindings, globalMediaHotkeys } = useHotkeySettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const [selected, setSelected] = useState<BindingActions | null>(null);
|
||||
|
||||
const debouncedSetHotkey = debounce(
|
||||
(binding: BindingActions, e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
const IGNORED_KEYS = ['Control', 'Alt', 'Shift', 'Meta', ' ', 'Escape'];
|
||||
const keys = [];
|
||||
if (e.ctrlKey) keys.push('mod');
|
||||
if (e.altKey) keys.push('alt');
|
||||
if (e.shiftKey) keys.push('shift');
|
||||
if (e.metaKey) keys.push('meta');
|
||||
if (e.key === ' ') keys.push('space');
|
||||
if (!IGNORED_KEYS.includes(e.key)) {
|
||||
if (e.code.includes('Numpad')) {
|
||||
if (e.key === '+') keys.push('numpadadd');
|
||||
else if (e.key === '-') keys.push('numpadsubtract');
|
||||
else if (e.key === '*') keys.push('numpadmultiply');
|
||||
else if (e.key === '/') keys.push('numpaddivide');
|
||||
else if (e.key === '.') keys.push('numpaddecimal');
|
||||
else keys.push(`numpad${e.key}`.toLowerCase());
|
||||
} else if (e.key === '+') {
|
||||
keys.push('equal');
|
||||
} else {
|
||||
keys.push(e.key?.toLowerCase());
|
||||
}
|
||||
}
|
||||
const debouncedSetHotkey = debounce(
|
||||
(binding: BindingActions, e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
const IGNORED_KEYS = ['Control', 'Alt', 'Shift', 'Meta', ' ', 'Escape'];
|
||||
const keys = [];
|
||||
if (e.ctrlKey) keys.push('mod');
|
||||
if (e.altKey) keys.push('alt');
|
||||
if (e.shiftKey) keys.push('shift');
|
||||
if (e.metaKey) keys.push('meta');
|
||||
if (e.key === ' ') keys.push('space');
|
||||
if (!IGNORED_KEYS.includes(e.key)) {
|
||||
if (e.code.includes('Numpad')) {
|
||||
if (e.key === '+') keys.push('numpadadd');
|
||||
else if (e.key === '-') keys.push('numpadsubtract');
|
||||
else if (e.key === '*') keys.push('numpadmultiply');
|
||||
else if (e.key === '/') keys.push('numpaddivide');
|
||||
else if (e.key === '.') keys.push('numpaddecimal');
|
||||
else keys.push(`numpad${e.key}`.toLowerCase());
|
||||
} else if (e.key === '+') {
|
||||
keys.push('equal');
|
||||
} else {
|
||||
keys.push(e.key?.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
const bindingString = keys.join('+');
|
||||
const bindingString = keys.join('+');
|
||||
|
||||
const updatedBindings = {
|
||||
...bindings,
|
||||
[binding]: { ...bindings[binding], hotkey: bindingString },
|
||||
};
|
||||
const updatedBindings = {
|
||||
...bindings,
|
||||
[binding]: { ...bindings[binding], hotkey: bindingString },
|
||||
};
|
||||
|
||||
setSettings({
|
||||
hotkeys: {
|
||||
bindings: updatedBindings,
|
||||
globalMediaHotkeys,
|
||||
setSettings({
|
||||
hotkeys: {
|
||||
bindings: updatedBindings,
|
||||
globalMediaHotkeys,
|
||||
},
|
||||
});
|
||||
|
||||
ipc?.send('set-global-shortcuts', updatedBindings);
|
||||
},
|
||||
});
|
||||
20,
|
||||
);
|
||||
|
||||
ipc?.send('set-global-shortcuts', updatedBindings);
|
||||
},
|
||||
20,
|
||||
);
|
||||
const handleSetHotkey = useCallback(debouncedSetHotkey, [
|
||||
bindings,
|
||||
globalMediaHotkeys,
|
||||
setSettings,
|
||||
debouncedSetHotkey,
|
||||
]);
|
||||
|
||||
const handleSetHotkey = useCallback(debouncedSetHotkey, [
|
||||
bindings,
|
||||
globalMediaHotkeys,
|
||||
setSettings,
|
||||
debouncedSetHotkey,
|
||||
]);
|
||||
const handleSetGlobalHotkey = useCallback(
|
||||
(binding: BindingActions, e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedBindings = {
|
||||
...bindings,
|
||||
[binding]: { ...bindings[binding], isGlobal: e.currentTarget.checked },
|
||||
};
|
||||
|
||||
const handleSetGlobalHotkey = useCallback(
|
||||
(binding: BindingActions, e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedBindings = {
|
||||
...bindings,
|
||||
[binding]: { ...bindings[binding], isGlobal: e.currentTarget.checked },
|
||||
};
|
||||
setSettings({
|
||||
hotkeys: {
|
||||
bindings: updatedBindings,
|
||||
globalMediaHotkeys,
|
||||
},
|
||||
});
|
||||
|
||||
setSettings({
|
||||
hotkeys: {
|
||||
bindings: updatedBindings,
|
||||
globalMediaHotkeys,
|
||||
ipc?.send('set-global-shortcuts', updatedBindings);
|
||||
},
|
||||
});
|
||||
[bindings, globalMediaHotkeys, setSettings],
|
||||
);
|
||||
|
||||
ipc?.send('set-global-shortcuts', updatedBindings);
|
||||
},
|
||||
[bindings, globalMediaHotkeys, setSettings],
|
||||
);
|
||||
const handleClearHotkey = useCallback(
|
||||
(binding: BindingActions) => {
|
||||
const updatedBindings = {
|
||||
...bindings,
|
||||
[binding]: { ...bindings[binding], hotkey: '', isGlobal: false },
|
||||
};
|
||||
|
||||
const handleClearHotkey = useCallback(
|
||||
(binding: BindingActions) => {
|
||||
const updatedBindings = {
|
||||
...bindings,
|
||||
[binding]: { ...bindings[binding], hotkey: '', isGlobal: false },
|
||||
};
|
||||
setSettings({
|
||||
hotkeys: {
|
||||
bindings: updatedBindings,
|
||||
globalMediaHotkeys,
|
||||
},
|
||||
});
|
||||
|
||||
setSettings({
|
||||
hotkeys: {
|
||||
bindings: updatedBindings,
|
||||
globalMediaHotkeys,
|
||||
ipc?.send('set-global-shortcuts', updatedBindings);
|
||||
},
|
||||
});
|
||||
[bindings, globalMediaHotkeys, setSettings],
|
||||
);
|
||||
|
||||
ipc?.send('set-global-shortcuts', updatedBindings);
|
||||
},
|
||||
[bindings, globalMediaHotkeys, setSettings],
|
||||
);
|
||||
const duplicateHotkeyMap = useMemo(() => {
|
||||
const countPerHotkey = Object.values(bindings).reduce((acc, key) => {
|
||||
const hotkey = key.hotkey;
|
||||
if (!hotkey) return acc;
|
||||
|
||||
const duplicateHotkeyMap = useMemo(() => {
|
||||
const countPerHotkey = Object.values(bindings).reduce((acc, key) => {
|
||||
const hotkey = key.hotkey;
|
||||
if (!hotkey) return acc;
|
||||
if (acc[hotkey]) {
|
||||
acc[hotkey] += 1;
|
||||
} else {
|
||||
acc[hotkey] = 1;
|
||||
}
|
||||
|
||||
if (acc[hotkey]) {
|
||||
acc[hotkey] += 1;
|
||||
} else {
|
||||
acc[hotkey] = 1;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
const duplicateKeys = Object.keys(countPerHotkey).filter((key) => countPerHotkey[key] > 1);
|
||||
|
||||
const duplicateKeys = Object.keys(countPerHotkey).filter((key) => countPerHotkey[key] > 1);
|
||||
return duplicateKeys;
|
||||
}, [bindings]);
|
||||
|
||||
return duplicateKeys;
|
||||
}, [bindings]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsOptions
|
||||
control={<></>}
|
||||
description="Configure application hotkeys. Toggle the checkbox to set as a global hotkey (desktop only)"
|
||||
title="Application hotkeys"
|
||||
/>
|
||||
<HotkeysContainer>
|
||||
{Object.keys(bindings)
|
||||
.filter((binding) => BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP])
|
||||
.map((binding) => (
|
||||
<Group
|
||||
key={`hotkey-${binding}`}
|
||||
noWrap
|
||||
>
|
||||
<TextInput
|
||||
readOnly
|
||||
style={{ userSelect: 'none' }}
|
||||
value={BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]}
|
||||
/>
|
||||
<TextInput
|
||||
readOnly
|
||||
icon={<RiKeyboardBoxLine />}
|
||||
id={`hotkey-${binding}`}
|
||||
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}
|
||||
onBlur={() => setSelected(null)}
|
||||
onChange={() => {}}
|
||||
onKeyDownCapture={(e) => {
|
||||
if (selected !== (binding as BindingActions)) return;
|
||||
handleSetHotkey(binding as BindingActions, e);
|
||||
}}
|
||||
/>
|
||||
{isElectron() && (
|
||||
<Checkbox
|
||||
checked={bindings[binding as keyof typeof BINDINGS_MAP].isGlobal}
|
||||
disabled={bindings[binding as keyof typeof BINDINGS_MAP].hotkey === ''}
|
||||
size="xl"
|
||||
style={{
|
||||
opacity: bindings[binding as keyof typeof BINDINGS_MAP].allowGlobal ? 1 : 0,
|
||||
}}
|
||||
onChange={(e) => handleSetGlobalHotkey(binding as BindingActions, e)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="default"
|
||||
w={100}
|
||||
onClick={() => {
|
||||
setSelected(binding as BindingActions);
|
||||
document.getElementById(`hotkey-${binding}`)?.focus();
|
||||
}}
|
||||
>
|
||||
<RiEditLine />
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => handleClearHotkey(binding as BindingActions)}
|
||||
>
|
||||
<RiDeleteBinLine />
|
||||
</Button>
|
||||
</Group>
|
||||
))}
|
||||
</HotkeysContainer>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<SettingsOptions
|
||||
control={<></>}
|
||||
description="Configure application hotkeys. Toggle the checkbox to set as a global hotkey (desktop only)"
|
||||
title="Application hotkeys"
|
||||
/>
|
||||
<HotkeysContainer>
|
||||
{Object.keys(bindings)
|
||||
.filter((binding) => BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP])
|
||||
.map((binding) => (
|
||||
<Group
|
||||
key={`hotkey-${binding}`}
|
||||
noWrap
|
||||
>
|
||||
<TextInput
|
||||
readOnly
|
||||
style={{ userSelect: 'none' }}
|
||||
value={BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]}
|
||||
/>
|
||||
<TextInput
|
||||
readOnly
|
||||
icon={<RiKeyboardBoxLine />}
|
||||
id={`hotkey-${binding}`}
|
||||
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}
|
||||
onBlur={() => setSelected(null)}
|
||||
onChange={() => {}}
|
||||
onKeyDownCapture={(e) => {
|
||||
if (selected !== (binding as BindingActions)) return;
|
||||
handleSetHotkey(binding as BindingActions, e);
|
||||
}}
|
||||
/>
|
||||
{isElectron() && (
|
||||
<Checkbox
|
||||
checked={
|
||||
bindings[binding as keyof typeof BINDINGS_MAP].isGlobal
|
||||
}
|
||||
disabled={
|
||||
bindings[binding as keyof typeof BINDINGS_MAP].hotkey === ''
|
||||
}
|
||||
size="xl"
|
||||
style={{
|
||||
opacity: bindings[binding as keyof typeof BINDINGS_MAP]
|
||||
.allowGlobal
|
||||
? 1
|
||||
: 0,
|
||||
}}
|
||||
onChange={(e) =>
|
||||
handleSetGlobalHotkey(binding as BindingActions, e)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="default"
|
||||
w={100}
|
||||
onClick={() => {
|
||||
setSelected(binding as BindingActions);
|
||||
document.getElementById(`hotkey-${binding}`)?.focus();
|
||||
}}
|
||||
>
|
||||
<RiEditLine />
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => handleClearHotkey(binding as BindingActions)}
|
||||
>
|
||||
<RiDeleteBinLine />
|
||||
</Button>
|
||||
</Group>
|
||||
))}
|
||||
</HotkeysContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import { WindowHotkeySettings } from './window-hotkey-settings';
|
|||
import { HotkeyManagerSettings } from '/@/renderer/features/settings/components/hotkeys/hotkey-manager-settings';
|
||||
|
||||
export const HotkeysTab = () => {
|
||||
return (
|
||||
<Stack spacing="md">
|
||||
<WindowHotkeySettings />
|
||||
<Divider />
|
||||
<HotkeyManagerSettings />
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack spacing="md">
|
||||
<WindowHotkeySettings />
|
||||
<Divider />
|
||||
<HotkeyManagerSettings />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,39 +6,39 @@ import { useHotkeySettings, useSettingsStoreActions } from '/@/renderer/store';
|
|||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
|
||||
export const WindowHotkeySettings = () => {
|
||||
const settings = useHotkeySettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const settings = useHotkeySettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const options: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Toggle global media hotkeys"
|
||||
defaultChecked={settings.globalMediaHotkeys}
|
||||
disabled={!isElectron()}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
hotkeys: {
|
||||
...settings,
|
||||
globalMediaHotkeys: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
localSettings.set('global_media_hotkeys', e.currentTarget.checked);
|
||||
const options: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Toggle global media hotkeys"
|
||||
defaultChecked={settings.globalMediaHotkeys}
|
||||
disabled={!isElectron()}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
hotkeys: {
|
||||
...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',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Global media hotkeys',
|
||||
},
|
||||
];
|
||||
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',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Global media hotkeys',
|
||||
},
|
||||
];
|
||||
|
||||
return <SettingsSection options={options} />;
|
||||
return <SettingsSection options={options} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { SelectItem } from '@mantine/core';
|
|||
import isElectron from 'is-electron';
|
||||
import { Select, Slider, toast } from '/@/renderer/components';
|
||||
import {
|
||||
SettingsSection,
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
SettingOption,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { useCurrentStatus, usePlayerStore } from '/@/renderer/store';
|
||||
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
|
|
@ -13,146 +13,152 @@ import { PlaybackType, PlayerStatus, PlaybackStyle, CrossfadeStyle } from '/@/re
|
|||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
|
||||
const getAudioDevice = async () => {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return (devices || []).filter((dev: MediaDeviceInfo) => dev.kind === 'audiooutput');
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
return (devices || []).filter((dev: MediaDeviceInfo) => dev.kind === 'audiooutput');
|
||||
};
|
||||
|
||||
export const AudioSettings = () => {
|
||||
const settings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const status = useCurrentStatus();
|
||||
const settings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const status = useCurrentStatus();
|
||||
|
||||
const [audioDevices, setAudioDevices] = useState<SelectItem[]>([]);
|
||||
const [audioDevices, setAudioDevices] = useState<SelectItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const getAudioDevices = () => {
|
||||
getAudioDevice()
|
||||
.then((dev) => setAudioDevices(dev.map((d) => ({ label: d.label, value: d.deviceId }))))
|
||||
.catch(() => toast.error({ message: 'Error fetching audio devices' }));
|
||||
};
|
||||
useEffect(() => {
|
||||
const getAudioDevices = () => {
|
||||
getAudioDevice()
|
||||
.then((dev) =>
|
||||
setAudioDevices(dev.map((d) => ({ label: d.label, value: d.deviceId }))),
|
||||
)
|
||||
.catch(() => toast.error({ message: 'Error fetching audio devices' }));
|
||||
};
|
||||
|
||||
if (settings.type === PlaybackType.WEB) {
|
||||
getAudioDevices();
|
||||
}
|
||||
}, [settings.type]);
|
||||
if (settings.type === PlaybackType.WEB) {
|
||||
getAudioDevices();
|
||||
}
|
||||
}, [settings.type]);
|
||||
|
||||
const audioOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{
|
||||
disabled: !isElectron(),
|
||||
label: 'MPV',
|
||||
value: PlaybackType.LOCAL,
|
||||
},
|
||||
{ label: 'Web', value: PlaybackType.WEB },
|
||||
]}
|
||||
defaultValue={settings.type}
|
||||
disabled={status === PlayerStatus.PLAYING}
|
||||
onChange={(e) => {
|
||||
setSettings({ playback: { ...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: (
|
||||
<Select
|
||||
clearable
|
||||
data={audioDevices}
|
||||
defaultValue={settings.audioDeviceId}
|
||||
disabled={settings.type !== PlaybackType.WEB}
|
||||
onChange={(e) => setSettings({ playback: { ...settings, audioDeviceId: e } })}
|
||||
/>
|
||||
),
|
||||
description: 'The audio device to use for playback (web player only)',
|
||||
isHidden: !isElectron() || settings.type !== PlaybackType.WEB,
|
||||
title: 'Audio device',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
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({ playback: { ...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({ playback: { ...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({
|
||||
playback: { ...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',
|
||||
},
|
||||
];
|
||||
const audioOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{
|
||||
disabled: !isElectron(),
|
||||
label: 'MPV',
|
||||
value: PlaybackType.LOCAL,
|
||||
},
|
||||
{ label: 'Web', value: PlaybackType.WEB },
|
||||
]}
|
||||
defaultValue={settings.type}
|
||||
disabled={status === PlayerStatus.PLAYING}
|
||||
onChange={(e) => {
|
||||
setSettings({ playback: { ...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: (
|
||||
<Select
|
||||
clearable
|
||||
data={audioDevices}
|
||||
defaultValue={settings.audioDeviceId}
|
||||
disabled={settings.type !== PlaybackType.WEB}
|
||||
onChange={(e) => setSettings({ playback: { ...settings, audioDeviceId: e } })}
|
||||
/>
|
||||
),
|
||||
description: 'The audio device to use for playback (web player only)',
|
||||
isHidden: !isElectron() || settings.type !== PlaybackType.WEB,
|
||||
title: 'Audio device',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
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({ playback: { ...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({ playback: { ...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({
|
||||
playback: { ...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',
|
||||
},
|
||||
];
|
||||
|
||||
return <SettingsSection options={audioOptions} />;
|
||||
return <SettingsSection options={audioOptions} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { useLyricsSettings, useSettingsStoreActions } from '/@/renderer/store';
|
||||
import { MultiSelect, MultiSelectProps, NumberInput, Switch } from '/@/renderer/components';
|
||||
|
|
@ -11,99 +11,99 @@ import { LyricSource } from '/@/renderer/api/types';
|
|||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
|
||||
const WorkingButtonSelect = styled(MultiSelect)<MultiSelectProps>`
|
||||
& button {
|
||||
padding: 0;
|
||||
}
|
||||
& button {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const LyricSettings = () => {
|
||||
const settings = useLyricsSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const settings = useLyricsSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const lyricOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Follow lyrics"
|
||||
defaultChecked={settings.follow}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
lyrics: {
|
||||
...settings,
|
||||
follow: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enable or disable following of current lyric',
|
||||
title: 'Follow current lyric',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Enable fetching lyrics"
|
||||
defaultChecked={settings.fetch}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
lyrics: {
|
||||
...settings,
|
||||
fetch: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enable or disable fetching lyrics for the current song',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Fetch lyrics from the internet',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<WorkingButtonSelect
|
||||
clearable
|
||||
aria-label="Lyric providers"
|
||||
data={Object.values(LyricSource)}
|
||||
defaultValue={settings.sources}
|
||||
width={300}
|
||||
onChange={(e: LyricSource[]) => {
|
||||
localSettings?.set('lyrics', e);
|
||||
setSettings({
|
||||
lyrics: {
|
||||
...settings,
|
||||
sources: e,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'List of lyric fetchers (in order of preference)',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Providers to fetch music',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<NumberInput
|
||||
defaultValue={settings.delayMs}
|
||||
step={10}
|
||||
width={100}
|
||||
onBlur={(e) => {
|
||||
const value = Number(e.currentTarget.value);
|
||||
setSettings({
|
||||
lyrics: {
|
||||
...settings,
|
||||
delayMs: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Lyric offset (in milliseconds). Positive values mean that lyrics are shown later, and negative mean that lyrics are shown earlier',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Lyric offset',
|
||||
},
|
||||
];
|
||||
const lyricOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Follow lyrics"
|
||||
defaultChecked={settings.follow}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
lyrics: {
|
||||
...settings,
|
||||
follow: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enable or disable following of current lyric',
|
||||
title: 'Follow current lyric',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Enable fetching lyrics"
|
||||
defaultChecked={settings.fetch}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
lyrics: {
|
||||
...settings,
|
||||
fetch: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enable or disable fetching lyrics for the current song',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Fetch lyrics from the internet',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<WorkingButtonSelect
|
||||
clearable
|
||||
aria-label="Lyric providers"
|
||||
data={Object.values(LyricSource)}
|
||||
defaultValue={settings.sources}
|
||||
width={300}
|
||||
onChange={(e: LyricSource[]) => {
|
||||
localSettings?.set('lyrics', e);
|
||||
setSettings({
|
||||
lyrics: {
|
||||
...settings,
|
||||
sources: e,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'List of lyric fetchers (in order of preference)',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Providers to fetch music',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<NumberInput
|
||||
defaultValue={settings.delayMs}
|
||||
step={10}
|
||||
width={100}
|
||||
onBlur={(e) => {
|
||||
const value = Number(e.currentTarget.value);
|
||||
setSettings({
|
||||
lyrics: {
|
||||
...settings,
|
||||
delayMs: value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Lyric offset (in milliseconds). Positive values mean that lyrics are shown later, and negative mean that lyrics are shown earlier',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Lyric offset',
|
||||
},
|
||||
];
|
||||
|
||||
return <SettingsSection options={lyricOptions} />;
|
||||
return <SettingsSection options={lyricOptions} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { Divider, Stack } from '@mantine/core';
|
|||
import isElectron from 'is-electron';
|
||||
import { FileInput, Textarea, Text, Select, NumberInput, Switch } from '/@/renderer/components';
|
||||
import {
|
||||
SettingsSection,
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
SettingOption,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import {
|
||||
SettingsState,
|
||||
usePlaybackSettings,
|
||||
useSettingsStoreActions,
|
||||
SettingsState,
|
||||
usePlaybackSettings,
|
||||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { PlaybackType } from '/@/renderer/types';
|
||||
|
||||
|
|
@ -17,267 +17,275 @@ const localSettings = isElectron() ? window.electron.localSettings : null;
|
|||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
|
||||
export const getMpvSetting = (
|
||||
key: keyof SettingsState['playback']['mpvProperties'],
|
||||
value: any,
|
||||
key: keyof SettingsState['playback']['mpvProperties'],
|
||||
value: any,
|
||||
) => {
|
||||
switch (key) {
|
||||
case 'audioExclusiveMode':
|
||||
return { 'audio-exclusive': value || 'no' };
|
||||
case 'audioSampleRateHz':
|
||||
return { 'audio-samplerate': value };
|
||||
case 'gaplessAudio':
|
||||
return { 'gapless-audio': value || 'weak' };
|
||||
case 'replayGainMode':
|
||||
return { replaygain: value || 'no' };
|
||||
case 'replayGainClip':
|
||||
return { 'replaygain-clip': value || 'no' };
|
||||
case 'replayGainFallbackDB':
|
||||
return { 'replaygain-fallback': value };
|
||||
case 'replayGainPreampDB':
|
||||
return { 'replaygain-preamp': value || 0 };
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
switch (key) {
|
||||
case 'audioExclusiveMode':
|
||||
return { 'audio-exclusive': value || 'no' };
|
||||
case 'audioSampleRateHz':
|
||||
return { 'audio-samplerate': value };
|
||||
case 'gaplessAudio':
|
||||
return { 'gapless-audio': value || 'weak' };
|
||||
case 'replayGainMode':
|
||||
return { replaygain: value || 'no' };
|
||||
case 'replayGainClip':
|
||||
return { 'replaygain-clip': value || 'no' };
|
||||
case 'replayGainFallbackDB':
|
||||
return { 'replaygain-fallback': value };
|
||||
case 'replayGainPreampDB':
|
||||
return { 'replaygain-preamp': value || 0 };
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
};
|
||||
|
||||
export const getMpvProperties = (settings: SettingsState['playback']['mpvProperties']) => {
|
||||
const properties: Record<string, any> = {
|
||||
'audio-exclusive': settings.audioExclusiveMode || 'no',
|
||||
'audio-samplerate': settings.audioSampleRateHz === 0 ? undefined : settings.audioSampleRateHz,
|
||||
'gapless-audio': settings.gaplessAudio || 'weak',
|
||||
replaygain: settings.replayGainMode || 'no',
|
||||
'replaygain-clip': settings.replayGainClip || 'no',
|
||||
'replaygain-fallback': settings.replayGainFallbackDB,
|
||||
'replaygain-preamp': settings.replayGainPreampDB || 0,
|
||||
};
|
||||
const properties: Record<string, any> = {
|
||||
'audio-exclusive': settings.audioExclusiveMode || 'no',
|
||||
'audio-samplerate':
|
||||
settings.audioSampleRateHz === 0 ? undefined : settings.audioSampleRateHz,
|
||||
'gapless-audio': settings.gaplessAudio || 'weak',
|
||||
replaygain: settings.replayGainMode || 'no',
|
||||
'replaygain-clip': settings.replayGainClip || 'no',
|
||||
'replaygain-fallback': settings.replayGainFallbackDB,
|
||||
'replaygain-preamp': settings.replayGainPreampDB || 0,
|
||||
};
|
||||
|
||||
Object.keys(properties).forEach((key) =>
|
||||
properties[key] === undefined ? delete properties[key] : {},
|
||||
);
|
||||
Object.keys(properties).forEach((key) =>
|
||||
properties[key] === undefined ? delete properties[key] : {},
|
||||
);
|
||||
|
||||
return properties;
|
||||
return properties;
|
||||
};
|
||||
|
||||
export const MpvSettings = () => {
|
||||
const settings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const settings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const [mpvPath, setMpvPath] = useState('');
|
||||
const [mpvPath, setMpvPath] = 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 handleSetMpvPath = (e: File) => {
|
||||
localSettings.set('mpv_path', e.path);
|
||||
};
|
||||
|
||||
getMpvPath();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const getMpvPath = async () => {
|
||||
if (!isElectron()) return setMpvPath('');
|
||||
const mpvPath = (await localSettings.get('mpv_path')) as string;
|
||||
return setMpvPath(mpvPath);
|
||||
};
|
||||
|
||||
const handleSetMpvProperty = (
|
||||
setting: keyof SettingsState['playback']['mpvProperties'],
|
||||
value: any,
|
||||
) => {
|
||||
setSettings({
|
||||
playback: {
|
||||
...settings,
|
||||
mpvProperties: {
|
||||
...settings.mpvProperties,
|
||||
[setting]: value,
|
||||
getMpvPath();
|
||||
}, []);
|
||||
|
||||
const handleSetMpvProperty = (
|
||||
setting: keyof SettingsState['playback']['mpvProperties'],
|
||||
value: any,
|
||||
) => {
|
||||
setSettings({
|
||||
playback: {
|
||||
...settings,
|
||||
mpvProperties: {
|
||||
...settings.mpvProperties,
|
||||
[setting]: value,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mpvSetting = getMpvSetting(setting, value);
|
||||
|
||||
mpvPlayer?.setProperties(mpvSetting);
|
||||
};
|
||||
|
||||
const handleSetExtraParameters = (data: string[]) => {
|
||||
setSettings({
|
||||
playback: {
|
||||
...settings,
|
||||
mpvExtraParameters: data,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const options: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<FileInput
|
||||
placeholder={mpvPath}
|
||||
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={settings.mpvExtraParameters.join('\n')}
|
||||
minRows={4}
|
||||
placeholder={
|
||||
'(Add one per line):\n--gapless-audio=weak\n--prefetch-playlist=yes'
|
||||
}
|
||||
width={225}
|
||||
onBlur={(e) => {
|
||||
handleSetExtraParameters(e.currentTarget.value.split('\n'));
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
),
|
||||
description: (
|
||||
<Stack spacing={0}>
|
||||
<Text
|
||||
$noSelect
|
||||
$secondary
|
||||
size="sm"
|
||||
>
|
||||
Options to pass to the player
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
<a
|
||||
href="https://mpv.io/manual/stable/#audio"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
https://mpv.io/manual/stable/#audio
|
||||
</a>
|
||||
</Text>
|
||||
</Stack>
|
||||
),
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
note: 'Restart required',
|
||||
title: 'MPV parameters',
|
||||
},
|
||||
];
|
||||
|
||||
const mpvSetting = getMpvSetting(setting, value);
|
||||
const generalOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{ label: 'No', value: 'no' },
|
||||
{ label: 'Yes', value: 'yes' },
|
||||
{ label: 'Weak (recommended)', value: 'weak' },
|
||||
]}
|
||||
defaultValue={settings.mpvProperties.gaplessAudio}
|
||||
onChange={(e) => handleSetMpvProperty('gaplessAudio', e)}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Try to play consecutive audio files with no silence or disruption at the point of file change (--gapless-audio)',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
title: 'Gapless audio',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<NumberInput
|
||||
defaultValue={settings.mpvProperties.audioSampleRateHz}
|
||||
width={100}
|
||||
onBlur={(e) => {
|
||||
const value = Number(e.currentTarget.value);
|
||||
handleSetMpvProperty('audioSampleRateHz', value > 0 ? value : undefined);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Select the output sample rate to be used if the sample frequency selected is different from that of the current media',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
title: 'Sample rate',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
defaultChecked={settings.mpvProperties.audioExclusiveMode === 'yes'}
|
||||
onChange={(e) =>
|
||||
handleSetMpvProperty(
|
||||
'audioExclusiveMode',
|
||||
e.currentTarget.checked ? 'yes' : 'no',
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
|
||||
mpvPlayer?.setProperties(mpvSetting);
|
||||
};
|
||||
description:
|
||||
'Enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio (--audio-exclusive)',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
title: 'Audio exclusive mode',
|
||||
},
|
||||
];
|
||||
|
||||
const handleSetExtraParameters = (data: string[]) => {
|
||||
setSettings({
|
||||
playback: {
|
||||
...settings,
|
||||
mpvExtraParameters: data,
|
||||
},
|
||||
});
|
||||
};
|
||||
const replayGainOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{ label: 'None', value: 'no' },
|
||||
{ label: 'Track', value: 'track' },
|
||||
{ label: 'Album', value: 'album' },
|
||||
]}
|
||||
defaultValue={settings.mpvProperties.replayGainMode}
|
||||
onChange={(e) => handleSetMpvProperty('replayGainMode', e)}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Adjust volume gain according to replaygain values stored in the file metadata (--replaygain)',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
note: 'Restart required',
|
||||
title: 'ReplayGain mode',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<NumberInput
|
||||
defaultValue={settings.mpvProperties.replayGainPreampDB}
|
||||
width={75}
|
||||
onChange={(e) => handleSetMpvProperty('replayGainPreampDB', e)}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Pre-amplification gain in dB to apply to the selected replaygain gain (--replaygain-preamp)',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
title: 'ReplayGain preamp (dB)',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
defaultChecked={settings.mpvProperties.replayGainClip}
|
||||
onChange={(e) =>
|
||||
handleSetMpvProperty('replayGainClip', e.currentTarget.checked)
|
||||
}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Prevent clipping caused by replaygain by automatically lowering the gain (--replaygain-clip)',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
title: 'ReplayGain clipping',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<NumberInput
|
||||
defaultValue={settings.mpvProperties.replayGainFallbackDB}
|
||||
width={75}
|
||||
onBlur={(e) => handleSetMpvProperty('replayGainFallbackDB', e)}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Gain in dB to apply if the file has no replay gain tags. This option is always applied if the replaygain logic is somehow inactive. If this is applied, no other replaygain options are applied',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
title: 'ReplayGain fallback (dB)',
|
||||
},
|
||||
];
|
||||
|
||||
const options: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<FileInput
|
||||
placeholder={mpvPath}
|
||||
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={settings.mpvExtraParameters.join('\n')}
|
||||
minRows={4}
|
||||
placeholder={'(Add one per line):\n--gapless-audio=weak\n--prefetch-playlist=yes'}
|
||||
width={225}
|
||||
onBlur={(e) => {
|
||||
handleSetExtraParameters(e.currentTarget.value.split('\n'));
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
),
|
||||
description: (
|
||||
<Stack spacing={0}>
|
||||
<Text
|
||||
$noSelect
|
||||
$secondary
|
||||
size="sm"
|
||||
>
|
||||
Options to pass to the player
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
<a
|
||||
href="https://mpv.io/manual/stable/#audio"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
https://mpv.io/manual/stable/#audio
|
||||
</a>
|
||||
</Text>
|
||||
</Stack>
|
||||
),
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
note: 'Restart required',
|
||||
title: 'MPV parameters',
|
||||
},
|
||||
];
|
||||
|
||||
const generalOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{ label: 'No', value: 'no' },
|
||||
{ label: 'Yes', value: 'yes' },
|
||||
{ label: 'Weak (recommended)', value: 'weak' },
|
||||
]}
|
||||
defaultValue={settings.mpvProperties.gaplessAudio}
|
||||
onChange={(e) => handleSetMpvProperty('gaplessAudio', e)}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Try to play consecutive audio files with no silence or disruption at the point of file change (--gapless-audio)',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
title: 'Gapless audio',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<NumberInput
|
||||
defaultValue={settings.mpvProperties.audioSampleRateHz}
|
||||
width={100}
|
||||
onBlur={(e) => {
|
||||
const value = Number(e.currentTarget.value);
|
||||
handleSetMpvProperty('audioSampleRateHz', value > 0 ? value : undefined);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Select the output sample rate to be used if the sample frequency selected is different from that of the current media',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
title: 'Sample rate',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
defaultChecked={settings.mpvProperties.audioExclusiveMode === 'yes'}
|
||||
onChange={(e) =>
|
||||
handleSetMpvProperty('audioExclusiveMode', e.currentTarget.checked ? 'yes' : 'no')
|
||||
}
|
||||
/>
|
||||
),
|
||||
|
||||
description:
|
||||
'Enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio (--audio-exclusive)',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
title: 'Audio exclusive mode',
|
||||
},
|
||||
];
|
||||
|
||||
const replayGainOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{ label: 'None', value: 'no' },
|
||||
{ label: 'Track', value: 'track' },
|
||||
{ label: 'Album', value: 'album' },
|
||||
]}
|
||||
defaultValue={settings.mpvProperties.replayGainMode}
|
||||
onChange={(e) => handleSetMpvProperty('replayGainMode', e)}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Adjust volume gain according to replaygain values stored in the file metadata (--replaygain)',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
note: 'Restart required',
|
||||
title: 'ReplayGain mode',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<NumberInput
|
||||
defaultValue={settings.mpvProperties.replayGainPreampDB}
|
||||
width={75}
|
||||
onChange={(e) => handleSetMpvProperty('replayGainPreampDB', e)}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Pre-amplification gain in dB to apply to the selected replaygain gain (--replaygain-preamp)',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
title: 'ReplayGain preamp (dB)',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
defaultChecked={settings.mpvProperties.replayGainClip}
|
||||
onChange={(e) => handleSetMpvProperty('replayGainClip', e.currentTarget.checked)}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Prevent clipping caused by replaygain by automatically lowering the gain (--replaygain-clip)',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
title: 'ReplayGain clipping',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<NumberInput
|
||||
defaultValue={settings.mpvProperties.replayGainFallbackDB}
|
||||
width={75}
|
||||
onBlur={(e) => handleSetMpvProperty('replayGainFallbackDB', e)}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Gain in dB to apply if the file has no replay gain tags. This option is always applied if the replaygain logic is somehow inactive. If this is applied, no other replaygain options are applied',
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
title: 'ReplayGain fallback (dB)',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSection options={options} />
|
||||
<Divider />
|
||||
<SettingsSection options={generalOptions} />
|
||||
<Divider />
|
||||
<SettingsSection options={replayGainOptions} />
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<SettingsSection options={options} />
|
||||
<Divider />
|
||||
<SettingsSection options={generalOptions} />
|
||||
<Divider />
|
||||
<SettingsSection options={replayGainOptions} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,20 +6,20 @@ import isElectron from 'is-electron';
|
|||
import { LyricSettings } from '/@/renderer/features/settings/components/playback/lyric-settings';
|
||||
|
||||
const MpvSettings = lazy(() =>
|
||||
import('/@/renderer/features/settings/components/playback/mpv-settings').then((module) => {
|
||||
return { default: module.MpvSettings };
|
||||
}),
|
||||
import('/@/renderer/features/settings/components/playback/mpv-settings').then((module) => {
|
||||
return { default: module.MpvSettings };
|
||||
}),
|
||||
);
|
||||
|
||||
export const PlaybackTab = () => {
|
||||
return (
|
||||
<Stack spacing="md">
|
||||
<AudioSettings />
|
||||
<Suspense fallback={<></>}>{isElectron() && <MpvSettings />}</Suspense>
|
||||
<Divider />
|
||||
<ScrobbleSettings />
|
||||
<Divider />
|
||||
<LyricSettings />
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack spacing="md">
|
||||
<AudioSettings />
|
||||
<Suspense fallback={<></>}>{isElectron() && <MpvSettings />}</Suspense>
|
||||
<Divider />
|
||||
<ScrobbleSettings />
|
||||
<Divider />
|
||||
<LyricSettings />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,96 +4,97 @@ import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/
|
|||
import { SettingOption, SettingsSection } from '../settings-section';
|
||||
|
||||
export const ScrobbleSettings = () => {
|
||||
const settings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const settings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const scrobbleOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Toggle scrobble"
|
||||
defaultChecked={settings.scrobble.enabled}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
playback: {
|
||||
...settings,
|
||||
scrobble: {
|
||||
...settings.scrobble,
|
||||
enabled: e.currentTarget.checked,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enable or disable scrobbling to your media server',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Scrobble',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Slider
|
||||
aria-label="Scrobble percentage"
|
||||
defaultValue={settings.scrobble.scrobbleAtPercentage}
|
||||
label={`${settings.scrobble.scrobbleAtPercentage}%`}
|
||||
max={90}
|
||||
min={25}
|
||||
w={100}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
playback: {
|
||||
...settings,
|
||||
scrobble: {
|
||||
...settings.scrobble,
|
||||
scrobbleAtPercentage: e,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'The percentage of the song that must be played before submitting a scrobble',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Minimum scrobble percentage*',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<NumberInput
|
||||
aria-label="Scrobble duration in seconds"
|
||||
defaultValue={settings.scrobble.scrobbleAtDuration}
|
||||
max={1200}
|
||||
min={0}
|
||||
width={75}
|
||||
onChange={(e) => {
|
||||
if (e === '') return;
|
||||
setSettings({
|
||||
playback: {
|
||||
...settings,
|
||||
scrobble: {
|
||||
...settings.scrobble,
|
||||
scrobbleAtDuration: e,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'The duration in seconds of a song that must be played before submitting a scrobble',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Minimum scrobble duration (seconds)*',
|
||||
},
|
||||
];
|
||||
const scrobbleOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Toggle scrobble"
|
||||
defaultChecked={settings.scrobble.enabled}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
playback: {
|
||||
...settings,
|
||||
scrobble: {
|
||||
...settings.scrobble,
|
||||
enabled: e.currentTarget.checked,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enable or disable scrobbling to your media server',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Scrobble',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Slider
|
||||
aria-label="Scrobble percentage"
|
||||
defaultValue={settings.scrobble.scrobbleAtPercentage}
|
||||
label={`${settings.scrobble.scrobbleAtPercentage}%`}
|
||||
max={90}
|
||||
min={25}
|
||||
w={100}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
playback: {
|
||||
...settings,
|
||||
scrobble: {
|
||||
...settings.scrobble,
|
||||
scrobbleAtPercentage: e,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'The percentage of the song that must be played before submitting a scrobble',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Minimum scrobble percentage*',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<NumberInput
|
||||
aria-label="Scrobble duration in seconds"
|
||||
defaultValue={settings.scrobble.scrobbleAtDuration}
|
||||
max={1200}
|
||||
min={0}
|
||||
width={75}
|
||||
onChange={(e) => {
|
||||
if (e === '') return;
|
||||
setSettings({
|
||||
playback: {
|
||||
...settings,
|
||||
scrobble: {
|
||||
...settings.scrobble,
|
||||
scrobbleAtDuration: e,
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'The duration in seconds of a song that must be played before submitting a scrobble',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Minimum scrobble duration (seconds)*',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSection options={scrobbleOptions} />
|
||||
<Text
|
||||
$secondary
|
||||
size="sm"
|
||||
>
|
||||
*The scrobble will be submitted if one or more of the above conditions is met
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<SettingsSection options={scrobbleOptions} />
|
||||
<Text
|
||||
$secondary
|
||||
size="sm"
|
||||
>
|
||||
*The scrobble will be submitted if one or more of the above conditions is met
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,70 +5,70 @@ import isElectron from 'is-electron';
|
|||
import styled from 'styled-components';
|
||||
|
||||
const GeneralTab = lazy(() =>
|
||||
import('/@/renderer/features/settings/components/general/general-tab').then((module) => ({
|
||||
default: module.GeneralTab,
|
||||
})),
|
||||
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,
|
||||
})),
|
||||
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,
|
||||
})),
|
||||
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,
|
||||
})),
|
||||
import('/@/renderer/features/settings/components/hotkeys/hotkeys-tab').then((module) => ({
|
||||
default: module.HotkeysTab,
|
||||
})),
|
||||
);
|
||||
|
||||
const TabContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
overflow: scroll;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
overflow: scroll;
|
||||
`;
|
||||
|
||||
export const SettingsContent = () => {
|
||||
const currentTab = useSettingsStore((state) => state.tab);
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const currentTab = useSettingsStore((state) => state.tab);
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
return (
|
||||
<TabContainer>
|
||||
<Tabs
|
||||
keepMounted={false}
|
||||
orientation="horizontal"
|
||||
value={currentTab}
|
||||
variant="default"
|
||||
onTabChange={(e) => e && setSettings({ tab: e })}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="general">General</Tabs.Tab>
|
||||
<Tabs.Tab value="playback">Playback</Tabs.Tab>
|
||||
<Tabs.Tab value="hotkeys">Hotkeys</Tabs.Tab>
|
||||
{isElectron() && <Tabs.Tab value="window">Window</Tabs.Tab>}
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="general">
|
||||
<GeneralTab />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="playback">
|
||||
<PlaybackTab />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="hotkeys">
|
||||
<HotkeysTab />
|
||||
</Tabs.Panel>
|
||||
{isElectron() && (
|
||||
<Tabs.Panel value="window">
|
||||
<ApplicationTab />
|
||||
</Tabs.Panel>
|
||||
)}
|
||||
</Tabs>
|
||||
</TabContainer>
|
||||
);
|
||||
return (
|
||||
<TabContainer>
|
||||
<Tabs
|
||||
keepMounted={false}
|
||||
orientation="horizontal"
|
||||
value={currentTab}
|
||||
variant="default"
|
||||
onTabChange={(e) => e && setSettings({ tab: e })}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="general">General</Tabs.Tab>
|
||||
<Tabs.Tab value="playback">Playback</Tabs.Tab>
|
||||
<Tabs.Tab value="hotkeys">Hotkeys</Tabs.Tab>
|
||||
{isElectron() && <Tabs.Tab value="window">Window</Tabs.Tab>}
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="general">
|
||||
<GeneralTab />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="playback">
|
||||
<PlaybackTab />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="hotkeys">
|
||||
<HotkeysTab />
|
||||
</Tabs.Panel>
|
||||
{isElectron() && (
|
||||
<Tabs.Panel value="window">
|
||||
<ApplicationTab />
|
||||
</Tabs.Panel>
|
||||
)}
|
||||
</Tabs>
|
||||
</TabContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,43 +6,43 @@ import { LibraryHeaderBar } from '/@/renderer/features/shared';
|
|||
import { useSettingsStoreActions } from '../../../store/settings.store';
|
||||
|
||||
export const SettingsHeader = () => {
|
||||
const { reset } = useSettingsStoreActions();
|
||||
const { reset } = useSettingsStoreActions();
|
||||
|
||||
const handleResetToDefault = () => {
|
||||
reset();
|
||||
closeAllModals();
|
||||
};
|
||||
const handleResetToDefault = () => {
|
||||
reset();
|
||||
closeAllModals();
|
||||
};
|
||||
|
||||
const openResetConfirmModal = () => {
|
||||
openModal({
|
||||
children: <ConfirmModal onConfirm={handleResetToDefault}>Are you sure?</ConfirmModal>,
|
||||
title: 'Reset settings to default',
|
||||
});
|
||||
};
|
||||
const openResetConfirmModal = () => {
|
||||
openModal({
|
||||
children: <ConfirmModal onConfirm={handleResetToDefault}>Are you sure?</ConfirmModal>,
|
||||
title: 'Reset settings to default',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex>
|
||||
<PageHeader>
|
||||
<LibraryHeaderBar>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
>
|
||||
<Group noWrap>
|
||||
<RiSettings2Fill size="2rem" />
|
||||
<LibraryHeaderBar.Title>Settings</LibraryHeaderBar.Title>
|
||||
</Group>
|
||||
<Button
|
||||
compact
|
||||
variant="default"
|
||||
onClick={openResetConfirmModal}
|
||||
>
|
||||
Reset to default
|
||||
</Button>
|
||||
</Flex>
|
||||
</LibraryHeaderBar>
|
||||
</PageHeader>
|
||||
</Flex>
|
||||
);
|
||||
return (
|
||||
<Flex>
|
||||
<PageHeader>
|
||||
<LibraryHeaderBar>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
>
|
||||
<Group noWrap>
|
||||
<RiSettings2Fill size="2rem" />
|
||||
<LibraryHeaderBar.Title>Settings</LibraryHeaderBar.Title>
|
||||
</Group>
|
||||
<Button
|
||||
compact
|
||||
variant="default"
|
||||
onClick={openResetConfirmModal}
|
||||
>
|
||||
Reset to default
|
||||
</Button>
|
||||
</Flex>
|
||||
</LibraryHeaderBar>
|
||||
</PageHeader>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,65 +4,65 @@ 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;
|
||||
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="md"
|
||||
return (
|
||||
<>
|
||||
<Group
|
||||
noWrap
|
||||
position="apart"
|
||||
sx={{ alignItems: 'center' }}
|
||||
>
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
<Stack
|
||||
spacing="xs"
|
||||
sx={{
|
||||
alignSelf: 'flex-start',
|
||||
display: 'flex',
|
||||
maxWidth: '50%',
|
||||
}}
|
||||
>
|
||||
<Group>
|
||||
<Text
|
||||
$noSelect
|
||||
size="md"
|
||||
>
|
||||
{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,
|
||||
description: undefined,
|
||||
note: undefined,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,28 +1,28 @@
|
|||
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
|
||||
|
||||
export type SettingOption = {
|
||||
control: JSX.Element;
|
||||
description: string | JSX.Element;
|
||||
isHidden?: boolean;
|
||||
note?: string;
|
||||
title: string;
|
||||
control: JSX.Element;
|
||||
description: string | JSX.Element;
|
||||
isHidden?: boolean;
|
||||
note?: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
interface SettingsSectionProps {
|
||||
options: SettingOption[];
|
||||
options: SettingOption[];
|
||||
}
|
||||
|
||||
export const SettingsSection = ({ options }: SettingsSectionProps) => {
|
||||
return (
|
||||
<>
|
||||
{options
|
||||
.filter((o) => !o.isHidden)
|
||||
.map((option) => (
|
||||
<SettingsOptions
|
||||
key={`general-${option.title}`}
|
||||
{...option}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{options
|
||||
.filter((o) => !o.isHidden)
|
||||
.map((option) => (
|
||||
<SettingsOptions
|
||||
key={`general-${option.title}`}
|
||||
{...option}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,41 +1,41 @@
|
|||
import isElectron from 'is-electron';
|
||||
import { useWindowSettings, useSettingsStoreActions } from '../../../../store/settings.store';
|
||||
import {
|
||||
SettingsSection,
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
SettingOption,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { Switch } from '/@/renderer/components';
|
||||
|
||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
|
||||
export const UpdateSettings = () => {
|
||||
const settings = useWindowSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const settings = useWindowSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const updateOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Disable automatic updates"
|
||||
defaultChecked={settings.disableAutoUpdate}
|
||||
disabled={!isElectron()}
|
||||
onChange={(e) => {
|
||||
if (!e) return;
|
||||
localSettings?.set('disable_auto_updates', e.currentTarget.checked);
|
||||
setSettings({
|
||||
window: {
|
||||
...settings,
|
||||
disableAutoUpdate: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enabling this option will disable checking for new versions on startup',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Disable automatic updates',
|
||||
},
|
||||
];
|
||||
const updateOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Disable automatic updates"
|
||||
defaultChecked={settings.disableAutoUpdate}
|
||||
disabled={!isElectron()}
|
||||
onChange={(e) => {
|
||||
if (!e) return;
|
||||
localSettings?.set('disable_auto_updates', e.currentTarget.checked);
|
||||
setSettings({
|
||||
window: {
|
||||
...settings,
|
||||
disableAutoUpdate: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enabling this option will disable checking for new versions on startup',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Disable automatic updates',
|
||||
},
|
||||
];
|
||||
|
||||
return <SettingsSection options={updateOptions} />;
|
||||
return <SettingsSection options={updateOptions} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,114 +2,122 @@ import isElectron from 'is-electron';
|
|||
import { Platform } from '/@/renderer/types';
|
||||
import { useWindowSettings, useSettingsStoreActions } from '../../../../store/settings.store';
|
||||
import {
|
||||
SettingsSection,
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
SettingOption,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { Select, Switch, toast } from '/@/renderer/components';
|
||||
|
||||
const WINDOW_BAR_OPTIONS = [
|
||||
{ label: 'Web (hidden)', value: Platform.WEB },
|
||||
{ label: 'Windows', value: Platform.WINDOWS },
|
||||
{ label: 'macOS', value: Platform.MACOS },
|
||||
{ label: 'Native', value: Platform.LINUX },
|
||||
{ label: 'Web (hidden)', value: Platform.WEB },
|
||||
{ label: 'Windows', value: Platform.WINDOWS },
|
||||
{ label: 'macOS', value: Platform.MACOS },
|
||||
{ label: 'Native', value: Platform.LINUX },
|
||||
];
|
||||
|
||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
|
||||
export const WindowSettings = () => {
|
||||
const settings = useWindowSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const settings = useWindowSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const windowOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={WINDOW_BAR_OPTIONS}
|
||||
disabled={!isElectron()}
|
||||
value={settings.windowBarStyle}
|
||||
onChange={(e) => {
|
||||
if (!e) return;
|
||||
const windowOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={WINDOW_BAR_OPTIONS}
|
||||
disabled={!isElectron()}
|
||||
value={settings.windowBarStyle}
|
||||
onChange={(e) => {
|
||||
if (!e) return;
|
||||
|
||||
// Platform.LINUX is used as the native frame option regardless of the actual platform
|
||||
const hasFrame = localSettings?.get('window_has_frame') as boolean | undefined;
|
||||
const isSwitchingToFrame = !hasFrame && e === Platform.LINUX;
|
||||
const isSwitchingToNoFrame = hasFrame && e !== Platform.LINUX;
|
||||
// Platform.LINUX is used as the native frame option regardless of the actual platform
|
||||
const hasFrame = localSettings?.get('window_has_frame') as
|
||||
| boolean
|
||||
| undefined;
|
||||
const isSwitchingToFrame = !hasFrame && e === Platform.LINUX;
|
||||
const isSwitchingToNoFrame = hasFrame && e !== Platform.LINUX;
|
||||
|
||||
const requireRestart = isSwitchingToFrame || isSwitchingToNoFrame;
|
||||
const requireRestart = isSwitchingToFrame || isSwitchingToNoFrame;
|
||||
|
||||
if (requireRestart) {
|
||||
toast.info({
|
||||
autoClose: false,
|
||||
id: 'restart-toast',
|
||||
message: 'Restart to apply changes... close the notification to restart Feishin',
|
||||
onClose: () => {
|
||||
window.electron.ipc.send('app-restart');
|
||||
},
|
||||
title: 'Restart required',
|
||||
});
|
||||
} else {
|
||||
toast.update({ autoClose: 0, id: 'restart-toast', message: '', onClose: () => {} }); // clean old toasts
|
||||
}
|
||||
if (requireRestart) {
|
||||
toast.info({
|
||||
autoClose: false,
|
||||
id: 'restart-toast',
|
||||
message:
|
||||
'Restart to apply changes... close the notification to restart Feishin',
|
||||
onClose: () => {
|
||||
window.electron.ipc.send('app-restart');
|
||||
},
|
||||
title: 'Restart required',
|
||||
});
|
||||
} else {
|
||||
toast.update({
|
||||
autoClose: 0,
|
||||
id: 'restart-toast',
|
||||
message: '',
|
||||
onClose: () => {},
|
||||
}); // clean old toasts
|
||||
}
|
||||
|
||||
localSettings?.set('window_window_bar_style', e as Platform);
|
||||
setSettings({
|
||||
window: {
|
||||
...settings,
|
||||
windowBarStyle: e as Platform,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Adjust the style of the application window bar',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Window bar style',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Toggle minimize to tray"
|
||||
defaultChecked={settings.exitToTray}
|
||||
disabled={!isElectron()}
|
||||
onChange={(e) => {
|
||||
if (!e) return;
|
||||
localSettings?.set('window_minimize_to_tray', e.currentTarget.checked);
|
||||
setSettings({
|
||||
window: {
|
||||
...settings,
|
||||
minimizeToTray: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Minimize the application to the system tray',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Minimize to tray',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Toggle exit to tray"
|
||||
defaultChecked={settings.exitToTray}
|
||||
disabled={!isElectron()}
|
||||
onChange={(e) => {
|
||||
if (!e) return;
|
||||
localSettings?.set('window_exit_to_tray', e.currentTarget.checked);
|
||||
setSettings({
|
||||
window: {
|
||||
...settings,
|
||||
exitToTray: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Exit the application to the system tray',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Exit to tray',
|
||||
},
|
||||
];
|
||||
localSettings?.set('window_window_bar_style', e as Platform);
|
||||
setSettings({
|
||||
window: {
|
||||
...settings,
|
||||
windowBarStyle: e as Platform,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Adjust the style of the application window bar',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Window bar style',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Toggle minimize to tray"
|
||||
defaultChecked={settings.exitToTray}
|
||||
disabled={!isElectron()}
|
||||
onChange={(e) => {
|
||||
if (!e) return;
|
||||
localSettings?.set('window_minimize_to_tray', e.currentTarget.checked);
|
||||
setSettings({
|
||||
window: {
|
||||
...settings,
|
||||
minimizeToTray: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Minimize the application to the system tray',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Minimize to tray',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Toggle exit to tray"
|
||||
defaultChecked={settings.exitToTray}
|
||||
disabled={!isElectron()}
|
||||
onChange={(e) => {
|
||||
if (!e) return;
|
||||
localSettings?.set('window_exit_to_tray', e.currentTarget.checked);
|
||||
setSettings({
|
||||
window: {
|
||||
...settings,
|
||||
exitToTray: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Exit the application to the system tray',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Exit to tray',
|
||||
},
|
||||
];
|
||||
|
||||
return <SettingsSection options={windowOptions} />;
|
||||
return <SettingsSection options={windowOptions} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import { UpdateSettings } from '/@/renderer/features/settings/components/window/
|
|||
import { WindowSettings } from '/@/renderer/features/settings/components/window/window-settings';
|
||||
|
||||
export const WindowTab = () => {
|
||||
return (
|
||||
<Stack spacing="md">
|
||||
<WindowSettings />
|
||||
<Divider />
|
||||
<UpdateSettings />
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack spacing="md">
|
||||
<WindowSettings />
|
||||
<Divider />
|
||||
<UpdateSettings />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue