reorganize and redesign settings

This commit is contained in:
jeffvli
2025-11-23 18:15:38 -08:00
parent 7cc5ccd2c5
commit a2926ef47e
26 changed files with 629 additions and 540 deletions
@@ -7,6 +7,10 @@ import styles from './hotkeys-manager-settings.module.css';
import i18n from '/@/i18n/i18n';
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
import { useSettingSearchContext } from '/@/renderer/features/settings/context/search-context';
import { BindingActions, useHotkeySettings, useSettingsStoreActions } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
@@ -150,12 +154,12 @@ export const HotkeyManagerSettings = () => {
20,
);
const handleSetHotkey = useCallback(debouncedSetHotkey, [
bindings,
globalMediaHotkeys,
setSettings,
debouncedSetHotkey,
]);
const handleSetHotkey = useCallback(
(binding: BindingActions, e: KeyboardEvent<HTMLInputElement>) => {
debouncedSetHotkey(binding, e);
},
[debouncedSetHotkey],
);
const handleSetGlobalHotkey = useCallback(
(binding: BindingActions, e: ChangeEvent<HTMLInputElement>) => {
@@ -232,85 +236,97 @@ export const HotkeyManagerSettings = () => {
});
}, [bindings, keyword]);
const options: SettingOption[] = [];
return (
<>
<SettingsOptions
control={<></>}
description={t('setting.applicationHotkeys', {
context: 'description',
postProcess: 'sentenceCase',
})}
title={t('setting.applicationHotkeys', { postProcess: 'sentenceCase' })}
/>
<div className={styles.container}>
{filteredBindings.map((binding) => (
<Group key={`hotkey-${binding}`} wrap="nowrap">
<TextInput
readOnly
style={{ userSelect: 'none' }}
value={BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]}
/>
<TextInput
id={`hotkey-${binding}`}
leftSection={<Icon icon="keyboard" />}
onBlur={() => setSelected(null)}
onChange={() => {}}
onKeyDownCapture={(e) => {
if (selected !== (binding as BindingActions)) return;
handleSetHotkey(binding as BindingActions, e);
}}
readOnly
rightSection={
<ActionIcon
icon="edit"
onClick={() => {
setSelected(binding as BindingActions);
document.getElementById(`hotkey-${binding}`)?.focus();
}}
variant="transparent"
<SettingsSection
extra={
<>
<SettingsOptions
control={<></>}
description={t('setting.applicationHotkeys', {
context: 'description',
postProcess: 'sentenceCase',
})}
title={t('setting.applicationHotkeys', { postProcess: 'sentenceCase' })}
/>
<div className={styles.container}>
{filteredBindings.map((binding) => (
<Group key={`hotkey-${binding}`} wrap="nowrap">
<TextInput
readOnly
style={{ userSelect: 'none' }}
value={BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]}
/>
}
style={{
opacity: selected === (binding as BindingActions) ? 0.8 : 1,
outline: duplicateHotkeyMap.includes(
bindings[binding as keyof typeof BINDINGS_MAP].hotkey!,
)
? '1px dashed red'
: undefined,
}}
value={bindings[binding as keyof typeof BINDINGS_MAP].hotkey}
/>
{isElectron() && (
<Checkbox
checked={bindings[binding as keyof typeof BINDINGS_MAP].isGlobal}
disabled={
bindings[binding as keyof typeof BINDINGS_MAP].hotkey === ''
}
onChange={(e) =>
handleSetGlobalHotkey(binding as BindingActions, e)
}
size="md"
style={{
opacity: bindings[binding as keyof typeof BINDINGS_MAP]
.allowGlobal
? 1
: 0,
}}
/>
)}
{bindings[binding as keyof typeof BINDINGS_MAP].hotkey && (
<ActionIcon
icon="x"
iconProps={{
color: 'error',
}}
onClick={() => handleClearHotkey(binding as BindingActions)}
variant="transparent"
/>
)}
</Group>
))}
</div>
</>
<TextInput
id={`hotkey-${binding}`}
leftSection={<Icon icon="keyboard" />}
onBlur={() => setSelected(null)}
onChange={() => {}}
onKeyDownCapture={(e) => {
if (selected !== (binding as BindingActions)) return;
handleSetHotkey(binding as BindingActions, e);
}}
readOnly
rightSection={
<ActionIcon
icon="edit"
onClick={() => {
setSelected(binding as BindingActions);
document
.getElementById(`hotkey-${binding}`)
?.focus();
}}
variant="transparent"
/>
}
style={{
opacity: selected === (binding as BindingActions) ? 0.8 : 1,
outline: duplicateHotkeyMap.includes(
bindings[binding as keyof typeof BINDINGS_MAP].hotkey!,
)
? '1px dashed red'
: undefined,
}}
value={bindings[binding as keyof typeof BINDINGS_MAP].hotkey}
/>
{isElectron() && (
<Checkbox
checked={
bindings[binding as keyof typeof BINDINGS_MAP].isGlobal
}
disabled={
bindings[binding as keyof typeof BINDINGS_MAP]
.hotkey === ''
}
onChange={(e) =>
handleSetGlobalHotkey(binding as BindingActions, e)
}
size="md"
style={{
opacity: bindings[binding as keyof typeof BINDINGS_MAP]
.allowGlobal
? 1
: 0,
}}
/>
)}
{bindings[binding as keyof typeof BINDINGS_MAP].hotkey && (
<ActionIcon
icon="x"
iconProps={{
color: 'error',
}}
onClick={() => handleClearHotkey(binding as BindingActions)}
variant="transparent"
/>
)}
</Group>
))}
</div>
</>
}
options={options}
/>
);
};
@@ -1,14 +1,27 @@
import isElectron from 'is-electron';
import { Fragment } from 'react/jsx-runtime';
import { HotkeyManagerSettings } from '/@/renderer/features/settings/components/hotkeys/hotkey-manager-settings';
import { MediaSessionSettings } from '/@/renderer/features/settings/components/hotkeys/media-session-settings';
import { WindowHotkeySettings } from '/@/renderer/features/settings/components/hotkeys/window-hotkey-settings';
import { Divider } from '/@/shared/components/divider/divider';
import { Stack } from '/@/shared/components/stack/stack';
const sections = [
{ component: WindowHotkeySettings, hidden: !isElectron(), key: 'window' },
{ component: MediaSessionSettings, key: 'media-session' },
{ component: HotkeyManagerSettings, key: 'hotkey-manager' },
];
export const HotkeysTab = () => {
return (
<Stack gap="md">
{isElectron() && <WindowHotkeySettings />}
<HotkeyManagerSettings />
{sections.map(({ component: Section, hidden, key }, index) => (
<Fragment key={key}>
{!hidden && <Section />}
{index < sections.length - 1 && <Divider />}
</Fragment>
))}
</Stack>
);
};
@@ -0,0 +1,48 @@
import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next';
import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import { Switch } from '/@/shared/components/switch/switch';
import { PlayerType } from '/@/shared/types/types';
const isWindows = isElectron() ? window.api.utils.isWindows() : null;
const isDesktop = isElectron();
const ipc = isElectron() ? window.api.ipc : null;
export const MediaSessionSettings = () => {
const { t } = useTranslation();
const { mediaSession, type: playbackType } = usePlaybackSettings();
const { toggleMediaSession } = useSettingsStoreActions();
function handleMediaSessionChange() {
const current = mediaSession;
toggleMediaSession();
ipc?.send('settings-set', { property: 'mediaSession', value: !current });
}
const mediaSessionOptions: SettingOption[] = [
{
control: (
<Switch
aria-label="Toggle media Session"
defaultChecked={mediaSession}
disabled={!isWindows || !isDesktop || playbackType !== PlayerType.WEB}
onChange={handleMediaSessionChange}
/>
),
description: t('setting.mediaSession', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: !isWindows || !isDesktop,
note: t('common.restartRequired', { postProcess: 'sentenceCase' }),
title: t('setting.mediaSession', { postProcess: 'sentenceCase' }),
},
];
return <SettingsSection options={mediaSessionOptions} />;
};