mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-14 12:30:06 +02:00
add list filter collections
This commit is contained in:
@@ -47,10 +47,18 @@ export const ListFiltersModal = ({ isActive, itemType }: ListFiltersProps) => {
|
||||
|
||||
const [isOpen, handlers] = useDisclosure(false);
|
||||
|
||||
const albumListFilters = useAlbumListFilters(pageKey as ItemListKey);
|
||||
const songListFilters = useSongListFilters(pageKey as ItemListKey);
|
||||
const clear = itemType === LibraryItem.ALBUM ? albumListFilters.clear : songListFilters.clear;
|
||||
|
||||
const handlePin = () => {
|
||||
setIsSidebarOpen?.(!isSidebarOpen);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
clear();
|
||||
};
|
||||
|
||||
const canPin = Boolean(setIsSidebarOpen);
|
||||
|
||||
const disableArtistFilter = pageKey === ItemListKey.ALBUM_ARTIST_ALBUM;
|
||||
@@ -72,15 +80,21 @@ export const ListFiltersModal = ({ isActive, itemType }: ListFiltersProps) => {
|
||||
},
|
||||
}}
|
||||
title={
|
||||
<Group>
|
||||
{canPin && (
|
||||
<ActionIcon
|
||||
icon={isSidebarOpen ? 'unpin' : 'pin'}
|
||||
onClick={handlePin}
|
||||
variant="subtle"
|
||||
/>
|
||||
)}
|
||||
{t('common.filters', { postProcess: 'sentenceCase' })}
|
||||
<Group justify="space-between" style={{ paddingRight: '3rem', width: '100%' }}>
|
||||
<Group>
|
||||
{canPin && (
|
||||
<ActionIcon
|
||||
icon={isSidebarOpen ? 'unpin' : 'pin'}
|
||||
onClick={handlePin}
|
||||
variant="subtle"
|
||||
/>
|
||||
)}
|
||||
|
||||
{t('common.filters', { postProcess: 'sentenceCase' })}
|
||||
</Group>
|
||||
<Button onClick={handleReset} size="compact-sm" variant="subtle">
|
||||
{t('common.reset', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
.list {
|
||||
flex: 0 0 200px;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--theme-colors-border);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: var(--theme-spacing-xs);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams } from 'react-router';
|
||||
|
||||
import styles from './save-as-collection-button.module.css';
|
||||
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { useCollections, useSettingsStoreActions } from '/@/renderer/store';
|
||||
import { getFilterQueryStringFromSearchParams } from '/@/renderer/utils/query-params';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Popover } from '/@/shared/components/popover/popover';
|
||||
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
||||
import { useForm } from '/@/shared/hooks/use-form';
|
||||
import { LibraryItem, SavedCollection } from '/@/shared/types/domain-types';
|
||||
|
||||
interface SaveAsCollectionButtonProps {
|
||||
fullWidth?: boolean;
|
||||
itemType: LibraryItem.ALBUM | LibraryItem.SONG;
|
||||
}
|
||||
|
||||
export const SaveAsCollectionButton = ({ fullWidth, itemType }: SaveAsCollectionButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { customFilters } = useListContext();
|
||||
const collections = useCollections();
|
||||
const { addCollection, updateCollection } = useSettingsStoreActions();
|
||||
const [isOpen, handlers] = useDisclosure(false);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
const sameTypeCollections = useMemo(
|
||||
() => collections?.filter((c): c is SavedCollection => c.type === itemType) ?? [],
|
||||
[collections, itemType],
|
||||
);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
form.setValues({ name: '' });
|
||||
handlers.open();
|
||||
}, [form, handlers]);
|
||||
|
||||
const handleOverrideExisting = useCallback(
|
||||
(collection: SavedCollection) => {
|
||||
const filterQueryString = getFilterQueryStringFromSearchParams(
|
||||
searchParams,
|
||||
customFilters as Record<
|
||||
string,
|
||||
boolean | number | Record<string, unknown> | string | string[]
|
||||
>,
|
||||
);
|
||||
updateCollection(collection.id, { filterQueryString });
|
||||
handlers.close();
|
||||
},
|
||||
[customFilters, handlers, searchParams, updateCollection],
|
||||
);
|
||||
|
||||
const handleSubmit = form.onSubmit((values) => {
|
||||
const trimmed = values.name.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
const filterQueryString = getFilterQueryStringFromSearchParams(
|
||||
searchParams,
|
||||
customFilters as Record<
|
||||
string,
|
||||
boolean | number | Record<string, unknown> | string | string[]
|
||||
>,
|
||||
);
|
||||
|
||||
addCollection({
|
||||
filterQueryString,
|
||||
id: nanoid(),
|
||||
name: trimmed,
|
||||
type: itemType,
|
||||
});
|
||||
handlers.close();
|
||||
});
|
||||
|
||||
const handleFormKeyDown = useCallback((e: React.KeyboardEvent<HTMLFormElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
formRef.current?.requestSubmit();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Popover onClose={handlers.close} opened={isOpen} position="bottom-start" width={320}>
|
||||
<Popover.Target>
|
||||
{fullWidth ? (
|
||||
<Button fullWidth onClick={handleOpen} variant="default">
|
||||
{t('page.collections.saveAsCollection', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Button>
|
||||
) : (
|
||||
<ActionIcon
|
||||
icon="folder"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handleOpen}
|
||||
tooltip={{
|
||||
label: t('page.collections.saveAsCollection', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
}}
|
||||
variant="subtle"
|
||||
/>
|
||||
)}
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<form onKeyDown={handleFormKeyDown} onSubmit={handleSubmit} ref={formRef}>
|
||||
<Stack gap="sm">
|
||||
<Text fw={500} size="sm" ta="center">
|
||||
{t('page.collections.overrideExisting', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Text>
|
||||
<div className={styles.list}>
|
||||
<ScrollArea>
|
||||
<Stack gap={0}>
|
||||
{sameTypeCollections.map((collection) => (
|
||||
<Button
|
||||
className={styles.row}
|
||||
key={collection.id}
|
||||
onClick={() => handleOverrideExisting(collection)}
|
||||
type="button"
|
||||
variant="subtle"
|
||||
>
|
||||
<Text className={styles['row-name']} size="sm">
|
||||
{collection.name}
|
||||
</Text>
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
<TextInput autoFocus maxLength={128} {...form.getInputProps('name')} />
|
||||
<Group gap="xs" justify="flex-end">
|
||||
<Button onClick={handlers.close} type="button" variant="subtle">
|
||||
{t('common.cancel', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
<Button type="submit" variant="filled">
|
||||
{t('common.save', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user