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

View file

@ -5,31 +5,27 @@ import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
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 { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
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 { formatDurationString } from '/@/renderer/utils';
import { Badge } from '/@/shared/components/badge/badge';
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
import { Stack } from '/@/shared/components/stack/stack';
import {
LibraryItem,
PlaylistSongListQueryClientSide,
SongListSort,
SortOrder,
} from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
interface PlaylistDetailHeaderProps {
handlePlay: (playType: Play) => void;
handleToggleShowQueryBuilder: () => void;
itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const PlaylistDetailSongListHeader = ({
handlePlay,
handleToggleShowQueryBuilder,
itemCount,
tableRef,
@ -38,20 +34,6 @@ export const PlaylistDetailSongListHeader = ({
const { playlistId } = useParams() as { playlistId: string };
const server = useCurrentServer();
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();
@ -78,6 +60,7 @@ export const PlaylistDetailSongListHeader = ({
</PageHeader>
<FilterBar>
<PlaylistDetailSongListHeaderFilters
handlePlay={handlePlay}
handleToggleShowQueryBuilder={handleToggleShowQueryBuilder}
tableRef={tableRef}
/>

View file

@ -1,11 +1,13 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { closeAllModals, openModal } from '@mantine/modals';
import Fuse from 'fuse.js';
import { motion } from 'motion/react';
import { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
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 { PlaylistDetailSongListHeader } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header';
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 { toast } from '/@/shared/components/toast/toast';
import { ServerType, SongListSort, SortOrder, sortSongList } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
const PlaylistDetailSongListRoute = () => {
const { t } = useTranslation();
@ -30,6 +33,7 @@ const PlaylistDetailSongListRoute = () => {
const tableRef = useRef<AgGridReactType | null>(null);
const { playlistId } = useParams() as { playlistId: string };
const server = useCurrentServer();
const handlePlayQueueAdd = useHandlePlayQueueAdd();
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
const createPlaylistMutation = useCreatePlaylist({});
@ -151,21 +155,50 @@ const PlaylistDetailSongListRoute = () => {
serverId: server?.id,
});
const itemCount = playlistSongs.data?.totalRecordCount ?? undefined;
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 sortOrder = page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC;
return sortSongList(playlistSongs.data?.items, sortBy, sortOrder);
return sortSongList(items, sortBy, sortOrder);
} else {
return [];
}
}, [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 (
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
<PlaylistDetailSongListHeader
handlePlay={handlePlay}
handleToggleShowQueryBuilder={handleToggleShowQueryBuilder}
itemCount={itemCount}
tableRef={tableRef}