use searchparams, localstorage for list filters

This commit is contained in:
jeffvli
2025-10-10 18:36:59 -07:00
parent ac625a9032
commit f7f1d5f54d
4 changed files with 345 additions and 0 deletions
@@ -0,0 +1,65 @@
import { useLocalStorage } from '@mantine/hooks';
import { useQuery } from '@tanstack/react-query';
import { parseAsString, useQueryState } from 'nuqs';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { FolderButton } from '/@/renderer/features/shared/components/folder-button';
import { useCurrentServer } from '/@/renderer/store';
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
import { ItemListKey } from '/@/shared/types/types';
interface ListMusicFolderDropdownProps {
listKey: ItemListKey;
}
export const ListMusicFolderDropdown = ({ listKey }: ListMusicFolderDropdownProps) => {
const server = useCurrentServer();
const { data: musicFolders } = useQuery(
sharedQueries.musicFolders({ query: null, serverId: server.id }),
);
const [persisted, setPersisted] = useLocalStorage({
defaultValue: '',
key: getPersistenceKey(listKey),
});
const [musicFolderId, setMusicFolderId] = useQueryState(
getPersistenceKey(listKey),
parseAsString.withDefault(persisted || ''),
);
const handleSetMusicFolder = (e: string) => {
if (e === musicFolderId) {
setMusicFolderId('');
setPersisted('');
return;
}
setMusicFolderId(e);
setPersisted(e);
};
return (
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<FolderButton isActive={!!musicFolderId} />
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
{musicFolders?.items.map((folder) => (
<DropdownMenu.Item
isSelected={musicFolderId === folder.id}
key={`musicFolder-${folder.id}`}
onClick={() => handleSetMusicFolder(folder.id)}
value={folder.id}
>
{folder.name}
</DropdownMenu.Item>
))}
</DropdownMenu.Dropdown>
</DropdownMenu>
);
};
const getPersistenceKey = (listKey: ItemListKey) => {
return `list-${listKey}-musicFolder`;
};
@@ -0,0 +1,17 @@
import { useCallback } from 'react';
import { eventEmitter } from '/@/renderer/events/event-emitter';
import { RefreshButton } from '/@/renderer/features/shared/components/refresh-button';
import { ItemListKey } from '/@/shared/types/types';
interface ListRefreshButtonProps {
listKey: ItemListKey;
}
export const ListRefreshButton = ({ listKey }: ListRefreshButtonProps) => {
const handleRefresh = useCallback(() => {
eventEmitter.emit('ITEM_LIST_REFRESH', { key: listKey });
}, [listKey]);
return <RefreshButton onClick={handleRefresh} />;
};
@@ -0,0 +1,226 @@
import { useLocalStorage } from '@mantine/hooks';
import { parseAsString, useQueryState } from 'nuqs';
import i18n from '/@/i18n/i18n';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { useCurrentServer } from '/@/renderer/store';
import { Button } from '/@/shared/components/button/button';
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
import { AlbumListSort, LibraryItem, ServerType, SortOrder } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
interface ListSortByDropdownProps {
defaultSortByValue: string;
itemType: LibraryItem;
listKey: ItemListKey;
onChange?: (value: string) => void;
target?: React.ReactNode;
}
export const ListSortByDropdown = ({
defaultSortByValue,
itemType,
listKey,
onChange,
target,
}: ListSortByDropdownProps) => {
const server = useCurrentServer();
const [persisted, setPersisted] = useLocalStorage({
defaultValue: defaultSortByValue,
key: getPersistenceKey(listKey),
});
const [sortBy, setSortBy] = useQueryState(
FILTER_KEYS.SORT_BY,
parseAsString.withDefault(persisted || defaultSortByValue),
);
const sortByLabel =
(itemType && FILTERS[itemType][server.type].find((f) => f.value === sortBy)?.name) || '—';
const handleSortByChange = (sortBy: string) => {
setSortBy(sortBy);
setPersisted(sortBy);
onChange?.(sortBy);
};
return (
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
{target ? target : <Button variant="subtle">{sortByLabel}</Button>}
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
{FILTERS[itemType][server.type].map((f) => (
<DropdownMenu.Item
isSelected={f.value === sortBy}
key={`filter-${f.name}`}
onClick={() => handleSortByChange(f.value)}
value={f.value}
>
{f.name}
</DropdownMenu.Item>
))}
</DropdownMenu.Dropdown>
</DropdownMenu>
);
};
const ALBUM_LIST_FILTERS: Partial<
Record<ServerType, Array<{ defaultOrder: SortOrder; name: string; value: string }>>
> = {
[ServerType.JELLYFIN]: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
value: AlbumListSort.ALBUM_ARTIST,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.communityRating', { postProcess: 'titleCase' }),
value: AlbumListSort.COMMUNITY_RATING,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.criticRating', { postProcess: 'titleCase' }),
value: AlbumListSort.CRITIC_RATING,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumListSort.NAME,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.playCount', { postProcess: 'titleCase' }),
value: AlbumListSort.PLAY_COUNT,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: AlbumListSort.RANDOM,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_ADDED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.releaseDate', { postProcess: 'titleCase' }),
value: AlbumListSort.RELEASE_DATE,
},
],
[ServerType.NAVIDROME]: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
value: AlbumListSort.ALBUM_ARTIST,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
value: AlbumListSort.ARTIST,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
value: AlbumListSort.DURATION,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
value: AlbumListSort.PLAY_COUNT,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumListSort.NAME,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: AlbumListSort.RANDOM,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
value: AlbumListSort.RATING,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_ADDED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_PLAYED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
value: AlbumListSort.SONG_COUNT,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.favorited', { postProcess: 'titleCase' }),
value: AlbumListSort.FAVORITED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
value: AlbumListSort.YEAR,
},
],
[ServerType.SUBSONIC]: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
value: AlbumListSort.ALBUM_ARTIST,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
value: AlbumListSort.PLAY_COUNT,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: AlbumListSort.NAME,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: AlbumListSort.RANDOM,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_ADDED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
value: AlbumListSort.RECENTLY_PLAYED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.favorited', { postProcess: 'titleCase' }),
value: AlbumListSort.FAVORITED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
value: AlbumListSort.YEAR,
},
],
};
const FILTERS: Partial<Record<LibraryItem, any>> = {
[LibraryItem.ALBUM]: ALBUM_LIST_FILTERS,
};
const getPersistenceKey = (listKey: ItemListKey) => {
return `item_list_${listKey}-${FILTER_KEYS.SORT_BY}`;
};
@@ -0,0 +1,37 @@
import { useLocalStorage } from '@mantine/hooks';
import { parseAsString, useQueryState } from 'nuqs';
import { OrderToggleButton } from '/@/renderer/features/shared/components/order-toggle-button';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { SortOrder } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
interface ListSortOrderToggleButtonProps {
listKey: ItemListKey;
}
export const ListSortOrderToggleButton = ({ listKey }: ListSortOrderToggleButtonProps) => {
const [persisted, setPersisted] = useLocalStorage({
defaultValue: SortOrder.ASC,
key: getPersistenceKey(listKey),
});
const [sortOrder, setSortOrder] = useQueryState(
FILTER_KEYS.SORT_ORDER,
parseAsString.withDefault(persisted || SortOrder.ASC),
);
const handleToggleSortOrder = () => {
const newSortOrder = sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
setSortOrder(newSortOrder);
setPersisted(newSortOrder);
};
return (
<OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={sortOrder as SortOrder} />
);
};
const getPersistenceKey = (listKey: ItemListKey) => {
return `item_list_${listKey}-${FILTER_KEYS.SORT_ORDER}`;
};