mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 18:33:33 +00:00
Add localization support (#333)
* Add updated i18n config and en locale
This commit is contained in:
parent
11863fd4c1
commit
8430b1ec95
90 changed files with 2679 additions and 908 deletions
|
|
@ -10,8 +10,10 @@ import {
|
|||
useGeneralSettings,
|
||||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FontType } from '/@/renderer/types';
|
||||
import i18n, { languages } from '/@/i18n/i18n';
|
||||
|
||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
const ipc = isElectron() ? window.electron.ipc : null;
|
||||
|
|
@ -33,17 +35,32 @@ const FONT_OPTIONS: Font[] = [
|
|||
{ label: 'Work Sans', value: 'Work Sans' },
|
||||
];
|
||||
|
||||
const FONT_TYPES: Font[] = [{ label: 'Built-in font', value: FontType.BUILT_IN }];
|
||||
const FONT_TYPES: Font[] = [
|
||||
{
|
||||
label: i18n.t('setting.fontType', {
|
||||
context: 'optionBuiltIn',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
value: FontType.BUILT_IN,
|
||||
},
|
||||
];
|
||||
|
||||
if (window.queryLocalFonts) {
|
||||
FONT_TYPES.push({ label: 'System font', value: FontType.SYSTEM });
|
||||
FONT_TYPES.push({
|
||||
label: i18n.t('setting.fontType', { context: 'optionSystem', postProcess: 'sentenceCase' }),
|
||||
value: FontType.SYSTEM,
|
||||
});
|
||||
}
|
||||
|
||||
if (isElectron()) {
|
||||
FONT_TYPES.push({ label: 'Custom font', value: FontType.CUSTOM });
|
||||
FONT_TYPES.push({
|
||||
label: i18n.t('setting.fontType', { context: 'optionCustom', postProcess: 'sentenceCase' }),
|
||||
value: FontType.CUSTOM,
|
||||
});
|
||||
}
|
||||
|
||||
export const ApplicationSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = useGeneralSettings();
|
||||
const fontSettings = useFontSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
|
@ -100,7 +117,9 @@ export const ApplicationSettings = () => {
|
|||
const status = await navigator.permissions.query({ name: 'local-fonts' });
|
||||
|
||||
if (status.state === 'denied') {
|
||||
throw new Error('Access denied to local fonts');
|
||||
throw new Error(
|
||||
t('error.localFontAccessDenied', { postProcess: 'sentenceCase' }),
|
||||
);
|
||||
}
|
||||
|
||||
const data = await window.queryLocalFonts();
|
||||
|
|
@ -112,7 +131,7 @@ export const ApplicationSettings = () => {
|
|||
);
|
||||
} catch (error) {
|
||||
toast.error({
|
||||
message: 'An error occurred when trying to get system fonts',
|
||||
message: t('error.systemFontError', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
|
||||
setSettings({
|
||||
|
|
@ -125,19 +144,32 @@ export const ApplicationSettings = () => {
|
|||
}
|
||||
};
|
||||
getFonts();
|
||||
}, [fontSettings, localFonts, setSettings]);
|
||||
}, [fontSettings, localFonts, setSettings, t]);
|
||||
|
||||
const handleChangeLanguage = (e: string) => {
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
language: e,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const options: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
disabled
|
||||
data={[]}
|
||||
data={languages}
|
||||
value={settings.language}
|
||||
onChange={handleChangeLanguage}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the application language',
|
||||
description: t('setting.language', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: false,
|
||||
title: 'Language',
|
||||
title: t('setting.language', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -155,10 +187,12 @@ export const ApplicationSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'What font to use. Built-in font selects one of the fonts provided by Feishin. System font allows you to select any font provided by your OS. Custom allows you to provide your own font',
|
||||
description: t('setting.fontType', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: FONT_TYPES.length === 1,
|
||||
title: 'Use system font',
|
||||
title: t('setting.fontType', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -177,9 +211,9 @@ export const ApplicationSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the application content font',
|
||||
description: t('setting.font', { context: 'description', postProcess: 'sentenceCase' }),
|
||||
isHidden: localFonts && fontSettings.type !== FontType.BUILT_IN,
|
||||
title: 'Font (Content)',
|
||||
title: t('setting.font', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -199,9 +233,9 @@ export const ApplicationSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the application content font',
|
||||
description: t('setting.font', { context: 'description', postProcess: 'sentenceCase' }),
|
||||
isHidden: !localFonts || fontSettings.type !== FontType.SYSTEM,
|
||||
title: 'Font (Content)',
|
||||
title: t('setting.font', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -219,9 +253,12 @@ export const ApplicationSettings = () => {
|
|||
}
|
||||
/>
|
||||
),
|
||||
description: 'Path to custom font',
|
||||
description: t('setting.customFontPath', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: fontSettings.type !== FontType.CUSTOM,
|
||||
title: 'Path to custom font',
|
||||
title: t('setting.customFontPath', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -244,9 +281,14 @@ export const ApplicationSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the application zoom factor in percent',
|
||||
description: t('setting.zoom', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Zoom factor',
|
||||
title: t('setting.zoom', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import isElectron from 'is-electron';
|
||||
import { Group } from '@mantine/core';
|
||||
import { t } from 'i18next';
|
||||
import isElectron from 'is-electron';
|
||||
import { Select, Tooltip, NumberInput, Switch, Slider } from '/@/renderer/components';
|
||||
import { SettingsSection } from '/@/renderer/features/settings/components/settings-section';
|
||||
import {
|
||||
|
|
@ -8,15 +9,29 @@ import {
|
|||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { Play } from '/@/renderer/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
|
||||
const SIDE_QUEUE_OPTIONS = [
|
||||
{ label: 'Fixed', value: 'sideQueue' },
|
||||
{ label: 'Floating', value: 'sideDrawerQueue' },
|
||||
{
|
||||
label: t('setting.sidePlayQueueStyle', {
|
||||
context: 'optionAttached',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
value: 'sideQueue',
|
||||
},
|
||||
{
|
||||
label: t('setting.sidePlayQueueStyle', {
|
||||
context: 'optionDetached',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
value: 'sideDrawerQueue',
|
||||
},
|
||||
];
|
||||
|
||||
export const ControlSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = useGeneralSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -39,14 +54,17 @@ export const ControlSettings = () => {
|
|||
}
|
||||
/>
|
||||
),
|
||||
description: 'Show or hide the skip buttons on the playerbar',
|
||||
description: t('setting.showSkipButtons', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: false,
|
||||
title: 'Show skip buttons',
|
||||
title: t('setting.showSkipButtons', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Group>
|
||||
<Tooltip label="Backward">
|
||||
<Tooltip label={t('common.backward', { postProcess: 'titleCase' })}>
|
||||
<NumberInput
|
||||
defaultValue={settings.skipButtons.skipBackwardSeconds}
|
||||
min={0}
|
||||
|
|
@ -66,7 +84,7 @@ export const ControlSettings = () => {
|
|||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label="Forward">
|
||||
<Tooltip label={t('common.forward', { postProcess: 'titleCase' })}>
|
||||
<NumberInput
|
||||
defaultValue={settings.skipButtons.skipForwardSeconds}
|
||||
min={0}
|
||||
|
|
@ -88,18 +106,38 @@ export const ControlSettings = () => {
|
|||
</Tooltip>
|
||||
</Group>
|
||||
),
|
||||
description:
|
||||
'The number (in seconds) to skip forward or backward when using the skip buttons',
|
||||
description: t('setting.skipDuration', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: false,
|
||||
title: 'Skip duration',
|
||||
title: t('setting.skipDuration', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{ label: 'Now', value: Play.NOW },
|
||||
{ label: 'Next', value: Play.NEXT },
|
||||
{ label: 'Last', value: Play.LAST },
|
||||
{
|
||||
label: t('setting.playButtonBehavior', {
|
||||
context: 'optionPlay',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: Play.NOW,
|
||||
},
|
||||
{
|
||||
label: t('setting.playButtonBehavior', {
|
||||
context: 'optionAddNext',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: Play.NEXT,
|
||||
},
|
||||
{
|
||||
label: t('setting.playButtonBehavior', {
|
||||
context: 'optionAddLast',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: Play.LAST,
|
||||
},
|
||||
]}
|
||||
defaultValue={settings.playButtonBehavior}
|
||||
onChange={(e) =>
|
||||
|
|
@ -112,9 +150,12 @@ export const ControlSettings = () => {
|
|||
}
|
||||
/>
|
||||
),
|
||||
description: 'The default behavior of the play button when adding songs to the queue',
|
||||
description: t('setting.playButtonBehavior', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: false,
|
||||
title: 'Play button behavior',
|
||||
title: t('setting.playButtonBehavior', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -131,9 +172,12 @@ export const ControlSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'The style of the sidebar play queue',
|
||||
description: t('setting.sidePlayQueueStyle', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: false,
|
||||
title: 'Side play queue style',
|
||||
title: t('setting.sidePlayQueueStyle', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -149,10 +193,12 @@ export const ControlSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Display a hover icon on the right side of the application view the play queue',
|
||||
description: t('setting.sidePlayQueueStyle', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: false,
|
||||
title: 'Show floating queue hover area',
|
||||
title: t('setting.floatingQueueArea', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -171,10 +217,12 @@ export const ControlSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'The amount of volume to change when scrolling the mouse wheel on the volume slider',
|
||||
description: t('setting.volumeWheelStep', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: false,
|
||||
title: 'Volume wheel step',
|
||||
title: t('setting.volumeWheelStep', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -191,9 +239,12 @@ export const ControlSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'When exiting, save the current play queue and restore it when reopening',
|
||||
description: t('setting.savePlayQueue', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Save play queue',
|
||||
title: t('setting.savePlayQueue', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -210,10 +261,12 @@ export const ControlSettings = () => {
|
|||
}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'When navigating to a playlist, go to the playlist song list page instead of the default page',
|
||||
description: t('setting.skipPlaylistPage', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: false,
|
||||
title: 'Go to playlist songs page by default',
|
||||
title: t('setting.skipPlaylistPage', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ import { SettingsSection } from '/@/renderer/features/settings/components/settin
|
|||
import { useRemoteSettings, useSettingsStoreActions } from '/@/renderer/store';
|
||||
import { NumberInput, Switch, Text, TextInput, toast } from '/@/renderer/components';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const remote = isElectron() ? window.electron.remote : null;
|
||||
|
||||
export const RemoteSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = useRemoteSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -25,7 +27,9 @@ export const RemoteSettings = () => {
|
|||
} else {
|
||||
toast.error({
|
||||
message: errorMsg,
|
||||
title: enabled ? 'Error enabling remote' : 'Error disabling remote',
|
||||
title: enabled
|
||||
? t('error.remoteEnableError', { postProcess: 'sentenceCase' })
|
||||
: t('error.remoteDisableError', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
}
|
||||
}, 50);
|
||||
|
|
@ -40,12 +44,12 @@ export const RemoteSettings = () => {
|
|||
},
|
||||
});
|
||||
toast.warn({
|
||||
message: 'To have your port change take effect, stop and restart the server',
|
||||
message: t('error.remotePortWarning', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
} else {
|
||||
toast.error({
|
||||
message: errorMsg,
|
||||
title: 'Error setting port',
|
||||
title: t('error.remotePortError', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
|
|
@ -56,7 +60,6 @@ export const RemoteSettings = () => {
|
|||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Enable remote control server"
|
||||
defaultChecked={settings.enabled}
|
||||
onChange={async (e) => {
|
||||
const enabled = e.currentTarget.checked;
|
||||
|
|
@ -65,8 +68,15 @@ export const RemoteSettings = () => {
|
|||
/>
|
||||
),
|
||||
description: (
|
||||
<div>
|
||||
Start an HTTP server to remotely control Feishin. This will listen on{' '}
|
||||
<Text
|
||||
$noSelect
|
||||
$secondary
|
||||
size="sm"
|
||||
>
|
||||
{t('setting.enableRemote', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
})}{' '}
|
||||
<a
|
||||
href={url}
|
||||
rel="noreferrer noopener"
|
||||
|
|
@ -74,15 +84,14 @@ export const RemoteSettings = () => {
|
|||
>
|
||||
{url}
|
||||
</a>
|
||||
</div>
|
||||
</Text>
|
||||
),
|
||||
isHidden,
|
||||
title: 'Enable remote control',
|
||||
title: t('setting.enableRemote', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<NumberInput
|
||||
aria-label="Set remote port"
|
||||
max={65535}
|
||||
value={settings.port}
|
||||
onBlur={async (e) => {
|
||||
|
|
@ -92,15 +101,16 @@ export const RemoteSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Remote server port. Changes here only take effect when you enable the remote',
|
||||
description: t('setting.remotePort', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden,
|
||||
title: 'Remove server port',
|
||||
title: t('setting.remotePort', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<TextInput
|
||||
aria-label="Set remote username"
|
||||
defaultValue={settings.username}
|
||||
onBlur={(e) => {
|
||||
const username = e.currentTarget.value;
|
||||
|
|
@ -115,15 +125,16 @@ export const RemoteSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Username that must be provided to access remote. If both username and password are empty, disable authentication',
|
||||
description: t('setting.remoteUsername', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden,
|
||||
title: 'Remote username',
|
||||
title: t('setting.remoteUsername', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<TextInput
|
||||
aria-label="Set remote password"
|
||||
defaultValue={settings.password}
|
||||
onBlur={(e) => {
|
||||
const password = e.currentTarget.value;
|
||||
|
|
@ -138,22 +149,14 @@ export const RemoteSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Password to access remote',
|
||||
description: t('setting.remotePassword', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden,
|
||||
title: 'Remote password',
|
||||
title: t('setting.remotePassword', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSection options={controlOptions} />
|
||||
<Text size="lg">
|
||||
<b>
|
||||
NOTE: these credentials are by default transferred insecurely. Do not use a
|
||||
password you care about. Changing username/password will disconnect clients and
|
||||
require them to reauthenticate
|
||||
</b>
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
return <SettingsSection options={controlOptions} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { ChangeEvent, useCallback, useState } from 'react';
|
|||
import { Group } from '@mantine/core';
|
||||
import { Reorder, useDragControls } from 'framer-motion';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MdDragIndicator } from 'react-icons/md';
|
||||
import { Button, Checkbox, Switch } from '/@/renderer/components';
|
||||
import { useSettingsStoreActions, useGeneralSettings } from '../../../../store/settings.store';
|
||||
|
|
@ -54,6 +55,7 @@ const DraggableSidebarItem = ({ item, handleChangeDisabled }: DraggableSidebarIt
|
|||
};
|
||||
|
||||
export const SidebarSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = useGeneralSettings();
|
||||
const { setSidebarItems, setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -107,8 +109,11 @@ export const SidebarSettings = () => {
|
|||
onChange={handleSetSidebarPlaylistList}
|
||||
/>
|
||||
}
|
||||
description="Show playlist list in sidebar"
|
||||
title="Sidebar playlist list"
|
||||
description={t('setting.sidebarPlaylistList', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
title={t('setting.sidebarPlaylistList', { postProcess: 'sentenceCase' })}
|
||||
/>
|
||||
<SettingsOptions
|
||||
control={
|
||||
|
|
@ -117,8 +122,11 @@ export const SidebarSettings = () => {
|
|||
onChange={handleSetSidebarCollapsedNavigation}
|
||||
/>
|
||||
}
|
||||
description="Show navigation buttons in the collapsed sidebar"
|
||||
title="Sidebar (collapsed) navigation"
|
||||
description={t('setting.sidebarPlaylistList', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
title={t('setting.sidebarCollapsedNavigation', { postProcess: 'sentenceCase' })}
|
||||
/>
|
||||
<SettingsOptions
|
||||
control={
|
||||
|
|
@ -128,11 +136,14 @@ export const SidebarSettings = () => {
|
|||
variant="filled"
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save sidebar configuration
|
||||
{t('common.save', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
}
|
||||
description="Select the items and order in which they appear in the sidebar"
|
||||
title="Sidebar configuration"
|
||||
description={t('setting.sidebarCollapsedNavigation', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
title={t('setting.sidebarConfiguration', { postProcess: 'sentenceCase' })}
|
||||
/>
|
||||
<Reorder.Group
|
||||
axis="y"
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import {
|
|||
import { THEME_DATA } from '/@/renderer/hooks';
|
||||
import { useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { AppTheme } from '/@/renderer/themes/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const ThemeSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = useGeneralSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -27,9 +29,12 @@ export const ThemeSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Follows the system-defined light or dark preference',
|
||||
description: t('setting.useSystemTheme', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: false,
|
||||
title: 'Use system theme',
|
||||
title: t('setting.useSystemTheme', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -46,9 +51,12 @@ export const ThemeSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the default theme',
|
||||
description: t('setting.theme', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.followSystemTheme,
|
||||
title: 'Theme',
|
||||
title: t('setting.theme', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -65,9 +73,12 @@ export const ThemeSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the dark theme',
|
||||
description: t('setting.themeDark', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !settings.followSystemTheme,
|
||||
title: 'Theme (dark)',
|
||||
title: t('setting.themeDark', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -84,9 +95,12 @@ export const ThemeSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Sets the light theme',
|
||||
description: t('setting.themeLight', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !settings.followSystemTheme,
|
||||
title: 'Theme (light)',
|
||||
title: t('setting.themeLight', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -114,8 +128,11 @@ export const ThemeSettings = () => {
|
|||
<Text>{settings.accent}</Text>
|
||||
</Stack>
|
||||
),
|
||||
description: 'Sets the accent color',
|
||||
title: 'Accent color',
|
||||
description: t('setting.accentColor', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.accentColor', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -2,48 +2,92 @@ import { useCallback, useMemo, useState, KeyboardEvent, ChangeEvent } from 'reac
|
|||
import { Group } from '@mantine/core';
|
||||
import isElectron from 'is-electron';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiDeleteBinLine, RiEditLine, RiKeyboardBoxLine } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
import { Button, TextInput, Checkbox } from '/@/renderer/components';
|
||||
import { BindingActions, useHotkeySettings, useSettingsStoreActions } from '/@/renderer/store';
|
||||
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
|
||||
const ipc = isElectron() ? window.electron.ipc : null;
|
||||
|
||||
const BINDINGS_MAP: Record<BindingActions, string> = {
|
||||
browserBack: 'Browser back',
|
||||
browserForward: 'Browser forward',
|
||||
favoriteCurrentAdd: 'Favorite current song',
|
||||
favoriteCurrentRemove: 'Unfavorite current song',
|
||||
favoriteCurrentToggle: 'Toggle current song favorite',
|
||||
favoritePreviousAdd: 'Favorite previous song',
|
||||
favoritePreviousRemove: 'Unfavorite previous song',
|
||||
favoritePreviousToggle: 'Toggle previous song favorite',
|
||||
globalSearch: 'Global search',
|
||||
localSearch: 'In-page search',
|
||||
next: 'Next track',
|
||||
pause: 'Pause',
|
||||
play: 'Play',
|
||||
playPause: 'Play / Pause',
|
||||
previous: 'Previous track',
|
||||
rate0: 'Rating clear',
|
||||
rate1: 'Rating 1 star',
|
||||
rate2: 'Rating 2 star',
|
||||
rate3: 'Rating 3 star',
|
||||
rate4: 'Rating 4 star',
|
||||
rate5: 'Rating 5 star',
|
||||
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',
|
||||
browserBack: i18n.t('setting.hotkey', { context: 'browserBack', postProcess: 'sentenceCase' }),
|
||||
browserForward: i18n.t('setting.hotkey', {
|
||||
context: 'browserForward',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
favoriteCurrentAdd: i18n.t('setting.hotkey', {
|
||||
context: 'favoriteCurrentSong',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
favoriteCurrentRemove: i18n.t('setting.hotkey', {
|
||||
context: 'unfavoriteCurrentSong',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
favoriteCurrentToggle: i18n.t('setting.hotkey', {
|
||||
context: 'toggleCurrentSongFavorite',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
favoritePreviousAdd: i18n.t('setting.hotkey', {
|
||||
context: 'favoritePreviousSong',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
favoritePreviousRemove: i18n.t('setting.hotkey', {
|
||||
context: 'unfavoritePreviousSong',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
favoritePreviousToggle: i18n.t('setting.hotkey', {
|
||||
context: 'togglePreviousSongFavorite',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
globalSearch: i18n.t('setting.hotkey', {
|
||||
context: 'globalSearch',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
localSearch: i18n.t('setting.hotkey', { context: 'localSearch', postProcess: 'sentenceCase' }),
|
||||
next: i18n.t('setting.hotkey', { context: 'playbackNext', postProcess: 'sentenceCase' }),
|
||||
pause: i18n.t('setting.hotkey', { context: 'playbackPause', postProcess: 'sentenceCase' }),
|
||||
play: i18n.t('setting.hotkey', { context: 'playbackPlay', postProcess: 'sentenceCase' }),
|
||||
playPause: i18n.t('setting.hotkey', {
|
||||
context: 'playbackPlayPause',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
previous: i18n.t('setting.hotkey', {
|
||||
context: 'playbackPrevious',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
rate0: i18n.t('setting.hotkey', { context: 'rate0', postProcess: 'sentenceCase' }),
|
||||
rate1: i18n.t('setting.hotkey', { context: 'rate1', postProcess: 'sentenceCase' }),
|
||||
rate2: i18n.t('setting.hotkey', { context: 'rate2', postProcess: 'sentenceCase' }),
|
||||
rate3: i18n.t('setting.hotkey', { context: 'rate3', postProcess: 'sentenceCase' }),
|
||||
rate4: i18n.t('setting.hotkey', { context: 'rate4', postProcess: 'sentenceCase' }),
|
||||
rate5: i18n.t('setting.hotkey', { context: 'rate5', postProcess: 'sentenceCase' }),
|
||||
skipBackward: i18n.t('setting.hotkey', {
|
||||
context: 'skipBackward',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
skipForward: i18n.t('setting.hotkey', { context: 'skipForward', postProcess: 'sentenceCase' }),
|
||||
stop: i18n.t('setting.hotkey', { context: 'playbackStop', postProcess: 'sentenceCase' }),
|
||||
toggleFullscreenPlayer: i18n.t('setting.hotkey', {
|
||||
context: 'toggleFullScreenPlayer',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
toggleQueue: i18n.t('setting.hotkey', { context: 'toggleQueue', postProcess: 'sentenceCase' }),
|
||||
toggleRepeat: i18n.t('setting.hotkey', {
|
||||
context: 'toggleRepeat',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
toggleShuffle: i18n.t('setting.hotkey', {
|
||||
context: 'toggleShuffle',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
volumeDown: i18n.t('setting.hotkey', { context: 'volumeDown', postProcess: 'sentenceCase' }),
|
||||
volumeMute: i18n.t('setting.hotkey', { context: 'volumeMute', postProcess: 'sentenceCase' }),
|
||||
volumeUp: i18n.t('setting.hotkey', { context: 'volumeUp', postProcess: 'sentenceCase' }),
|
||||
zoomIn: i18n.t('setting.hotkey', { context: 'zoomIn', postProcess: 'sentenceCase' }),
|
||||
zoomOut: i18n.t('setting.hotkey', { context: 'zoomOut', postProcess: 'sentenceCase' }),
|
||||
};
|
||||
|
||||
const HotkeysContainer = styled.div`
|
||||
|
|
@ -59,6 +103,7 @@ const HotkeysContainer = styled.div`
|
|||
`;
|
||||
|
||||
export const HotkeyManagerSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const { bindings, globalMediaHotkeys } = useHotkeySettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const [selected, setSelected] = useState<BindingActions | null>(null);
|
||||
|
|
@ -175,8 +220,11 @@ export const HotkeyManagerSettings = () => {
|
|||
<>
|
||||
<SettingsOptions
|
||||
control={<></>}
|
||||
description="Configure application hotkeys. Toggle the checkbox to set as a global hotkey (desktop only)"
|
||||
title="Application hotkeys"
|
||||
description={t('setting.applicationHotkeys', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
title={t('setting.applicationHotkeys', { postProcess: 'sentenceCase' })}
|
||||
/>
|
||||
<HotkeysContainer>
|
||||
{Object.keys(bindings)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import isElectron from 'is-electron';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SettingOption, SettingsSection } from '../settings-section';
|
||||
import { Switch } from '/@/renderer/components';
|
||||
import { useHotkeySettings, useSettingsStoreActions } from '/@/renderer/store';
|
||||
|
|
@ -6,6 +7,7 @@ import { useHotkeySettings, useSettingsStoreActions } from '/@/renderer/store';
|
|||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
|
||||
export const WindowHotkeySettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = useHotkeySettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -13,7 +15,6 @@ export const WindowHotkeySettings = () => {
|
|||
{
|
||||
control: (
|
||||
<Switch
|
||||
aria-label="Toggle global media hotkeys"
|
||||
defaultChecked={settings.globalMediaHotkeys}
|
||||
disabled={!isElectron()}
|
||||
onChange={(e) => {
|
||||
|
|
@ -33,10 +34,12 @@ export const WindowHotkeySettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Enable or disable the usage of your system media hotkeys to control the audio player',
|
||||
description: t('setting.globalMediaHotkeys', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Global media hotkeys',
|
||||
title: t('setting.globalMediaHotkeys', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { useCurrentStatus, usePlayerStore } from '/@/renderer/store';
|
||||
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { PlaybackType, PlayerStatus, PlaybackStyle, CrossfadeStyle } from '/@/renderer/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ const getAudioDevice = async () => {
|
|||
};
|
||||
|
||||
export const AudioSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const status = useCurrentStatus();
|
||||
|
|
@ -30,13 +32,17 @@ export const AudioSettings = () => {
|
|||
.then((dev) =>
|
||||
setAudioDevices(dev.map((d) => ({ label: d.label, value: d.deviceId }))),
|
||||
)
|
||||
.catch(() => toast.error({ message: 'Error fetching audio devices' }));
|
||||
.catch(() =>
|
||||
toast.error({
|
||||
message: t('error.audioDeviceFetchError', { postProcess: 'sentenceCase' }),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
if (settings.type === PlaybackType.WEB) {
|
||||
getAudioDevices();
|
||||
}
|
||||
}, [settings.type]);
|
||||
}, [settings.type, t]);
|
||||
|
||||
const audioOptions: SettingOption[] = [
|
||||
{
|
||||
|
|
@ -61,10 +67,16 @@ export const AudioSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'The audio player to use for playback',
|
||||
description: t('setting.audioPlayer', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||
title: 'Audio player',
|
||||
note:
|
||||
status === PlayerStatus.PLAYING
|
||||
? t('common.playerMustBePaused', { postProcess: 'sentenceCase' })
|
||||
: undefined,
|
||||
title: t('setting.audioPlayer', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -76,16 +88,31 @@ export const AudioSettings = () => {
|
|||
onChange={(e) => setSettings({ playback: { ...settings, audioDeviceId: e } })}
|
||||
/>
|
||||
),
|
||||
description: 'The audio device to use for playback (web player only)',
|
||||
description: t('setting.audioDevice', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron() || settings.type !== PlaybackType.WEB,
|
||||
title: 'Audio device',
|
||||
title: t('setting.audioDevice', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{ label: 'Normal', value: PlaybackStyle.GAPLESS },
|
||||
{ label: 'Crossfade', value: PlaybackStyle.CROSSFADE },
|
||||
{
|
||||
label: t('setting.playbackStyle', {
|
||||
context: 'optionNormal',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: PlaybackStyle.GAPLESS,
|
||||
},
|
||||
{
|
||||
label: t('setting.playbackStyle', {
|
||||
context: 'optionCrossFade',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: PlaybackStyle.CROSSFADE,
|
||||
},
|
||||
]}
|
||||
defaultValue={settings.style}
|
||||
disabled={settings.type !== PlaybackType.WEB || status === PlayerStatus.PLAYING}
|
||||
|
|
@ -94,10 +121,16 @@ export const AudioSettings = () => {
|
|||
}
|
||||
/>
|
||||
),
|
||||
description: 'Adjust the playback style (web player only)',
|
||||
description: t('setting.playbackStyle', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.WEB,
|
||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||
title: 'Playback style',
|
||||
title: t('setting.playbackStyle', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -116,10 +149,15 @@ export const AudioSettings = () => {
|
|||
}
|
||||
/>
|
||||
),
|
||||
description: 'Adjust the crossfade duration (web player only)',
|
||||
description: t('setting.crossfadeDuration', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.WEB,
|
||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||
title: 'Crossfade Duration',
|
||||
title: t('setting.crossfadeDuration', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -153,10 +191,13 @@ export const AudioSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Change the crossfade algorithm (web player only)',
|
||||
description: t('setting.crossfadeStyle', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.WEB,
|
||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||
title: 'Crossfade Style',
|
||||
title: t('setting.crossfadeStyle', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { MultiSelect, MultiSelectProps, NumberInput, Switch } from '/@/renderer/
|
|||
import isElectron from 'is-electron';
|
||||
import styled from 'styled-components';
|
||||
import { LyricSource } from '/@/renderer/api/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
|
||||
|
|
@ -17,6 +18,7 @@ const WorkingButtonSelect = styled(MultiSelect)<MultiSelectProps>`
|
|||
`;
|
||||
|
||||
export const LyricSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = useLyricsSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -36,8 +38,11 @@ export const LyricSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enable or disable following of current lyric',
|
||||
title: 'Follow current lyric',
|
||||
description: t('setting.followLyric', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.followLyric', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -54,9 +59,12 @@ export const LyricSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enable or disable fetching lyrics for the current song',
|
||||
description: t('setting.lyricFetch', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Fetch lyrics from the internet',
|
||||
title: t('setting.lyricFetch', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -77,10 +85,12 @@ export const LyricSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Lyric fetchers should be added in order of preference. This is the order in which they will be queried.',
|
||||
description: t('setting.lyricFetchProvider', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Providers to fetch lyrics',
|
||||
title: t('setting.lyricFetchProvider', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -99,10 +109,12 @@ export const LyricSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Lyric offset (in milliseconds). Positive values mean that lyrics are shown later, and negative mean that lyrics are shown earlier',
|
||||
description: t('setting.lyricOffset', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Lyric offset',
|
||||
title: t('setting.lyricOffset', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { PlaybackType } from '/@/renderer/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
|
|
@ -60,6 +61,7 @@ export const getMpvProperties = (settings: SettingsState['playback']['mpvPropert
|
|||
};
|
||||
|
||||
export const MpvSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -116,10 +118,13 @@ export const MpvSettings = () => {
|
|||
onChange={handleSetMpvPath}
|
||||
/>
|
||||
),
|
||||
description: 'The location of your mpv executable',
|
||||
description: t('setting.mpvExecutablePath', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
note: 'Restart required',
|
||||
title: 'MPV executable path',
|
||||
title: t('setting.mpvExecutablePath', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -128,9 +133,10 @@ export const MpvSettings = () => {
|
|||
autosize
|
||||
defaultValue={settings.mpvExtraParameters.join('\n')}
|
||||
minRows={4}
|
||||
placeholder={
|
||||
'(Add one per line):\n--gapless-audio=weak\n--prefetch-playlist=yes'
|
||||
}
|
||||
placeholder={`(${t('setting.mpvExtraParameters', {
|
||||
context: 'help',
|
||||
postProcess: 'sentenceCase',
|
||||
})}):\n--gapless-audio=weak\n--prefetch-playlist=yes`}
|
||||
width={225}
|
||||
onBlur={(e) => {
|
||||
handleSetExtraParameters(e.currentTarget.value.split('\n'));
|
||||
|
|
@ -145,7 +151,10 @@ export const MpvSettings = () => {
|
|||
$secondary
|
||||
size="sm"
|
||||
>
|
||||
Options to pass to the player
|
||||
{t('setting.mpvExtraParameters', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
<a
|
||||
|
|
@ -159,8 +168,12 @@ export const MpvSettings = () => {
|
|||
</Stack>
|
||||
),
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
note: 'Restart required',
|
||||
title: 'MPV parameters',
|
||||
note: t('common.restartRequired', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.mpvExtraParameters', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -169,18 +182,26 @@ export const MpvSettings = () => {
|
|||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{ label: 'No', value: 'no' },
|
||||
{ label: 'Yes', value: 'yes' },
|
||||
{ label: 'Weak (recommended)', value: 'weak' },
|
||||
{ label: t('common.no', { postProcess: 'titleCase' }), value: 'no' },
|
||||
{ label: t('common.yes', { postProcess: 'titleCase' }), value: 'yes' },
|
||||
{
|
||||
label: t('setting.gaplessAudio', {
|
||||
context: 'optionWeak',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
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)',
|
||||
description: t('setting.gaplessAudio', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
title: 'Gapless audio',
|
||||
title: t('setting.gaplessAudio', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -193,10 +214,12 @@ export const MpvSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Select the output sample rate to be used if the sample frequency selected is different from that of the current media',
|
||||
description: t('setting.sampleRate', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
note: 'Page refresh required for web player',
|
||||
title: 'Sample rate',
|
||||
title: t('setting.sampleRate', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -211,10 +234,12 @@ export const MpvSettings = () => {
|
|||
/>
|
||||
),
|
||||
|
||||
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)',
|
||||
description: t('setting.audioExclusiveMode', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
title: 'Audio exclusive mode',
|
||||
title: t('setting.audioExclusiveMode', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -223,18 +248,42 @@ export const MpvSettings = () => {
|
|||
control: (
|
||||
<Select
|
||||
data={[
|
||||
{ label: 'None', value: 'no' },
|
||||
{ label: 'Track', value: 'track' },
|
||||
{ label: 'Album', value: 'album' },
|
||||
{
|
||||
label: t('setting.replayGainMode', {
|
||||
context: 'optionNone',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: 'no',
|
||||
},
|
||||
{
|
||||
label: t('setting.replayGainMode', {
|
||||
context: 'optionTrack',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: 'track',
|
||||
},
|
||||
{
|
||||
label: t('setting.replayGainMode', {
|
||||
context: 'optionAlbum',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
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)',
|
||||
note: 'Restart required',
|
||||
title: 'ReplayGain mode',
|
||||
description: t('setting.replayGainMode', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
note: t('common.restartRequired', { postProcess: 'sentenceCase' }),
|
||||
title: t('setting.replayGainMode', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -244,9 +293,15 @@ export const MpvSettings = () => {
|
|||
onChange={(e) => handleSetMpvProperty('replayGainPreampDB', e)}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Pre-amplification gain in dB to apply to the selected replaygain gain (--replaygain-preamp)',
|
||||
title: 'ReplayGain preamp (dB)',
|
||||
description: t('setting.replayGainMode', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.replayGainPreamp', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -257,9 +312,14 @@ export const MpvSettings = () => {
|
|||
}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Prevent clipping caused by replaygain by automatically lowering the gain (--replaygain-clip)',
|
||||
title: 'ReplayGain clipping',
|
||||
description: t('setting.replayGainClipping', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.replayGainClipping', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -269,9 +329,14 @@ export const MpvSettings = () => {
|
|||
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',
|
||||
title: 'ReplayGain fallback (dB)',
|
||||
description: t('setting.replayGainFallback', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.replayGainFallback', {
|
||||
ReplayGain: 'ReplayGain',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -20,12 +20,8 @@ export const PlaybackTab = () => {
|
|||
<AudioSettings />
|
||||
<Suspense fallback={<></>}>{hasFancyAudio && <MpvSettings />}</Suspense>
|
||||
<Divider />
|
||||
{isElectron() && (
|
||||
<>
|
||||
<ScrobbleSettings />
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<ScrobbleSettings />
|
||||
<Divider />
|
||||
<LyricSettings />
|
||||
</Stack>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { NumberInput, Slider, Switch, Text } from '/@/renderer/components';
|
||||
import { NumberInput, Slider, Switch } from '/@/renderer/components';
|
||||
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { SettingOption, SettingsSection } from '../settings-section';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const ScrobbleSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -25,8 +27,11 @@ export const ScrobbleSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enable or disable scrobbling to your media server',
|
||||
title: 'Scrobble',
|
||||
description: t('setting.scrobble', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.scrobble', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -50,9 +55,11 @@ export const ScrobbleSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'The percentage of the song that must be played before submitting a scrobble',
|
||||
title: 'Minimum scrobble percentage*',
|
||||
description: t('setting.minimumScrobblePercentage', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.minimumScrobblePercentage', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -76,21 +83,13 @@ export const ScrobbleSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'The duration in seconds of a song that must be played before submitting a scrobble',
|
||||
title: 'Minimum scrobble duration (seconds)*',
|
||||
description: t('setting.minimumScrobblePercentage', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.minimumScrobbleSeconds', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
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} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { lazy } from 'react';
|
|||
import { Tabs } from '/@/renderer/components';
|
||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import isElectron from 'is-electron';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const GeneralTab = lazy(() =>
|
||||
|
|
@ -36,6 +37,7 @@ const TabContainer = styled.div`
|
|||
`;
|
||||
|
||||
export const SettingsContent = () => {
|
||||
const { t } = useTranslation();
|
||||
const currentTab = useSettingsStore((state) => state.tab);
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -49,10 +51,20 @@ export const SettingsContent = () => {
|
|||
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.Tab value="general">
|
||||
{t('page.setting.generalTab', { postProcess: 'sentenceCase' })}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="playback">
|
||||
{t('page.setting.playbackTab', { postProcess: 'sentenceCase' })}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="hotkeys">
|
||||
{t('page.setting.hotkeysTab', { postProcess: 'sentenceCase' })}
|
||||
</Tabs.Tab>
|
||||
{isElectron() && (
|
||||
<Tabs.Tab value="window">
|
||||
{t('page.setting.windowTab', { postProcess: 'sentenceCase' })}
|
||||
</Tabs.Tab>
|
||||
)}
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="general">
|
||||
<GeneralTab />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { Flex, Group } from '@mantine/core';
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiSettings2Fill } from 'react-icons/ri';
|
||||
import { Button, ConfirmModal, PageHeader } from '/@/renderer/components';
|
||||
import { LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||
import { useSettingsStoreActions } from '../../../store/settings.store';
|
||||
|
||||
export const SettingsHeader = () => {
|
||||
const { t } = useTranslation();
|
||||
const { reset } = useSettingsStoreActions();
|
||||
|
||||
const handleResetToDefault = () => {
|
||||
|
|
@ -15,8 +17,12 @@ export const SettingsHeader = () => {
|
|||
|
||||
const openResetConfirmModal = () => {
|
||||
openModal({
|
||||
children: <ConfirmModal onConfirm={handleResetToDefault}>Are you sure?</ConfirmModal>,
|
||||
title: 'Reset settings to default',
|
||||
children: (
|
||||
<ConfirmModal onConfirm={handleResetToDefault}>
|
||||
{t('common.areYouSure', { postProcess: 'sentenceCase' })}
|
||||
</ConfirmModal>
|
||||
),
|
||||
title: t('common.resetToDefault', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -31,14 +37,16 @@ export const SettingsHeader = () => {
|
|||
>
|
||||
<Group noWrap>
|
||||
<RiSettings2Fill size="2rem" />
|
||||
<LibraryHeaderBar.Title>Settings</LibraryHeaderBar.Title>
|
||||
<LibraryHeaderBar.Title>
|
||||
{t('common.setting', { count: 2, postProcess: 'titleCase' })}
|
||||
</LibraryHeaderBar.Title>
|
||||
</Group>
|
||||
<Button
|
||||
compact
|
||||
variant="default"
|
||||
onClick={openResetConfirmModal}
|
||||
>
|
||||
Reset to default
|
||||
{t('common.resetToDefault', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
</Flex>
|
||||
</LibraryHeaderBar>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export const SettingsSection = ({ options }: SettingsSectionProps) => {
|
|||
.filter((o) => !o.isHidden)
|
||||
.map((option) => (
|
||||
<SettingsOptions
|
||||
key={`general-${option.title}`}
|
||||
key={`option-${option.title}`}
|
||||
{...option}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import {
|
|||
SettingsSection,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { useDiscordSetttings, useSettingsStoreActions } from '/@/renderer/store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const DiscordSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = useDiscordSetttings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -25,10 +27,19 @@ export const DiscordSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description:
|
||||
'Enable playback status in Discord rich presence. Image keys include: "icon", "playing", and "paused"',
|
||||
description: t('setting.discordRichPresence', {
|
||||
context: 'description',
|
||||
discord: 'Discord',
|
||||
icon: 'icon',
|
||||
paused: 'paused',
|
||||
playing: 'playing',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Discord rich presence',
|
||||
title: t('setting.discordRichPresence', {
|
||||
discord: 'Discord',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -44,9 +55,17 @@ export const DiscordSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'The Discord application ID (defaults to 1165957668758900787)',
|
||||
description: t('setting.discordApplicationId', {
|
||||
context: 'description',
|
||||
defaultId: '1165957668758900787',
|
||||
discord: 'Discord',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Discord application ID',
|
||||
title: t('setting.discordApplicationId', {
|
||||
discord: 'Discord',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -67,9 +86,15 @@ export const DiscordSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'The time in seconds between each update (minimum 15 seconds)',
|
||||
description: t('setting.discordUpdateInterval', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Rich presence update interval (seconds)',
|
||||
title: t('setting.discordUpdateInterval', {
|
||||
discord: 'Discord',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -85,9 +110,14 @@ export const DiscordSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'When enabled, the rich presence will update while player is idle',
|
||||
description: t('setting.discordIdleStatus', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Show rich presence when idle',
|
||||
title: t('setting.discordIdleStatus', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import isElectron from 'is-electron';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useWindowSettings, useSettingsStoreActions } from '../../../../store/settings.store';
|
||||
import {
|
||||
SettingsSection,
|
||||
|
|
@ -9,6 +10,7 @@ import { Switch } from '/@/renderer/components';
|
|||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
|
||||
export const UpdateSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = useWindowSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -31,9 +33,12 @@ export const UpdateSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Enabling this option will disable checking for new versions on startup',
|
||||
description: t('setting.disableAutomaticUpdates', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Disable automatic updates',
|
||||
title: t('setting.disableAutomaticUpdates', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import isElectron from 'is-electron';
|
||||
import { Platform } from '/@/renderer/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useWindowSettings, useSettingsStoreActions } from '../../../../store/settings.store';
|
||||
import {
|
||||
SettingsSection,
|
||||
|
|
@ -17,6 +18,7 @@ const WINDOW_BAR_OPTIONS = [
|
|||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
|
||||
export const WindowSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = useWindowSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
|
|
@ -43,12 +45,15 @@ export const WindowSettings = () => {
|
|||
toast.info({
|
||||
autoClose: false,
|
||||
id: 'restart-toast',
|
||||
message:
|
||||
'Restart to apply changes... close the notification to restart Feishin',
|
||||
message: t('common.forceRestartRequired', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
onClose: () => {
|
||||
window.electron.ipc!.send('app-restart');
|
||||
},
|
||||
title: 'Restart required',
|
||||
title: t('common.restartRequired', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
toast.update({
|
||||
|
|
@ -69,9 +74,12 @@ export const WindowSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Adjust the style of the application window bar',
|
||||
description: t('setting.windowBarStyle', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Window bar style',
|
||||
title: t('setting.windowBarStyle', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -91,9 +99,12 @@ export const WindowSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Minimize the application to the system tray',
|
||||
description: t('setting.minimizeToTray', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Minimize to tray',
|
||||
title: t('setting.minimizeToTray', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
control: (
|
||||
|
|
@ -113,9 +124,12 @@ export const WindowSettings = () => {
|
|||
}}
|
||||
/>
|
||||
),
|
||||
description: 'Exit the application to the system tray',
|
||||
description: t('setting.exitToTray', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: 'Exit to tray',
|
||||
title: t('setting.exitToTray', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue