From f7f1d5f54d738c0df2ffb970f2feb0efa1a96d6e Mon Sep 17 00:00:00 2001 From: jeffvli Date: Fri, 10 Oct 2025 18:36:59 -0700 Subject: [PATCH] use searchparams, localstorage for list filters --- .../components/list-music-folder-dropdown.tsx | 65 +++++ .../shared/components/list-refresh-button.tsx | 17 ++ .../components/list-sort-by-dropdown.tsx | 226 ++++++++++++++++++ .../list-sort-order-toggle-button.tsx | 37 +++ 4 files changed, 345 insertions(+) create mode 100644 src/renderer/features/shared/components/list-music-folder-dropdown.tsx create mode 100644 src/renderer/features/shared/components/list-refresh-button.tsx create mode 100644 src/renderer/features/shared/components/list-sort-by-dropdown.tsx create mode 100644 src/renderer/features/shared/components/list-sort-order-toggle-button.tsx diff --git a/src/renderer/features/shared/components/list-music-folder-dropdown.tsx b/src/renderer/features/shared/components/list-music-folder-dropdown.tsx new file mode 100644 index 000000000..b0bb5bd14 --- /dev/null +++ b/src/renderer/features/shared/components/list-music-folder-dropdown.tsx @@ -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 ( + + + + + + {musicFolders?.items.map((folder) => ( + handleSetMusicFolder(folder.id)} + value={folder.id} + > + {folder.name} + + ))} + + + ); +}; + +const getPersistenceKey = (listKey: ItemListKey) => { + return `list-${listKey}-musicFolder`; +}; diff --git a/src/renderer/features/shared/components/list-refresh-button.tsx b/src/renderer/features/shared/components/list-refresh-button.tsx new file mode 100644 index 000000000..e7d656b99 --- /dev/null +++ b/src/renderer/features/shared/components/list-refresh-button.tsx @@ -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 ; +}; diff --git a/src/renderer/features/shared/components/list-sort-by-dropdown.tsx b/src/renderer/features/shared/components/list-sort-by-dropdown.tsx new file mode 100644 index 000000000..d9196084f --- /dev/null +++ b/src/renderer/features/shared/components/list-sort-by-dropdown.tsx @@ -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 ( + + + {target ? target : } + + + {FILTERS[itemType][server.type].map((f) => ( + handleSortByChange(f.value)} + value={f.value} + > + {f.name} + + ))} + + + ); +}; + +const ALBUM_LIST_FILTERS: Partial< + Record> +> = { + [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> = { + [LibraryItem.ALBUM]: ALBUM_LIST_FILTERS, +}; + +const getPersistenceKey = (listKey: ItemListKey) => { + return `item_list_${listKey}-${FILTER_KEYS.SORT_BY}`; +}; diff --git a/src/renderer/features/shared/components/list-sort-order-toggle-button.tsx b/src/renderer/features/shared/components/list-sort-order-toggle-button.tsx new file mode 100644 index 000000000..84d3e7232 --- /dev/null +++ b/src/renderer/features/shared/components/list-sort-order-toggle-button.tsx @@ -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 ( + + ); +}; + +const getPersistenceKey = (listKey: ItemListKey) => { + return `item_list_${listKey}-${FILTER_KEYS.SORT_ORDER}`; +};