[enhancement]: support serach on settings page

This commit is contained in:
Kendall Garner 2024-05-05 13:25:05 -07:00
parent 683bb0222c
commit 645697367d
No known key found for this signature in database
GPG Key ID: 18D2767419676C87
21 changed files with 439 additions and 385 deletions

View File

@ -2,7 +2,7 @@ root = true
[*] [*]
indent_style = space indent_style = space
indent_size = 2 indent_size = 4
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true

View File

@ -0,0 +1,126 @@
import isEqual from 'lodash/isEqual';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SortableItem } from '/@/renderer/store';
import { useSettingSearchContext } from '/@/renderer/features/settings/context/search-context';
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
import { Button } from '/@/renderer/components';
import { Reorder } from 'framer-motion';
import { Divider } from '@mantine/core';
import { DraggableItem } from '/@/renderer/features/settings/components/general/draggable-item';
export type DraggableItemsProps<K, T> = {
description: string;
itemLabels: Array<[K, string]>;
setItems: (items: T[]) => void;
settings: T[];
title: string;
};
export const DraggableItems = <K extends string, T extends SortableItem<K>>({
description,
itemLabels,
settings,
setItems,
title,
}: DraggableItemsProps<K, T>) => {
const { t } = useTranslation();
const keyword = useSettingSearchContext();
const [open, setOpen] = useState(false);
const translatedItemMap = useMemo(
() =>
Object.fromEntries(
itemLabels.map((label) => [label[0], t(label[1], { postProcess: 'sentenceCase' })]),
) as Record<K, string>,
[itemLabels, t],
);
const [localItems, setLocalItems] = useState(settings);
const handleChangeDisabled = useCallback((id: string, e: boolean) => {
setLocalItems((items) =>
items.map((item) => {
if (item.id === id) {
return {
...item,
disabled: !e,
};
}
return item;
}),
);
}, []);
const titleText = t(title, { postProcess: 'sentenceCase' });
const descriptionText = t(description, {
context: 'description',
postProcess: 'sentenceCase',
});
const shouldShow = useMemo(() => {
return (
keyword === '' ||
title.toLocaleLowerCase().includes(keyword) ||
description.toLocaleLowerCase().includes(keyword)
);
}, [description, keyword, title]);
if (!shouldShow) {
return <></>;
}
const isSaveButtonDisabled = isEqual(settings, localItems);
const handleSave = () => {
setItems(localItems);
};
return (
<>
<SettingsOptions
control={
<>
{open && (
<Button
compact
disabled={isSaveButtonDisabled}
variant="filled"
onClick={handleSave}
>
{t('common.save', { postProcess: 'titleCase' })}
</Button>
)}
<Button
compact
variant="filled"
onClick={() => setOpen(!open)}
>
{t(open ? 'common.close' : 'common.edit', { postProcess: 'titleCase' })}
</Button>
</>
}
description={descriptionText}
title={titleText}
/>
{open && (
<Reorder.Group
axis="y"
values={localItems}
onReorder={setLocalItems}
>
{localItems.map((item) => (
<DraggableItem
key={item.id}
handleChangeDisabled={handleChangeDisabled}
item={item}
value={translatedItemMap[item.id]}
/>
))}
</Reorder.Group>
)}
<Divider />
</>
);
};

View File

@ -1,4 +1,4 @@
import { Divider, Stack } from '@mantine/core'; import { Stack } from '@mantine/core';
import { ApplicationSettings } from '/@/renderer/features/settings/components/general/application-settings'; import { ApplicationSettings } from '/@/renderer/features/settings/components/general/application-settings';
import { ControlSettings } from '/@/renderer/features/settings/components/general/control-settings'; import { ControlSettings } from '/@/renderer/features/settings/components/general/control-settings';
import { SidebarSettings } from '/@/renderer/features/settings/components/general/sidebar-settings'; import { SidebarSettings } from '/@/renderer/features/settings/components/general/sidebar-settings';
@ -7,26 +7,18 @@ import { RemoteSettings } from '/@/renderer/features/settings/components/general
import { CacheSettings } from '/@/renderer/features/settings/components/window/cache-settngs'; import { CacheSettings } from '/@/renderer/features/settings/components/window/cache-settngs';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { HomeSettings } from '/@/renderer/features/settings/components/general/home-settings'; import { HomeSettings } from '/@/renderer/features/settings/components/general/home-settings';
import { SidebarReorder } from '/@/renderer/features/settings/components/general/sidebar-reorder';
export const GeneralTab = () => { export const GeneralTab = () => {
return ( return (
<Stack spacing="md"> <Stack spacing="md">
<ApplicationSettings /> <ApplicationSettings />
<Divider />
<ThemeSettings /> <ThemeSettings />
<Divider />
<ControlSettings /> <ControlSettings />
<Divider />
<HomeSettings /> <HomeSettings />
<Divider /> <SidebarReorder />
<SidebarSettings /> <SidebarSettings />
{isElectron() && ( {isElectron() && <RemoteSettings />}
<>
<Divider />
<RemoteSettings />
</>
)}
<Divider />
<CacheSettings /> <CacheSettings />
</Stack> </Stack>
); );

View File

@ -1,107 +1,28 @@
import { useCallback, useMemo, useState } from 'react';
import { Reorder } from 'framer-motion';
import isEqual from 'lodash/isEqual';
import { useTranslation } from 'react-i18next';
import { Button } from '/@/renderer/components';
import { import {
useSettingsStoreActions, useSettingsStoreActions,
useGeneralSettings, useGeneralSettings,
HomeItem, HomeItem,
} from '../../../../store/settings.store'; } from '../../../../store/settings.store';
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option'; import { DraggableItems } from '/@/renderer/features/settings/components/general/draggable-items';
import { DraggableItem } from '/@/renderer/features/settings/components/general/draggable-item';
const HOME_ITEMS: Array<[string, string]> = [
[HomeItem.RANDOM, 'page.home.explore'],
[HomeItem.RECENTLY_PLAYED, 'page.home.recentlyPlayed'],
[HomeItem.RECENTLY_ADDED, 'page.home.newlyAdded'],
[HomeItem.MOST_PLAYED, 'page.home.mostPlayed'],
];
export const HomeSettings = () => { export const HomeSettings = () => {
const { t } = useTranslation();
const { homeItems } = useGeneralSettings(); const { homeItems } = useGeneralSettings();
const { setHomeItems } = useSettingsStoreActions(); const { setHomeItems } = useSettingsStoreActions();
const [open, setOpen] = useState(false);
const translatedSidebarItemMap = useMemo(
() => ({
[HomeItem.RANDOM]: t('page.home.explore', { postProcess: 'sentenceCase' }),
[HomeItem.RECENTLY_PLAYED]: t('page.home.recentlyPlayed', {
postProcess: 'sentenceCase',
}),
[HomeItem.RECENTLY_ADDED]: t('page.home.newlyAdded', { postProcess: 'sentenceCase' }),
[HomeItem.MOST_PLAYED]: t('page.home.mostPlayed', { postProcess: 'sentenceCase' }),
}),
[t],
);
const [localHomeItems, setLocalHomeItems] = useState(homeItems);
const handleSave = () => {
setHomeItems(localHomeItems);
};
const handleChangeDisabled = useCallback((id: string, e: boolean) => {
setLocalHomeItems((items) =>
items.map((item) => {
if (item.id === id) {
return {
...item,
disabled: !e,
};
}
return item;
}),
);
}, []);
const isSaveButtonDisabled = isEqual(homeItems, localHomeItems);
return ( return (
<> <DraggableItems
<SettingsOptions description="setting.homeConfiguration"
control={ itemLabels={HOME_ITEMS}
<> setItems={setHomeItems}
{open && ( settings={homeItems}
<Button title="setting.homeConfiguration"
compact
disabled={isSaveButtonDisabled}
variant="filled"
onClick={handleSave}
>
{t('common.save', { postProcess: 'titleCase' })}
</Button>
)}
<Button
compact
variant="filled"
onClick={() => setOpen(!open)}
>
{t(open ? 'common.close' : 'common.edit', { postProcess: 'titleCase' })}
</Button>
</>
}
description={t('setting.homeConfiguration', {
context: 'description',
postProcess: 'sentenceCase',
})}
title={t('setting.homeConfiguration', { postProcess: 'sentenceCase' })}
/> />
{open && (
<Reorder.Group
axis="y"
values={localHomeItems}
onReorder={setLocalHomeItems}
>
{localHomeItems.map((item) => (
<DraggableItem
key={item.id}
handleChangeDisabled={handleChangeDisabled}
item={item}
value={
translatedSidebarItemMap[
item.id as keyof typeof translatedSidebarItemMap
]
}
/>
))}
</Reorder.Group>
)}
</>
); );
}; };

View File

@ -0,0 +1,30 @@
import { DraggableItems } from '/@/renderer/features/settings/components/general/draggable-items';
import { useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store';
const SIDEBAR_ITEMS: Array<[string, string]> = [
['Albums', 'page.sidebar.albums'],
['Artists', 'page.sidebar.artists'],
['Folders', 'page.sidebar.folders'],
['Genres', 'page.sidebar.genres'],
['Home', 'page.sidebar.home'],
['Now Playing', 'page.sidebar.nowPlaying'],
['Playlists', 'page.sidebar.playlists'],
['Search', 'page.sidebar.search'],
['Settings', 'page.sidebar.settings'],
['Tracks', 'page.sidebar.tracks'],
];
export const SidebarReorder = () => {
const { sidebarItems } = useGeneralSettings();
const { setSidebarItems } = useSettingsStoreActions();
return (
<DraggableItems
description="setting.sidebarCollapsedNavigation"
itemLabels={SIDEBAR_ITEMS}
setItems={setSidebarItems}
settings={sidebarItems}
title="setting.sidebarConfiguration"
/>
);
};

View File

@ -1,54 +1,16 @@
import { ChangeEvent, useCallback, useMemo, useState } from 'react'; import { ChangeEvent } from 'react';
import { Reorder } from 'framer-motion';
import isEqual from 'lodash/isEqual';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Switch } from '/@/renderer/components'; import { Switch } from '/@/renderer/components';
import { useSettingsStoreActions, useGeneralSettings } from '../../../../store/settings.store'; import { useSettingsStoreActions, useGeneralSettings } from '../../../../store/settings.store';
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option'; import {
import { DraggableItem } from '/@/renderer/features/settings/components/general/draggable-item'; SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
export const SidebarSettings = () => { export const SidebarSettings = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const settings = useGeneralSettings(); const settings = useGeneralSettings();
const { setSidebarItems, setSettings } = useSettingsStoreActions(); const { setSettings } = useSettingsStoreActions();
const [open, setOpen] = useState(false);
const translatedSidebarItemMap = useMemo(
() => ({
Albums: t('page.sidebar.albums', { postProcess: 'titleCase' }),
Artists: t('page.sidebar.artists', { postProcess: 'titleCase' }),
Folders: t('page.sidebar.folders', { postProcess: 'titleCase' }),
Genres: t('page.sidebar.genres', { postProcess: 'titleCase' }),
Home: t('page.sidebar.home', { postProcess: 'titleCase' }),
'Now Playing': t('page.sidebar.nowPlaying', { postProcess: 'titleCase' }),
Playlists: t('page.sidebar.playlists', { postProcess: 'titleCase' }),
Search: t('page.sidebar.search', { postProcess: 'titleCase' }),
Settings: t('page.sidebar.settings', { postProcess: 'titleCase' }),
Tracks: t('page.sidebar.tracks', { postProcess: 'titleCase' }),
}),
[t],
);
const [localSidebarItems, setLocalSidebarItems] = useState(settings.sidebarItems);
const handleSave = () => {
setSidebarItems(localSidebarItems);
};
const handleChangeDisabled = useCallback((id: string, e: boolean) => {
setLocalSidebarItems((items) =>
items.map((item) => {
if (item.id === id) {
return {
...item,
disabled: !e,
};
}
return item;
}),
);
}, []);
const handleSetSidebarPlaylistList = (e: ChangeEvent<HTMLInputElement>) => { const handleSetSidebarPlaylistList = (e: ChangeEvent<HTMLInputElement>) => {
setSettings({ setSettings({
@ -68,84 +30,34 @@ export const SidebarSettings = () => {
}); });
}; };
const isSaveButtonDisabled = isEqual(settings.sidebarItems, localSidebarItems); const options: SettingOption[] = [
{
return ( control: (
<>
<SettingsOptions
control={
<Switch <Switch
checked={settings.sidebarPlaylistList} checked={settings.sidebarPlaylistList}
onChange={handleSetSidebarPlaylistList} onChange={handleSetSidebarPlaylistList}
/> />
} ),
description={t('setting.sidebarPlaylistList', { description: t('setting.sidebarPlaylistList', {
context: 'description', context: 'description',
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
})} }),
title={t('setting.sidebarPlaylistList', { postProcess: 'sentenceCase' })} title: t('setting.sidebarPlaylistList', { postProcess: 'sentenceCase' }),
/> },
<SettingsOptions {
control={ control: (
<Switch <Switch
checked={settings.sidebarCollapsedNavigation} checked={settings.sidebarCollapsedNavigation}
onChange={handleSetSidebarCollapsedNavigation} onChange={handleSetSidebarCollapsedNavigation}
/> />
} ),
description={t('setting.sidebarPlaylistList', { description: t('setting.sidebarPlaylistList', {
context: 'description', context: 'description',
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
})} }),
title={t('setting.sidebarCollapsedNavigation', { postProcess: 'sentenceCase' })} title: t('setting.sidebarCollapsedNavigation', { postProcess: 'sentenceCase' }),
/> },
<SettingsOptions ];
control={
<> return <SettingsSection options={options} />;
{open && (
<Button
compact
disabled={isSaveButtonDisabled}
variant="filled"
onClick={handleSave}
>
{t('common.save', { postProcess: 'titleCase' })}
</Button>
)}
<Button
compact
variant="filled"
onClick={() => setOpen(!open)}
>
{t(open ? 'common.close' : 'common.edit', { postProcess: 'titleCase' })}
</Button>
</>
}
description={t('setting.sidebarCollapsedNavigation', {
context: 'description',
postProcess: 'sentenceCase',
})}
title={t('setting.sidebarConfiguration', { postProcess: 'sentenceCase' })}
/>
{open && (
<Reorder.Group
axis="y"
values={localSidebarItems}
onReorder={setLocalSidebarItems}
>
{localSidebarItems.map((item) => (
<DraggableItem
key={item.id}
handleChangeDisabled={handleChangeDisabled}
item={item}
value={
translatedSidebarItemMap[
item.id as keyof typeof translatedSidebarItemMap
]
}
/>
))}
</Reorder.Group>
)}
</>
);
}; };

View File

@ -9,6 +9,7 @@ import { Button, TextInput, Checkbox } from '/@/renderer/components';
import { BindingActions, useHotkeySettings, useSettingsStoreActions } from '/@/renderer/store'; import { BindingActions, useHotkeySettings, useSettingsStoreActions } from '/@/renderer/store';
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option'; import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
import { useSettingSearchContext } from '/@/renderer/features/settings/context/search-context';
const ipc = isElectron() ? window.electron.ipc : null; const ipc = isElectron() ? window.electron.ipc : null;
@ -107,6 +108,7 @@ export const HotkeyManagerSettings = () => {
const { bindings, globalMediaHotkeys } = useHotkeySettings(); const { bindings, globalMediaHotkeys } = useHotkeySettings();
const { setSettings } = useSettingsStoreActions(); const { setSettings } = useSettingsStoreActions();
const [selected, setSelected] = useState<BindingActions | null>(null); const [selected, setSelected] = useState<BindingActions | null>(null);
const keyword = useSettingSearchContext();
const debouncedSetHotkey = debounce( const debouncedSetHotkey = debounce(
(binding: BindingActions, e: KeyboardEvent<HTMLInputElement>) => { (binding: BindingActions, e: KeyboardEvent<HTMLInputElement>) => {
@ -216,6 +218,21 @@ export const HotkeyManagerSettings = () => {
return duplicateKeys; return duplicateKeys;
}, [bindings]); }, [bindings]);
const filteredBindings = useMemo(() => {
const base = Object.keys(bindings);
if (keyword === '') {
return base.filter((binding) => BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]);
}
return base.filter((binding) => {
const item = BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP];
if (!item) return false;
return item.toLocaleLowerCase().includes(keyword);
});
}, [bindings, keyword]);
return ( return (
<> <>
<SettingsOptions <SettingsOptions
@ -227,9 +244,7 @@ export const HotkeyManagerSettings = () => {
title={t('setting.applicationHotkeys', { postProcess: 'sentenceCase' })} title={t('setting.applicationHotkeys', { postProcess: 'sentenceCase' })}
/> />
<HotkeysContainer> <HotkeysContainer>
{Object.keys(bindings) {filteredBindings.map((binding) => (
.filter((binding) => BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP])
.map((binding) => (
<Group <Group
key={`hotkey-${binding}`} key={`hotkey-${binding}`}
noWrap noWrap
@ -261,9 +276,7 @@ export const HotkeyManagerSettings = () => {
/> />
{isElectron() && ( {isElectron() && (
<Checkbox <Checkbox
checked={ checked={bindings[binding as keyof typeof BINDINGS_MAP].isGlobal}
bindings[binding as keyof typeof BINDINGS_MAP].isGlobal
}
disabled={ disabled={
bindings[binding as keyof typeof BINDINGS_MAP].hotkey === '' bindings[binding as keyof typeof BINDINGS_MAP].hotkey === ''
} }

View File

@ -1,4 +1,4 @@
import { Divider, Stack } from '@mantine/core'; import { Stack } from '@mantine/core';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { WindowHotkeySettings } from './window-hotkey-settings'; import { WindowHotkeySettings } from './window-hotkey-settings';
import { HotkeyManagerSettings } from '/@/renderer/features/settings/components/hotkeys/hotkey-manager-settings'; import { HotkeyManagerSettings } from '/@/renderer/features/settings/components/hotkeys/hotkey-manager-settings';
@ -6,12 +6,7 @@ import { HotkeyManagerSettings } from '/@/renderer/features/settings/components/
export const HotkeysTab = () => { export const HotkeysTab = () => {
return ( return (
<Stack spacing="md"> <Stack spacing="md">
{isElectron() && ( {isElectron() && <WindowHotkeySettings />}
<>
<WindowHotkeySettings />
<Divider />
</>
)}
<HotkeyManagerSettings /> <HotkeyManagerSettings />
</Stack> </Stack>
); );

View File

@ -18,7 +18,7 @@ const getAudioDevice = async () => {
return (devices || []).filter((dev: MediaDeviceInfo) => dev.kind === 'audiooutput'); return (devices || []).filter((dev: MediaDeviceInfo) => dev.kind === 'audiooutput');
}; };
export const AudioSettings = () => { export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const settings = usePlaybackSettings(); const settings = usePlaybackSettings();
const { setSettings } = useSettingsStoreActions(); const { setSettings } = useSettingsStoreActions();
@ -201,5 +201,10 @@ export const AudioSettings = () => {
}, },
]; ];
return <SettingsSection options={audioOptions} />; return (
<SettingsSection
divider={!hasFancyAudio}
options={audioOptions}
/>
);
}; };

View File

@ -118,5 +118,10 @@ export const LyricSettings = () => {
}, },
]; ];
return <SettingsSection options={lyricOptions} />; return (
<SettingsSection
divider={false}
options={lyricOptions}
/>
);
}; };

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Divider, Group, Stack } from '@mantine/core'; import { Group, Stack } from '@mantine/core';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { import {
FileInput, FileInput,
@ -414,9 +414,7 @@ export const MpvSettings = () => {
return ( return (
<> <>
<SettingsSection options={options} /> <SettingsSection options={options} />
<Divider />
<SettingsSection options={generalOptions} /> <SettingsSection options={generalOptions} />
<Divider />
<SettingsSection options={replayGainOptions} /> <SettingsSection options={replayGainOptions} />
</> </>
); );

View File

@ -1,5 +1,5 @@
import { lazy, Suspense, useMemo } from 'react'; import { lazy, Suspense, useMemo } from 'react';
import { Divider, Stack } from '@mantine/core'; import { Stack } from '@mantine/core';
import { AudioSettings } from '/@/renderer/features/settings/components/playback/audio-settings'; import { AudioSettings } from '/@/renderer/features/settings/components/playback/audio-settings';
import { ScrobbleSettings } from '/@/renderer/features/settings/components/playback/scrobble-settings'; import { ScrobbleSettings } from '/@/renderer/features/settings/components/playback/scrobble-settings';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
@ -15,13 +15,12 @@ export const PlaybackTab = () => {
const hasFancyAudio = useMemo(() => { const hasFancyAudio = useMemo(() => {
return isElectron() || 'AudioContext' in window; return isElectron() || 'AudioContext' in window;
}, []); }, []);
return ( return (
<Stack spacing="md"> <Stack spacing="md">
<AudioSettings /> <AudioSettings hasFancyAudio={hasFancyAudio} />
<Suspense fallback={<></>}>{hasFancyAudio && <MpvSettings />}</Suspense> <Suspense fallback={<></>}>{hasFancyAudio && <MpvSettings />}</Suspense>
<Divider />
<ScrobbleSettings /> <ScrobbleSettings />
<Divider />
<LyricSettings /> <LyricSettings />
</Stack> </Stack>
); );

View File

@ -2,13 +2,23 @@ import { Flex, Group } from '@mantine/core';
import { closeAllModals, openModal } from '@mantine/modals'; import { closeAllModals, openModal } from '@mantine/modals';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RiSettings2Fill } from 'react-icons/ri'; import { RiSettings2Fill } from 'react-icons/ri';
import { Button, ConfirmModal, PageHeader } from '/@/renderer/components'; import { Button, ConfirmModal, PageHeader, SearchInput } from '/@/renderer/components';
import { LibraryHeaderBar } from '/@/renderer/features/shared'; import { LibraryHeaderBar } from '/@/renderer/features/shared';
import { useSettingsStoreActions } from '../../../store/settings.store'; import { useSettingsStoreActions } from '../../../store/settings.store';
import { useSettingSearchContext } from '/@/renderer/features/settings/context/search-context';
import { useContainerQuery } from '/@/renderer/hooks';
export const SettingsHeader = () => { export type SettingsHeaderProps = {
setSearch: (search: string) => void;
};
export const SettingsHeader = ({ setSearch }: SettingsHeaderProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { reset } = useSettingsStoreActions(); const { reset } = useSettingsStoreActions();
const search = useSettingSearchContext();
const cq = useContainerQuery();
console.log(cq);
const handleResetToDefault = () => { const handleResetToDefault = () => {
reset(); reset();
@ -27,7 +37,7 @@ export const SettingsHeader = () => {
}; };
return ( return (
<Flex> <Flex ref={cq.ref}>
<PageHeader> <PageHeader>
<LibraryHeaderBar> <LibraryHeaderBar>
<Flex <Flex
@ -41,6 +51,14 @@ export const SettingsHeader = () => {
{t('common.setting', { count: 2, postProcess: 'titleCase' })} {t('common.setting', { count: 2, postProcess: 'titleCase' })}
</LibraryHeaderBar.Title> </LibraryHeaderBar.Title>
</Group> </Group>
<Group>
<SearchInput
defaultValue={search}
openedWidth={cq.isMd ? 250 : cq.isSm ? 200 : 150}
onChange={(event) =>
setSearch(event.target.value.toLocaleLowerCase())
}
/>
<Button <Button
compact compact
variant="default" variant="default"
@ -48,6 +66,7 @@ export const SettingsHeader = () => {
> >
{t('common.resetToDefault', { postProcess: 'sentenceCase' })} {t('common.resetToDefault', { postProcess: 'sentenceCase' })}
</Button> </Button>
</Group>
</Flex> </Flex>
</LibraryHeaderBar> </LibraryHeaderBar>
</PageHeader> </PageHeader>

View File

@ -1,5 +1,7 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option'; import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
import { useSettingSearchContext } from '/@/renderer/features/settings/context/search-context';
import { Divider } from '@mantine/core';
export type SettingOption = { export type SettingOption = {
control: ReactNode; control: ReactNode;
@ -10,20 +12,27 @@ export type SettingOption = {
}; };
interface SettingsSectionProps { interface SettingsSectionProps {
divider?: boolean;
options: SettingOption[]; options: SettingOption[];
} }
export const SettingsSection = ({ options }: SettingsSectionProps) => { export const SettingsSection = ({ divider, options }: SettingsSectionProps) => {
const keyword = useSettingSearchContext();
const hasKeyword = keyword !== '';
const values = options.filter(
(o) => !o.isHidden && (!hasKeyword || o.title.toLocaleLowerCase().includes(keyword)),
);
return ( return (
<> <>
{options {values.map((option) => (
.filter((o) => !o.isHidden)
.map((option) => (
<SettingsOptions <SettingsOptions
key={`option-${option.title}`} key={`option-${option.title}`}
{...option} {...option}
/> />
))} ))}
{divider !== false && values.length > 0 && <Divider />}
</> </>
); );
}; };

View File

@ -3,8 +3,11 @@ import { useQueryClient } from '@tanstack/react-query';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, ConfirmModal, toast } from '/@/renderer/components'; import { Button, ConfirmModal, toast } from '/@/renderer/components';
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';
const browser = isElectron() ? window.electron.browser : null; const browser = isElectron() ? window.electron.browser : null;
@ -50,10 +53,9 @@ export const CacheSettings = () => {
}); });
}; };
return ( const options: SettingOption[] = [
<> {
<SettingsOptions control: (
control={
<Button <Button
compact compact
disabled={isClearing} disabled={isClearing}
@ -62,15 +64,14 @@ export const CacheSettings = () => {
> >
{t('common.clear', { postProcess: 'sentenceCase' })} {t('common.clear', { postProcess: 'sentenceCase' })}
</Button> </Button>
} ),
description={t('setting.clearQueryCache', { description: t('setting.clearQueryCache', {
context: 'description', context: 'description',
})} }),
title={t('setting.clearQueryCache')} title: t('setting.clearQueryCache'),
/> },
{browser && ( {
<SettingsOptions control: (
control={
<Button <Button
compact compact
disabled={isClearing} disabled={isClearing}
@ -79,13 +80,19 @@ export const CacheSettings = () => {
> >
{t('common.clear', { postProcess: 'sentenceCase' })} {t('common.clear', { postProcess: 'sentenceCase' })}
</Button> </Button>
} ),
description={t('setting.clearCache', { description: t('setting.clearCache', {
context: 'description', context: 'description',
})} }),
title={t('setting.clearCache')} isHidden: !browser,
title: t('setting.clearCache'),
},
];
return (
<SettingsSection
divider={false}
options={options}
/> />
)}
</>
); );
}; };

View File

@ -52,5 +52,10 @@ export const PasswordSettings = () => {
}, },
]; ];
return <SettingsSection options={updateOptions} />; return (
<SettingsSection
divider={false}
options={updateOptions}
/>
);
}; };

View File

@ -8,6 +8,7 @@ import {
import { Switch } from '/@/renderer/components'; import { Switch } from '/@/renderer/components';
const localSettings = isElectron() ? window.electron.localSettings : null; const localSettings = isElectron() ? window.electron.localSettings : null;
const utils = isElectron() ? window.electron.utils : null;
export const UpdateSettings = () => { export const UpdateSettings = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -42,5 +43,10 @@ export const UpdateSettings = () => {
}, },
]; ];
return <SettingsSection options={updateOptions} />; return (
<SettingsSection
divider={utils?.isLinux()}
options={updateOptions}
/>
);
}; };

View File

@ -1,4 +1,4 @@
import { Divider, Stack } from '@mantine/core'; import { Stack } from '@mantine/core';
import { UpdateSettings } from '/@/renderer/features/settings/components/window/update-settings'; import { UpdateSettings } from '/@/renderer/features/settings/components/window/update-settings';
import { WindowSettings } from '/@/renderer/features/settings/components/window/window-settings'; import { WindowSettings } from '/@/renderer/features/settings/components/window/window-settings';
import { DiscordSettings } from '/@/renderer/features/settings/components/window/discord-settings'; import { DiscordSettings } from '/@/renderer/features/settings/components/window/discord-settings';
@ -11,13 +11,10 @@ export const WindowTab = () => {
return ( return (
<Stack spacing="md"> <Stack spacing="md">
<WindowSettings /> <WindowSettings />
<Divider />
<DiscordSettings /> <DiscordSettings />
<Divider />
<UpdateSettings /> <UpdateSettings />
{utils?.isLinux() && ( {utils?.isLinux() && (
<> <>
<Divider />
<PasswordSettings /> <PasswordSettings />
</> </>
)} )}

View File

@ -0,0 +1,8 @@
import { createContext, useContext } from 'react';
export const SettingSearchContext = createContext<string>('');
export const useSettingSearchContext = () => {
const ctxValue = useContext(SettingSearchContext);
return ctxValue;
};

View File

@ -2,18 +2,24 @@ import { Flex } from '@mantine/core';
import { SettingsContent } from '/@/renderer/features/settings/components/settings-content'; import { SettingsContent } from '/@/renderer/features/settings/components/settings-content';
import { SettingsHeader } from '/@/renderer/features/settings/components/settings-header'; import { SettingsHeader } from '/@/renderer/features/settings/components/settings-header';
import { AnimatedPage } from '/@/renderer/features/shared'; import { AnimatedPage } from '/@/renderer/features/shared';
import { SettingSearchContext } from '/@/renderer/features/settings/context/search-context';
import { useState } from 'react';
const SettingsRoute = () => { const SettingsRoute = () => {
const [search, setSearch] = useState('');
return ( return (
<AnimatedPage> <AnimatedPage>
<SettingSearchContext.Provider value={search}>
<Flex <Flex
direction="column" direction="column"
h="100%" h="100%"
w="100%" w="100%"
> >
<SettingsHeader /> <SettingsHeader setSearch={setSearch} />
<SettingsContent /> <SettingsContent />
</Flex> </Flex>
</SettingSearchContext.Provider>
</AnimatedPage> </AnimatedPage>
); );
}; };

View File

@ -282,6 +282,7 @@ export interface SettingsSlice extends SettingsState {
actions: { actions: {
reset: () => void; reset: () => void;
resetSampleRate: () => void; resetSampleRate: () => void;
setGenreBehavior: (target: GenreTarget) => void;
setHomeItems: (item: SortableItem<HomeItem>[]) => void; setHomeItems: (item: SortableItem<HomeItem>[]) => void;
setSettings: (data: Partial<SettingsState>) => void; setSettings: (data: Partial<SettingsState>) => void;
setSidebarItems: (items: SidebarItemType[]) => void; setSidebarItems: (items: SidebarItemType[]) => void;