refactor playlist route state

This commit is contained in:
jeffvli
2026-02-11 18:43:28 -08:00
parent 04d8e013e1
commit 7f5742119b
7 changed files with 128 additions and 57 deletions
+1
View File
@@ -13,6 +13,7 @@ interface ListContextProps {
isSmartPlaylist?: boolean;
itemCount?: number;
listData?: unknown[];
listKey?: ItemListKey;
mode?: 'edit' | 'view';
pageKey: ItemListKey | string;
setDisplayMode?: (displayMode: ListDisplayMode) => void;
@@ -21,6 +21,7 @@ import { useListContext } from '/@/renderer/context/list-context';
import { eventEmitter } from '/@/renderer/events/event-emitter';
import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { usePlaylistTrackList } from '/@/renderer/features/playlists/hooks/use-playlist-track-list';
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
import { useCurrentServer, useGeneralSettings, useListSettings } from '/@/renderer/store';
@@ -70,7 +71,6 @@ const PlaylistDetailSongListGrid = lazy(() =>
export const PlaylistDetailSongListContent = () => {
const { playlistId } = useParams() as { playlistId: string };
const server = useCurrentServer();
const { displayMode, setItemCount } = useListContext();
const queryClient = useQueryClient();
const playlistSongsQuery = useSuspenseQuery(
@@ -82,18 +82,12 @@ export const PlaylistDetailSongListContent = () => {
}),
);
useEffect(() => {
if (
displayMode !== LibraryItem.ALBUM &&
playlistSongsQuery.data?.totalRecordCount != null
) {
setItemCount?.(playlistSongsQuery.data.totalRecordCount);
}
}, [displayMode, playlistSongsQuery.data?.totalRecordCount, setItemCount]);
useEffect(() => {
const handleRefresh = async (payload: { key: string }) => {
if (payload.key !== ItemListKey.PLAYLIST_SONG) {
if (
payload.key !== ItemListKey.PLAYLIST_SONG &&
payload.key !== ItemListKey.PLAYLIST_ALBUM
) {
return;
}
@@ -113,7 +107,7 @@ export const PlaylistDetailSongListContent = () => {
return () => {
eventEmitter.off('ITEM_LIST_REFRESH', handleRefresh);
};
}, [playlistId, queryClient, server.id]);
}, [playlistId, queryClient, server?.id]);
return (
<Suspense fallback={<Spinner container />}>
@@ -124,7 +118,13 @@ export const PlaylistDetailSongListContent = () => {
export type OverridePlaylistSongListQuery = Omit<Partial<PlaylistSongListQuery>, 'id'>;
export const PlaylistDetailSongListView = ({ data }: { data: PlaylistSongListResponse }) => {
interface PlaylistDetailSongListViewProps {
data: PlaylistSongListResponse;
/** When provided, table/grid use this instead of computing from data (avoids duplicate filter/sort). */
items?: Song[];
}
export const PlaylistDetailSongListView = ({ data, items }: PlaylistDetailSongListViewProps) => {
const server = useCurrentServer();
const { display, itemsPerPage, pagination, table } = useListSettings(ItemListKey.PLAYLIST_SONG);
const { currentPage, onChange: onPageChange } = useItemListPagination();
@@ -141,7 +141,12 @@ export const PlaylistDetailSongListView = ({ data }: { data: PlaylistSongListRes
switch (display) {
case ListDisplayType.GRID: {
return (
<PlaylistDetailSongListGrid data={data} serverId={server.id} {...paginationProps} />
<PlaylistDetailSongListGrid
data={data}
items={items}
serverId={server.id}
{...paginationProps}
/>
);
}
case ListDisplayType.TABLE: {
@@ -155,6 +160,7 @@ export const PlaylistDetailSongListView = ({ data }: { data: PlaylistSongListRes
enableHorizontalBorders={table.enableHorizontalBorders}
enableRowHoverHighlight={table.enableRowHoverHighlight}
enableVerticalBorders={table.enableVerticalBorders}
items={items}
serverId={server.id}
size={table.size}
{...paginationProps}
@@ -363,7 +369,7 @@ export function playlistSongsToAlbums(songs: Song[]): PlaylistAlbumRow[] {
return rows;
}
const PlaylistDetailAlbumList = ({ data }: { data: PlaylistSongListResponse }) => {
export const PlaylistDetailAlbumView = ({ data }: { data: PlaylistSongListResponse }) => {
const player = usePlayer();
const { setItemCount, setListData } = useListContext();
const { detail, display, grid, itemsPerPage, pagination, table } = useListSettings(
@@ -528,23 +534,33 @@ const PlaylistDetailAlbumList = ({ data }: { data: PlaylistSongListResponse }) =
return renderAlbumList();
};
const PlaylistDetailSongList = ({ data }: { data: PlaylistSongListResponse }) => {
const { displayMode, isSmartPlaylist, mode } = useListContext();
if (displayMode === LibraryItem.ALBUM) {
return <PlaylistDetailAlbumList data={data} />;
}
/** Track view: view mode uses centralized list derivation; edit mode uses local reorder state. */
const PlaylistDetailTrackView = ({ data }: { data: PlaylistSongListResponse }) => {
const { isSmartPlaylist, mode } = useListContext();
if (isSmartPlaylist) {
return <PlaylistDetailSongListView data={data} />;
return <PlaylistDetailTrackViewContent data={data} />;
}
switch (mode) {
case 'edit':
return <PlaylistDetailSongListEdit data={data} />;
case 'view':
return <PlaylistDetailSongListView data={data} />;
default:
return null;
if (mode === 'edit') {
return <PlaylistDetailSongListEdit data={data} />;
}
return <PlaylistDetailTrackViewContent data={data} />;
};
/** Uses usePlaylistTrackList once and passes derived items to the list view. */
const PlaylistDetailTrackViewContent = ({ data }: { data: PlaylistSongListResponse }) => {
const { sortedAndFilteredSongs } = usePlaylistTrackList(data);
return <PlaylistDetailSongListView data={data} items={sortedAndFilteredSongs} />;
};
const PlaylistDetailSongList = ({ data }: { data: PlaylistSongListResponse }) => {
const { displayMode } = useListContext();
if (displayMode === LibraryItem.ALBUM) {
return <PlaylistDetailAlbumView data={data} />;
}
return <PlaylistDetailTrackView data={data} />;
};
@@ -16,6 +16,7 @@ import {
LibraryItem,
PlaylistSongListQuery,
PlaylistSongListResponse,
Song,
} from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
@@ -23,36 +24,44 @@ interface PlaylistDetailSongListGridProps
extends Omit<ItemListGridComponentProps<PlaylistSongListQuery>, 'query'> {
currentPage?: number;
data: PlaylistSongListResponse;
items?: Song[];
itemsPerPage?: number;
onPageChange?: (page: number) => void;
}
export const PlaylistDetailSongListGrid = forwardRef<any, PlaylistDetailSongListGridProps>(
({ currentPage, data, itemsPerPage, onPageChange, saveScrollOffset = true }) => {
({
currentPage,
data,
items: itemsProp,
itemsPerPage,
onPageChange,
saveScrollOffset = true,
}) => {
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
enabled: saveScrollOffset,
});
const { searchTerm } = useSearchTermFilter();
const { query } = usePlaylistSongListFilters();
const { setListData } = useListContext();
const songData = useMemo(() => {
let items = data?.items || [];
const songDataFromData = useMemo(() => {
let list = data?.items || [];
if (searchTerm) {
items = searchLibraryItems(items, searchTerm, LibraryItem.SONG);
return items;
list = searchLibraryItems(list, searchTerm, LibraryItem.SONG);
return list;
}
return sortSongList(items, query.sortBy, query.sortOrder);
return sortSongList(list, query.sortBy, query.sortOrder);
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
const { setListData } = useListContext();
const songData = itemsProp ?? songDataFromData;
useEffect(() => {
if (setListData) {
setListData(songData);
if (itemsProp == null && setListData) {
setListData(songDataFromData);
}
}, [songData, setListData]);
}, [itemsProp, songDataFromData, setListData]);
const gridProps = useListSettings(ItemListKey.PLAYLIST_SONG).grid;
@@ -46,7 +46,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
isSmartPlaylist,
}: PlaylistDetailSongListHeaderFiltersProps) => {
const { t } = useTranslation();
const { mode, setMode } = useListContext();
const { listKey: listKeyFromContext, mode, setMode } = useListContext();
const { playlistId } = useParams() as { playlistId: string };
const playlistTarget = usePlaylistTarget();
const { setPlaylistBehavior } = useSettingsStoreActions();
@@ -66,8 +66,12 @@ export const PlaylistDetailSongListHeaderFilters = ({
});
};
const isAlbumMode = playlistTarget === PlaylistTarget.ALBUM;
const listKey = isAlbumMode ? ItemListKey.PLAYLIST_ALBUM : ItemListKey.PLAYLIST_SONG;
const listKey =
listKeyFromContext ??
(playlistTarget === PlaylistTarget.ALBUM
? ItemListKey.PLAYLIST_ALBUM
: ItemListKey.PLAYLIST_SONG);
const isAlbumMode = listKey === ItemListKey.PLAYLIST_ALBUM;
const toggleChoice = isAlbumMode
? t('entity.album', { count: 2, postProcess: 'titleCase' })
: t('entity.track', { count: 2, postProcess: 'titleCase' });
@@ -27,6 +27,7 @@ interface PlaylistDetailSongListTableProps
extends Omit<ItemListTableComponentProps<PlaylistSongListQuery>, 'query'> {
currentPage?: number;
data: PlaylistSongListResponse;
items?: Song[];
itemsPerPage?: number;
onPageChange?: (page: number) => void;
}
@@ -44,6 +45,7 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
enableRowHoverHighlight = true,
enableSelection = true,
enableVerticalBorders = false,
items: itemsProp,
itemsPerPage,
onPageChange,
saveScrollOffset = true,
@@ -65,24 +67,24 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
const { searchTerm } = useSearchTermFilter();
const { query } = usePlaylistSongListFilters();
const { setListData } = useListContext();
const songData = useMemo(() => {
let items = data?.items || [];
const songDataFromData = useMemo(() => {
let list = data?.items || [];
if (searchTerm) {
items = searchLibraryItems(items, searchTerm, LibraryItem.SONG);
return items;
list = searchLibraryItems(list, searchTerm, LibraryItem.SONG);
return list;
}
return sortSongList(items, query.sortBy, query.sortOrder);
return sortSongList(list, query.sortBy, query.sortOrder);
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
const { setListData } = useListContext();
const songData = itemsProp ?? songDataFromData;
useEffect(() => {
if (setListData) {
setListData(songData);
if (itemsProp == null && setListData) {
setListData(songDataFromData);
}
}, [songData, setListData]);
}, [itemsProp, songDataFromData, setListData]);
const player = usePlayer();
@@ -0,0 +1,36 @@
import { useEffect, useMemo } from 'react';
import { useListContext } from '/@/renderer/context/list-context';
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
import { sortSongList } from '/@/shared/api/utils';
import { LibraryItem, PlaylistSongListResponse, Song } from '/@/shared/types/domain-types';
export function usePlaylistTrackList(data: PlaylistSongListResponse | undefined): {
sortedAndFilteredSongs: Song[];
totalCount: number;
} {
const { setItemCount, setListData } = useListContext();
const { searchTerm } = useSearchTermFilter();
const { query } = usePlaylistSongListFilters();
const sortedAndFilteredSongs = useMemo(() => {
const raw = data?.items ?? [];
if (searchTerm) {
return searchLibraryItems(raw, searchTerm, LibraryItem.SONG);
}
return sortSongList(raw, query.sortBy, query.sortOrder);
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
const totalCount = sortedAndFilteredSongs.length;
useEffect(() => {
setListData?.(sortedAndFilteredSongs);
setItemCount?.(totalCount);
}, [sortedAndFilteredSongs, totalCount, setListData, setItemCount]);
return { sortedAndFilteredSongs, totalCount };
}
@@ -402,6 +402,8 @@ const PlaylistDetailSongListRoute = () => {
const playlistTarget = usePlaylistTarget();
const displayMode: LibraryItem.ALBUM | LibraryItem.SONG =
playlistTarget === PlaylistTarget.ALBUM ? LibraryItem.ALBUM : LibraryItem.SONG;
const listKey =
displayMode === LibraryItem.ALBUM ? ItemListKey.PLAYLIST_ALBUM : ItemListKey.PLAYLIST_SONG;
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
const [listData, setListData] = useState<unknown[]>([]);
@@ -415,13 +417,14 @@ const PlaylistDetailSongListRoute = () => {
isSmartPlaylist,
itemCount,
listData,
listKey,
mode,
pageKey: ItemListKey.PLAYLIST_SONG,
pageKey: listKey,
setItemCount,
setListData,
setMode,
};
}, [playlistId, isSmartPlaylist, displayMode, itemCount, listData, mode]);
}, [playlistId, isSmartPlaylist, displayMode, listKey, itemCount, listData, mode]);
return (
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>