mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-10 06:12:43 +02:00
Add album view for playlists (#1700)
* update client side song ordering to include album order * add compact styling to LibraryHeader * move search button to top right of LibraryHeader
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
||||
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
||||
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||
import { ItemDetailList } from '/@/renderer/components/item-list/item-detail-list/item-detail-list';
|
||||
import { ItemGridList } from '/@/renderer/components/item-list/item-grid-list/item-grid-list';
|
||||
import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination';
|
||||
import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination';
|
||||
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||
import { DefaultItemControlProps, ItemControls } from '/@/renderer/components/item-list/types';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
|
||||
import { type PlaylistAlbumRow, playlistSongsToAlbums } from '/@/renderer/features/playlists/utils';
|
||||
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
|
||||
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
|
||||
import { useGeneralSettings, useListSettings } from '/@/renderer/store';
|
||||
import { sortSongList } from '/@/shared/api/utils';
|
||||
import {
|
||||
LibraryItem,
|
||||
PlaylistSongListResponse,
|
||||
SongListSort,
|
||||
SortOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, ListDisplayType, ListPaginationType, Play } from '/@/shared/types/types';
|
||||
|
||||
export const PlaylistDetailAlbumView = ({ data }: { data: PlaylistSongListResponse }) => {
|
||||
const player = usePlayer();
|
||||
const { setItemCount, setListData } = useListContext();
|
||||
const { detail, display, grid, itemsPerPage, pagination, table } = useListSettings(
|
||||
ItemListKey.PLAYLIST_ALBUM,
|
||||
);
|
||||
const { enableGridMultiSelect } = useGeneralSettings();
|
||||
const { currentPage, onChange: onPageChange } = useItemListPagination();
|
||||
const { searchTerm } = useSearchTermFilter();
|
||||
const { query } = usePlaylistSongListFilters();
|
||||
|
||||
const sortedAlbums = useMemo(() => {
|
||||
let songs = data?.items ?? [];
|
||||
if (searchTerm?.trim()) {
|
||||
songs = searchLibraryItems(songs, searchTerm, LibraryItem.SONG);
|
||||
}
|
||||
const sortedSongs = sortSongList(
|
||||
songs,
|
||||
(query.sortBy as SongListSort) ?? SongListSort.ID,
|
||||
(query.sortOrder as SortOrder) ?? SortOrder.ASC,
|
||||
);
|
||||
return playlistSongsToAlbums(sortedSongs);
|
||||
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
|
||||
|
||||
const isPaginated = pagination === ListPaginationType.PAGINATED;
|
||||
const totalAlbumCount = sortedAlbums.length;
|
||||
const albumPageCount = Math.max(1, Math.ceil(totalAlbumCount / itemsPerPage));
|
||||
const paginatedAlbums = useMemo(() => {
|
||||
if (!isPaginated) return sortedAlbums;
|
||||
const start = currentPage * itemsPerPage;
|
||||
return sortedAlbums.slice(start, start + itemsPerPage);
|
||||
}, [isPaginated, currentPage, itemsPerPage, sortedAlbums]);
|
||||
const albumsToRender = isPaginated ? paginatedAlbums : sortedAlbums;
|
||||
|
||||
const playlistSongs = useMemo(() => data?.items ?? [], [data?.items]);
|
||||
|
||||
const albumControlOverrides = useMemo<Partial<ItemControls>>(() => {
|
||||
return {
|
||||
onPlay: ({
|
||||
item,
|
||||
itemType,
|
||||
playType,
|
||||
}: DefaultItemControlProps & { playType: Play }) => {
|
||||
if (!item) return;
|
||||
const rowSongs = (item as PlaylistAlbumRow)._playlistSongs;
|
||||
if (itemType === LibraryItem.ALBUM && rowSongs?.length) {
|
||||
player.addToQueueByData(rowSongs, playType);
|
||||
return;
|
||||
}
|
||||
player.addToQueueByFetch(item._serverId, [item.id], itemType, playType);
|
||||
},
|
||||
};
|
||||
}, [player]);
|
||||
|
||||
useEffect(() => {
|
||||
setItemCount?.(totalAlbumCount);
|
||||
}, [setItemCount, totalAlbumCount]);
|
||||
|
||||
useEffect(() => {
|
||||
setListData?.(data?.items ?? []);
|
||||
}, [data?.items, setListData]);
|
||||
|
||||
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({ enabled: true });
|
||||
const { handleColumnReordered } = useItemListColumnReorder({
|
||||
itemListKey: ItemListKey.PLAYLIST_ALBUM,
|
||||
});
|
||||
const { handleColumnResized } = useItemListColumnResize({
|
||||
itemListKey: ItemListKey.PLAYLIST_ALBUM,
|
||||
});
|
||||
const { handleColumnReordered: handleDetailColumnReordered } = useItemListColumnReorder({
|
||||
itemListKey: ItemListKey.PLAYLIST_ALBUM,
|
||||
tableKey: 'detail',
|
||||
});
|
||||
const { handleColumnResized: handleDetailColumnResized } = useItemListColumnResize({
|
||||
itemListKey: ItemListKey.PLAYLIST_ALBUM,
|
||||
tableKey: 'detail',
|
||||
});
|
||||
const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.PLAYLIST_ALBUM, grid.size);
|
||||
|
||||
const renderAlbumList = () => {
|
||||
switch (display) {
|
||||
case ListDisplayType.DETAIL:
|
||||
return (
|
||||
<ItemDetailList
|
||||
enableHeader={detail?.enableHeader}
|
||||
items={albumsToRender}
|
||||
listKey={ItemListKey.PLAYLIST_ALBUM}
|
||||
onColumnReordered={handleDetailColumnReordered}
|
||||
onColumnResized={handleDetailColumnResized}
|
||||
onScrollEnd={handleOnScrollEnd}
|
||||
onSongRowDoubleClick={({ internalState, item }) => {
|
||||
if (playlistSongs.length === 0) return;
|
||||
internalState?.setSelected([item]);
|
||||
player.addToQueueByData(playlistSongs, Play.NOW, item.id);
|
||||
}}
|
||||
overrideControls={albumControlOverrides}
|
||||
scrollOffset={scrollOffset ?? 0}
|
||||
songsByAlbumId={{}}
|
||||
tableId="album-detail"
|
||||
/>
|
||||
);
|
||||
case ListDisplayType.GRID:
|
||||
return (
|
||||
<ItemGridList
|
||||
data={albumsToRender}
|
||||
enableExpansion
|
||||
enableMultiSelect={enableGridMultiSelect}
|
||||
gap={grid.itemGap}
|
||||
initialTop={{
|
||||
to: scrollOffset ?? 0,
|
||||
type: 'offset',
|
||||
}}
|
||||
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||
itemType={LibraryItem.ALBUM}
|
||||
onScrollEnd={handleOnScrollEnd}
|
||||
overrideControls={albumControlOverrides}
|
||||
rows={rows}
|
||||
size={grid.size}
|
||||
/>
|
||||
);
|
||||
case ListDisplayType.TABLE:
|
||||
return (
|
||||
<ItemTableList
|
||||
autoFitColumns={table.autoFitColumns}
|
||||
CellComponent={ItemTableListColumn}
|
||||
columns={table.columns}
|
||||
data={albumsToRender}
|
||||
enableAlternateRowColors={table.enableAlternateRowColors}
|
||||
enableHeader={table.enableHeader}
|
||||
enableHorizontalBorders={table.enableHorizontalBorders}
|
||||
enableRowHoverHighlight={table.enableRowHoverHighlight}
|
||||
enableSelection
|
||||
enableVerticalBorders={table.enableVerticalBorders}
|
||||
initialTop={{
|
||||
to: scrollOffset ?? 0,
|
||||
type: 'offset',
|
||||
}}
|
||||
itemType={LibraryItem.ALBUM}
|
||||
onColumnReordered={handleColumnReordered}
|
||||
onColumnResized={handleColumnResized}
|
||||
onScrollEnd={handleOnScrollEnd}
|
||||
overrideControls={albumControlOverrides}
|
||||
size={table.size}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (isPaginated) {
|
||||
return (
|
||||
<ItemListWithPagination
|
||||
currentPage={currentPage}
|
||||
itemsPerPage={itemsPerPage}
|
||||
onChange={onPageChange}
|
||||
pageCount={albumPageCount}
|
||||
totalItemCount={totalAlbumCount}
|
||||
>
|
||||
{renderAlbumList()}
|
||||
</ItemListWithPagination>
|
||||
);
|
||||
}
|
||||
|
||||
return renderAlbumList();
|
||||
};
|
||||
@@ -2,14 +2,27 @@ import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination';
|
||||
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||
import { PlaylistDetailAlbumView } from '/@/renderer/features/playlists/components/playlist-detail-album-view';
|
||||
import { usePlaylistTrackList } from '/@/renderer/features/playlists/hooks/use-playlist-track-list';
|
||||
import { useCurrentServer, useListSettings } from '/@/renderer/store';
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
import { PlaylistSongListQuery, PlaylistSongListResponse } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, ListDisplayType, TableColumn } from '/@/shared/types/types';
|
||||
import {
|
||||
LibraryItem,
|
||||
PlaylistSongListQuery,
|
||||
PlaylistSongListResponse,
|
||||
Song,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import {
|
||||
ItemListKey,
|
||||
ListDisplayType,
|
||||
ListPaginationType,
|
||||
TableColumn,
|
||||
} from '/@/shared/types/types';
|
||||
|
||||
const PlaylistDetailSongListTable = lazy(() =>
|
||||
import('/@/renderer/features/playlists/components/playlist-detail-song-list-table').then(
|
||||
@@ -38,7 +51,6 @@ const PlaylistDetailSongListGrid = lazy(() =>
|
||||
export const PlaylistDetailSongListContent = () => {
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const server = useCurrentServer();
|
||||
const { setItemCount } = useListContext();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const playlistSongsQuery = useSuspenseQuery(
|
||||
@@ -50,18 +62,12 @@ export const PlaylistDetailSongListContent = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
playlistSongsQuery.data?.totalRecordCount !== undefined &&
|
||||
playlistSongsQuery.data.totalRecordCount !== null
|
||||
) {
|
||||
setItemCount?.(playlistSongsQuery.data.totalRecordCount);
|
||||
}
|
||||
}, [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;
|
||||
}
|
||||
|
||||
@@ -81,7 +87,7 @@ export const PlaylistDetailSongListContent = () => {
|
||||
return () => {
|
||||
eventEmitter.off('ITEM_LIST_REFRESH', handleRefresh);
|
||||
};
|
||||
}, [playlistId, queryClient, server.id]);
|
||||
}, [playlistId, queryClient, server?.id]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
@@ -92,13 +98,36 @@ 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, table } = useListSettings(ItemListKey.PLAYLIST_SONG);
|
||||
const { display, itemsPerPage, pagination, table } = useListSettings(ItemListKey.PLAYLIST_SONG);
|
||||
const { currentPage, onChange: onPageChange } = useItemListPagination();
|
||||
const isPaginated = pagination === ListPaginationType.PAGINATED;
|
||||
|
||||
const paginationProps = isPaginated
|
||||
? {
|
||||
currentPage,
|
||||
itemsPerPage,
|
||||
onPageChange,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
switch (display) {
|
||||
case ListDisplayType.GRID: {
|
||||
return <PlaylistDetailSongListGrid data={data} serverId={server.id} />;
|
||||
return (
|
||||
<PlaylistDetailSongListGrid
|
||||
data={data}
|
||||
items={items}
|
||||
serverId={server.id}
|
||||
{...paginationProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case ListDisplayType.TABLE: {
|
||||
return (
|
||||
@@ -111,8 +140,10 @@ export const PlaylistDetailSongListView = ({ data }: { data: PlaylistSongListRes
|
||||
enableHorizontalBorders={table.enableHorizontalBorders}
|
||||
enableRowHoverHighlight={table.enableRowHoverHighlight}
|
||||
enableVerticalBorders={table.enableVerticalBorders}
|
||||
items={items}
|
||||
serverId={server.id}
|
||||
size={table.size}
|
||||
{...paginationProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -252,19 +283,33 @@ export const PlaylistDetailSongListEdit = ({ data }: { data: PlaylistSongListRes
|
||||
}
|
||||
};
|
||||
|
||||
const PlaylistDetailSongList = ({ data }: { data: PlaylistSongListResponse }) => {
|
||||
/** 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} />;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect } from 'react';
|
||||
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||
import { ItemGridList } from '/@/renderer/components/item-list/item-grid-list/item-grid-list';
|
||||
import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination';
|
||||
import { ItemListGridComponentProps } from '/@/renderer/components/item-list/types';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
|
||||
@@ -15,40 +16,52 @@ import {
|
||||
LibraryItem,
|
||||
PlaylistSongListQuery,
|
||||
PlaylistSongListResponse,
|
||||
Song,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
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>(
|
||||
({ data, 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;
|
||||
|
||||
@@ -59,9 +72,22 @@ export const PlaylistDetailSongListGrid = forwardRef<any, PlaylistDetailSongList
|
||||
);
|
||||
const { enableGridMultiSelect } = useGeneralSettings();
|
||||
|
||||
return (
|
||||
const isPaginated =
|
||||
typeof currentPage === 'number' &&
|
||||
typeof itemsPerPage === 'number' &&
|
||||
typeof onPageChange === 'function';
|
||||
const totalCount = songData.length;
|
||||
const pageCount = Math.max(1, Math.ceil(totalCount / (itemsPerPage ?? 1)));
|
||||
const paginatedData = useMemo(() => {
|
||||
if (!isPaginated || currentPage == null || itemsPerPage == null) return songData;
|
||||
const start = currentPage * itemsPerPage;
|
||||
return songData.slice(start, start + itemsPerPage);
|
||||
}, [currentPage, isPaginated, itemsPerPage, songData]);
|
||||
const dataToRender = isPaginated ? paginatedData : songData;
|
||||
|
||||
const grid = (
|
||||
<ItemGridList
|
||||
data={songData}
|
||||
data={dataToRender}
|
||||
enableMultiSelect={enableGridMultiSelect}
|
||||
gap={gridProps.itemGap}
|
||||
initialTop={{
|
||||
@@ -75,5 +101,21 @@ export const PlaylistDetailSongListGrid = forwardRef<any, PlaylistDetailSongList
|
||||
size={gridProps.size}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isPaginated && itemsPerPage != null) {
|
||||
return (
|
||||
<ItemListWithPagination
|
||||
currentPage={currentPage!}
|
||||
itemsPerPage={itemsPerPage}
|
||||
onChange={onPageChange!}
|
||||
pageCount={pageCount}
|
||||
totalItemCount={totalCount}
|
||||
>
|
||||
{grid}
|
||||
</ItemListWithPagination>
|
||||
);
|
||||
}
|
||||
|
||||
return grid;
|
||||
},
|
||||
);
|
||||
|
||||
+59
-12
@@ -5,19 +5,27 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { PLAYLIST_SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import {
|
||||
ALBUM_TABLE_COLUMNS,
|
||||
PLAYLIST_SONG_TABLE_COLUMNS,
|
||||
SONG_TABLE_COLUMNS,
|
||||
} from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import { ListDisplayTypeToggleButton } from '/@/renderer/features/shared/components/list-display-type-toggle-button';
|
||||
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||
import { ListSearchInput } from '/@/renderer/features/shared/components/list-search-input';
|
||||
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
||||
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { useCurrentServerId } from '/@/renderer/store';
|
||||
import {
|
||||
PlaylistTarget,
|
||||
useCurrentServerId,
|
||||
usePlaylistTarget,
|
||||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
@@ -37,8 +45,10 @@ 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();
|
||||
const serverId = useCurrentServerId();
|
||||
|
||||
const detailQuery = useQuery(playlistsQueries.detail({ query: { id: playlistId }, serverId }));
|
||||
@@ -55,9 +65,25 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
||||
});
|
||||
};
|
||||
|
||||
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' });
|
||||
|
||||
const handleToggleDisplayMode = useCallback(() => {
|
||||
setPlaylistBehavior(
|
||||
playlistTarget === PlaylistTarget.ALBUM ? PlaylistTarget.TRACK : PlaylistTarget.ALBUM,
|
||||
);
|
||||
}, [playlistTarget, setPlaylistBehavior]);
|
||||
|
||||
const { ref: containerRef, ...breakpoints } = useContainerQuery();
|
||||
|
||||
const isViewEditMode = !isSmartPlaylist && breakpoints.isSm;
|
||||
const isViewEditMode = !isSmartPlaylist && (breakpoints.isSm || isAlbumMode);
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
const [collapsed, setCollapsed] = useLocalStorage<boolean>({
|
||||
@@ -68,6 +94,14 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
||||
return (
|
||||
<Flex justify="space-between" ref={containerRef}>
|
||||
<Group gap="sm" w="100%">
|
||||
<Button
|
||||
leftSection={<Icon icon="arrowLeftRight" />}
|
||||
onClick={handleToggleDisplayMode}
|
||||
variant="subtle"
|
||||
>
|
||||
{toggleChoice}
|
||||
</Button>
|
||||
<Divider orientation="vertical" />
|
||||
<ListSortByDropdown
|
||||
defaultSortByValue={SongListSort.ID}
|
||||
disabled={isEditMode}
|
||||
@@ -80,8 +114,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
||||
disabled={isEditMode}
|
||||
listKey={ItemListKey.PLAYLIST_SONG}
|
||||
/>
|
||||
{!collapsed && <ListSearchInput />}
|
||||
<ListRefreshButton disabled={isEditMode} listKey={ItemListKey.PLAYLIST_SONG} />
|
||||
<ListRefreshButton disabled={isEditMode} listKey={listKey} />
|
||||
<MoreButton onClick={handleMore} />
|
||||
</Group>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
@@ -109,11 +142,25 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
||||
variant="subtle"
|
||||
/>
|
||||
</Tooltip>
|
||||
<ListDisplayTypeToggleButton listKey={ItemListKey.PLAYLIST_SONG} />
|
||||
<ListConfigMenu
|
||||
listKey={ItemListKey.PLAYLIST_SONG}
|
||||
tableColumnsData={PLAYLIST_SONG_TABLE_COLUMNS}
|
||||
/>
|
||||
<ListDisplayTypeToggleButton enableDetail={isAlbumMode} listKey={listKey} />
|
||||
{isAlbumMode ? (
|
||||
<ListConfigMenu
|
||||
detailConfig={{
|
||||
optionsConfig: {
|
||||
autoFitColumns: { hidden: true },
|
||||
},
|
||||
tableColumnsData: SONG_TABLE_COLUMNS,
|
||||
tableKey: 'detail',
|
||||
}}
|
||||
listKey={listKey}
|
||||
tableColumnsData={ALBUM_TABLE_COLUMNS}
|
||||
/>
|
||||
) : (
|
||||
<ListConfigMenu
|
||||
listKey={listKey}
|
||||
tableColumnsData={PLAYLIST_SONG_TABLE_COLUMNS}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -93,6 +93,7 @@ export const PlaylistDetailSongListHeader = ({
|
||||
</PageHeader>
|
||||
) : (
|
||||
<LibraryHeader
|
||||
compact
|
||||
imageUrl={imageUrl}
|
||||
item={{
|
||||
imageId: detailQuery?.data?.imageId,
|
||||
@@ -101,6 +102,7 @@ export const PlaylistDetailSongListHeader = ({
|
||||
type: LibraryItem.PLAYLIST,
|
||||
}}
|
||||
title={detailQuery?.data?.name || ''}
|
||||
topRight={<ListSearchInput />}
|
||||
>
|
||||
<LibraryHeaderMenu
|
||||
onPlay={(type) => handlePlay(type)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect } from 'react';
|
||||
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
||||
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
||||
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||
import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination';
|
||||
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||
import { ItemControls, ItemListTableComponentProps } from '/@/renderer/components/item-list/types';
|
||||
@@ -24,7 +25,11 @@ import { ItemListKey, Play } from '/@/shared/types/types';
|
||||
|
||||
interface PlaylistDetailSongListTableProps
|
||||
extends Omit<ItemListTableComponentProps<PlaylistSongListQuery>, 'query'> {
|
||||
currentPage?: number;
|
||||
data: PlaylistSongListResponse;
|
||||
items?: Song[];
|
||||
itemsPerPage?: number;
|
||||
onPageChange?: (page: number) => void;
|
||||
}
|
||||
|
||||
export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongListTableProps>(
|
||||
@@ -32,6 +37,7 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
||||
{
|
||||
autoFitColumns = false,
|
||||
columns,
|
||||
currentPage,
|
||||
data,
|
||||
enableAlternateRowColors = false,
|
||||
enableHeader = true,
|
||||
@@ -39,6 +45,9 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
||||
enableRowHoverHighlight = true,
|
||||
enableSelection = true,
|
||||
enableVerticalBorders = false,
|
||||
items: itemsProp,
|
||||
itemsPerPage,
|
||||
onPageChange,
|
||||
saveScrollOffset = true,
|
||||
size = 'default',
|
||||
},
|
||||
@@ -58,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();
|
||||
|
||||
@@ -108,13 +117,26 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
const isPaginated =
|
||||
typeof currentPage === 'number' &&
|
||||
typeof itemsPerPage === 'number' &&
|
||||
typeof onPageChange === 'function';
|
||||
const totalCount = songData.length;
|
||||
const pageCount = Math.max(1, Math.ceil(totalCount / (itemsPerPage ?? 1)));
|
||||
const paginatedData = useMemo(() => {
|
||||
if (!isPaginated || currentPage == null || itemsPerPage == null) return songData;
|
||||
const start = currentPage * itemsPerPage;
|
||||
return songData.slice(start, start + itemsPerPage);
|
||||
}, [isPaginated, currentPage, itemsPerPage, songData]);
|
||||
const dataToRender = isPaginated ? paginatedData : songData;
|
||||
|
||||
const table = (
|
||||
<ItemTableList
|
||||
activeRowId={currentSong?.id}
|
||||
autoFitColumns={autoFitColumns}
|
||||
CellComponent={ItemTableListColumn}
|
||||
columns={columns}
|
||||
data={songData}
|
||||
data={dataToRender}
|
||||
enableAlternateRowColors={enableAlternateRowColors}
|
||||
enableExpansion={false}
|
||||
enableHeader={enableHeader}
|
||||
@@ -136,6 +158,22 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isPaginated && itemsPerPage != null) {
|
||||
return (
|
||||
<ItemListWithPagination
|
||||
currentPage={currentPage!}
|
||||
itemsPerPage={itemsPerPage}
|
||||
onChange={onPageChange!}
|
||||
pageCount={pageCount}
|
||||
totalItemCount={totalCount}
|
||||
>
|
||||
{table}
|
||||
</ItemListWithPagination>
|
||||
);
|
||||
}
|
||||
|
||||
return table;
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user