playlist sort and refactoring

This commit is contained in:
Kendall Garner 2025-10-05 19:13:35 -07:00
parent 1cbb3e56bc
commit 306167fee3
No known key found for this signature in database
GPG key ID: 9355F387FE765C94
3 changed files with 63 additions and 57 deletions

View file

@ -3,20 +3,20 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
import { closeAllModals, openModal } from '@mantine/modals'; import { closeAllModals, openModal } from '@mantine/modals';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { MouseEvent, MutableRefObject, useCallback } from 'react'; import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router'; import { useNavigate, useParams } from 'react-router';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form'; import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation'; import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query'; import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { OrderToggleButton } from '/@/renderer/features/shared'; import { OrderToggleButton } from '/@/renderer/features/shared';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu'; import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
import { MoreButton } from '/@/renderer/features/shared/components/more-button'; import { MoreButton } from '/@/renderer/features/shared/components/more-button';
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { import {
@ -37,13 +37,7 @@ import { Icon } from '/@/shared/components/icon/icon';
import { ConfirmModal } from '/@/shared/components/modal/modal'; import { ConfirmModal } from '/@/shared/components/modal/modal';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { toast } from '/@/shared/components/toast/toast'; import { toast } from '/@/shared/components/toast/toast';
import { import { ServerType, SongListSort, SortOrder } from '/@/shared/types/domain-types';
LibraryItem,
PlaylistSongListQueryClientSide,
ServerType,
SongListSort,
SortOrder,
} from '/@/shared/types/domain-types';
import { ListDisplayType, Play } from '/@/shared/types/types'; import { ListDisplayType, Play } from '/@/shared/types/types';
const FILTERS = { const FILTERS = {
@ -246,11 +240,13 @@ const FILTERS = {
}; };
interface PlaylistDetailSongListHeaderFiltersProps { interface PlaylistDetailSongListHeaderFiltersProps {
handlePlay: (playType: Play) => void;
handleToggleShowQueryBuilder: () => void; handleToggleShowQueryBuilder: () => void;
tableRef: MutableRefObject<AgGridReactType | null>; tableRef: MutableRefObject<AgGridReactType | null>;
} }
export const PlaylistDetailSongListHeaderFilters = ({ export const PlaylistDetailSongListHeaderFilters = ({
handlePlay,
handleToggleShowQueryBuilder, handleToggleShowQueryBuilder,
tableRef, tableRef,
}: PlaylistDetailSongListHeaderFiltersProps) => { }: PlaylistDetailSongListHeaderFiltersProps) => {
@ -262,16 +258,13 @@ export const PlaylistDetailSongListHeaderFilters = ({
const setPage = useSetPlaylistStore(); const setPage = useSetPlaylistStore();
const setFilter = useSetPlaylistDetailFilters(); const setFilter = useSetPlaylistDetailFilters();
const page = usePlaylistDetailStore(); const page = usePlaylistDetailStore();
const filters: Partial<PlaylistSongListQueryClientSide> = { const searchTerm = page?.table.id[playlistId]?.filter?.searchTerm;
sortBy: page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID, const sortBy = page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID;
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC, const sortOrder = page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC;
};
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
const isSmartPlaylist = detailQuery.data?.rules; const isSmartPlaylist = detailQuery.data?.rules;
const handlePlayQueueAdd = usePlayQueueAdd();
const cq = useContainerQuery(); const cq = useContainerQuery();
const setPagination = useSetPlaylistTablePagination(); const setPagination = useSetPlaylistTablePagination();
@ -279,8 +272,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
const sortByLabel = const sortByLabel =
(server?.type && (server?.type &&
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filters.sortBy) FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === sortBy)?.name) ||
?.name) ||
'Unknown'; 'Unknown';
const handleItemSize = (e: number) => { const handleItemSize = (e: number) => {
@ -307,13 +299,13 @@ export const PlaylistDetailSongListHeaderFilters = ({
(e: MouseEvent<HTMLButtonElement>) => { (e: MouseEvent<HTMLButtonElement>) => {
if (!e.currentTarget?.value || !server?.type) return; if (!e.currentTarget?.value || !server?.type) return;
const sortOrder = FILTERS[server.type as keyof typeof FILTERS].find( const newSortOrder = FILTERS[server.type as keyof typeof FILTERS].find(
(f) => f.value === e.currentTarget.value, (f) => f.value === e.currentTarget.value,
)?.defaultOrder; )?.defaultOrder;
setFilter(playlistId, { setFilter(playlistId, {
sortBy: e.currentTarget.value as SongListSort, sortBy: e.currentTarget.value as SongListSort,
sortOrder: sortOrder || SortOrder.ASC, sortOrder: newSortOrder || SortOrder.ASC,
}); });
handleFilterChange(); handleFilterChange();
@ -322,10 +314,15 @@ export const PlaylistDetailSongListHeaderFilters = ({
); );
const handleToggleSortOrder = useCallback(() => { const handleToggleSortOrder = useCallback(() => {
const newSortOrder = filters.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; const newSortOrder = sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
setFilter(playlistId, { sortOrder: newSortOrder }); setFilter(playlistId, { sortOrder: newSortOrder });
handleFilterChange(); handleFilterChange();
}, [filters.sortOrder, handleFilterChange, playlistId, setFilter]); }, [sortOrder, handleFilterChange, playlistId, setFilter]);
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
setFilter(playlistId, { searchTerm: e.target.value });
handleFilterChange();
}, 500);
const handleSetViewType = useCallback( const handleSetViewType = useCallback(
(displayType: ListDisplayType) => { (displayType: ListDisplayType) => {
@ -370,14 +367,6 @@ export const PlaylistDetailSongListHeaderFilters = ({
} }
}; };
const handlePlay = async (playType: Play) => {
handlePlayQueueAdd?.({
byItemType: { id: [playlistId], type: LibraryItem.PLAYLIST },
playType,
query: filters,
});
};
const deletePlaylistMutation = useDeletePlaylist({}); const deletePlaylistMutation = useDeletePlaylist({});
const handleDeletePlaylist = useCallback(() => { const handleDeletePlaylist = useCallback(() => {
@ -427,7 +416,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
<DropdownMenu.Dropdown> <DropdownMenu.Dropdown>
{FILTERS[server?.type as keyof typeof FILTERS].map((filter) => ( {FILTERS[server?.type as keyof typeof FILTERS].map((filter) => (
<DropdownMenu.Item <DropdownMenu.Item
isSelected={filter.value === filters.sortBy} isSelected={filter.value === sortBy}
key={`filter-${filter.name}`} key={`filter-${filter.name}`}
onClick={handleSetSortBy} onClick={handleSetSortBy}
value={filter.value} value={filter.value}
@ -441,7 +430,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
<Divider orientation="vertical" /> <Divider orientation="vertical" />
<OrderToggleButton <OrderToggleButton
onToggle={handleToggleSortOrder} onToggle={handleToggleSortOrder}
sortOrder={filters.sortOrder || SortOrder.ASC} sortOrder={sortOrder || SortOrder.ASC}
/> />
<DropdownMenu position="bottom-start"> <DropdownMenu position="bottom-start">
<DropdownMenu.Target> <DropdownMenu.Target>
@ -503,6 +492,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
)} )}
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
<SearchInput defaultValue={searchTerm} onChange={handleSearch} />
</Group> </Group>
<Group> <Group>
<ListConfigMenu <ListConfigMenu

View file

@ -5,31 +5,27 @@ import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { PageHeader } from '/@/renderer/components/page-header/page-header'; import { PageHeader } from '/@/renderer/components/page-header/page-header';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { PlaylistDetailSongListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header-filters'; import { PlaylistDetailSongListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header-filters';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query'; import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared'; import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
import { useCurrentServer, usePlaylistDetailStore } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { formatDurationString } from '/@/renderer/utils'; import { formatDurationString } from '/@/renderer/utils';
import { Badge } from '/@/shared/components/badge/badge'; import { Badge } from '/@/shared/components/badge/badge';
import { SpinnerIcon } from '/@/shared/components/spinner/spinner'; import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import {
LibraryItem,
PlaylistSongListQueryClientSide,
SongListSort,
SortOrder,
} from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
interface PlaylistDetailHeaderProps { interface PlaylistDetailHeaderProps {
handlePlay: (playType: Play) => void;
handleToggleShowQueryBuilder: () => void; handleToggleShowQueryBuilder: () => void;
itemCount?: number; itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>; tableRef: MutableRefObject<AgGridReactType | null>;
} }
export const PlaylistDetailSongListHeader = ({ export const PlaylistDetailSongListHeader = ({
handlePlay,
handleToggleShowQueryBuilder, handleToggleShowQueryBuilder,
itemCount, itemCount,
tableRef, tableRef,
@ -38,20 +34,6 @@ export const PlaylistDetailSongListHeader = ({
const { playlistId } = useParams() as { playlistId: string }; const { playlistId } = useParams() as { playlistId: string };
const server = useCurrentServer(); const server = useCurrentServer();
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
const handlePlayQueueAdd = usePlayQueueAdd();
const page = usePlaylistDetailStore();
const filters: Partial<PlaylistSongListQueryClientSide> = {
sortBy: page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID,
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC,
};
const handlePlay = async (playType: Play) => {
handlePlayQueueAdd?.({
byItemType: { id: [playlistId], type: LibraryItem.PLAYLIST },
playType,
query: filters,
});
};
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();
@ -78,6 +60,7 @@ export const PlaylistDetailSongListHeader = ({
</PageHeader> </PageHeader>
<FilterBar> <FilterBar>
<PlaylistDetailSongListHeaderFilters <PlaylistDetailSongListHeaderFilters
handlePlay={handlePlay}
handleToggleShowQueryBuilder={handleToggleShowQueryBuilder} handleToggleShowQueryBuilder={handleToggleShowQueryBuilder}
tableRef={tableRef} tableRef={tableRef}
/> />

View file

@ -1,11 +1,13 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { closeAllModals, openModal } from '@mantine/modals'; import { closeAllModals, openModal } from '@mantine/modals';
import Fuse from 'fuse.js';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
import { useMemo, useRef, useState } from 'react'; import { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath, useNavigate, useParams } from 'react-router'; import { generatePath, useNavigate, useParams } from 'react-router';
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
import { PlaylistDetailSongListContent } from '/@/renderer/features/playlists/components/playlist-detail-song-list-content'; import { PlaylistDetailSongListContent } from '/@/renderer/features/playlists/components/playlist-detail-song-list-content';
import { PlaylistDetailSongListHeader } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header'; import { PlaylistDetailSongListHeader } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header';
import { PlaylistQueryBuilder } from '/@/renderer/features/playlists/components/playlist-query-builder'; import { PlaylistQueryBuilder } from '/@/renderer/features/playlists/components/playlist-query-builder';
@ -23,6 +25,7 @@ import { Group } from '/@/shared/components/group/group';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { toast } from '/@/shared/components/toast/toast'; import { toast } from '/@/shared/components/toast/toast';
import { ServerType, SongListSort, SortOrder, sortSongList } from '/@/shared/types/domain-types'; import { ServerType, SongListSort, SortOrder, sortSongList } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
const PlaylistDetailSongListRoute = () => { const PlaylistDetailSongListRoute = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -30,6 +33,7 @@ const PlaylistDetailSongListRoute = () => {
const tableRef = useRef<AgGridReactType | null>(null); const tableRef = useRef<AgGridReactType | null>(null);
const { playlistId } = useParams() as { playlistId: string }; const { playlistId } = useParams() as { playlistId: string };
const server = useCurrentServer(); const server = useCurrentServer();
const handlePlayQueueAdd = useHandlePlayQueueAdd();
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
const createPlaylistMutation = useCreatePlaylist({}); const createPlaylistMutation = useCreatePlaylist({});
@ -151,21 +155,50 @@ const PlaylistDetailSongListRoute = () => {
serverId: server?.id, serverId: server?.id,
}); });
const itemCount = playlistSongs.data?.totalRecordCount ?? undefined;
const filterSortedSongs = useMemo(() => { const filterSortedSongs = useMemo(() => {
if (playlistSongs.data?.items) { let items = playlistSongs.data?.items;
if (items) {
const searchTerm = page?.table.id[playlistId]?.filter.searchTerm;
if (searchTerm) {
const fuse = new Fuse(items, {
fieldNormWeight: 1,
ignoreLocation: true,
keys: [
'name',
'album',
{
getFn: (song) => song.artists.map((artist) => artist.name),
name: 'artist',
},
],
threshold: 0,
});
items = fuse.search(searchTerm).map((item) => item.item);
}
const sortBy = page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID; const sortBy = page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID;
const sortOrder = page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC; const sortOrder = page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC;
return sortSongList(playlistSongs.data?.items, sortBy, sortOrder); return sortSongList(items, sortBy, sortOrder);
} else { } else {
return []; return [];
} }
}, [playlistSongs.data?.items, page?.table.id, playlistId]); }, [playlistSongs.data?.items, page?.table.id, playlistId]);
const itemCount = playlistSongs.data?.totalRecordCount ? filterSortedSongs.length : undefined;
const handlePlay = (play: Play) => {
handlePlayQueueAdd?.({
byData: filterSortedSongs,
playType: play,
});
};
return ( return (
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}> <AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
<PlaylistDetailSongListHeader <PlaylistDetailSongListHeader
handlePlay={handlePlay}
handleToggleShowQueryBuilder={handleToggleShowQueryBuilder} handleToggleShowQueryBuilder={handleToggleShowQueryBuilder}
itemCount={itemCount} itemCount={itemCount}
tableRef={tableRef} tableRef={tableRef}