mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f02307ff2a | |||
| 2647c36326 | |||
| 16a9d6e702 | |||
| 0a4d789f08 | |||
| 7f5742119b | |||
| 04d8e013e1 |
@@ -38,6 +38,7 @@ const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefin
|
||||
[AlbumListSort.DURATION]: undefined,
|
||||
[AlbumListSort.EXPLICIT_STATUS]: undefined,
|
||||
[AlbumListSort.FAVORITED]: AlbumListSortType.STARRED,
|
||||
[AlbumListSort.ID]: undefined,
|
||||
[AlbumListSort.NAME]: AlbumListSortType.ALPHABETICAL_BY_NAME,
|
||||
[AlbumListSort.PLAY_COUNT]: AlbumListSortType.FREQUENT,
|
||||
[AlbumListSort.RANDOM]: AlbumListSortType.RANDOM,
|
||||
|
||||
@@ -244,8 +244,6 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
const singleSongOnly = meta?.singleSongOnly === true;
|
||||
|
||||
// For single-song actions (e.g. image play button), or NEXT/LAST/..., only add the clicked song
|
||||
// For row double-click with NOW/SHUFFLE, add a range of songs around the clicked song
|
||||
let songsToAdd: Song[];
|
||||
if (
|
||||
singleSongOnly ||
|
||||
|
||||
@@ -84,6 +84,7 @@ interface ItemDetailListProps {
|
||||
internalState?: ItemListStateActions;
|
||||
itemCount?: number;
|
||||
items?: unknown[];
|
||||
listKey?: ItemListKey;
|
||||
onColumnReordered?: (
|
||||
columnIdFrom: TableColumn,
|
||||
columnIdTo: TableColumn,
|
||||
@@ -92,8 +93,15 @@ interface ItemDetailListProps {
|
||||
onColumnResized?: (columnId: TableColumn, width: number) => void;
|
||||
onRangeChanged?: (range: { startIndex: number; stopIndex: number }) => Promise<void> | void;
|
||||
onScrollEnd?: (rowIndex: number) => void;
|
||||
onSongRowDoubleClick?: (params: {
|
||||
index: number;
|
||||
internalState: ItemListStateActions;
|
||||
item: Song;
|
||||
}) => void;
|
||||
overrideControls?: Partial<ItemControls>;
|
||||
rowHeight?: number;
|
||||
scrollOffset?: number;
|
||||
songsByAlbumId?: Record<string, Song[]>;
|
||||
tableId?: string;
|
||||
}
|
||||
|
||||
@@ -109,7 +117,13 @@ interface RowData {
|
||||
getItem?: (index: number) => unknown;
|
||||
internalState: ItemListStateActions;
|
||||
isMutatingFavorite: boolean;
|
||||
onSongRowDoubleClick?: (params: {
|
||||
index: number;
|
||||
internalState: ItemListStateActions;
|
||||
item: Song;
|
||||
}) => void;
|
||||
registerSongs: (albumId: string, songs: Song[]) => void;
|
||||
songsByAlbumId?: Record<string, Song[]>;
|
||||
trackColumns: ItemTableListColumnConfig[];
|
||||
trackTableSize: 'compact' | 'default' | 'large';
|
||||
}
|
||||
@@ -126,6 +140,11 @@ interface TrackRowProps {
|
||||
internalState: ItemListStateActions;
|
||||
isMutatingFavorite: boolean;
|
||||
isSongsLoading?: boolean;
|
||||
onSongRowDoubleClick?: (params: {
|
||||
index: number;
|
||||
internalState: ItemListStateActions;
|
||||
item: Song;
|
||||
}) => void;
|
||||
rowIndex: number;
|
||||
size: 'compact' | 'default' | 'large';
|
||||
song: Song;
|
||||
@@ -147,6 +166,7 @@ const TrackRow = memo(
|
||||
internalState,
|
||||
isMutatingFavorite,
|
||||
isSongsLoading,
|
||||
onSongRowDoubleClick,
|
||||
rowIndex,
|
||||
size,
|
||||
song,
|
||||
@@ -167,11 +187,37 @@ const TrackRow = memo(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onSongRowDoubleClick) {
|
||||
onSongRowDoubleClick({
|
||||
index: internalState.findItemIndex(song.id),
|
||||
internalState,
|
||||
item: song,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (controls?.onDoubleClick) {
|
||||
controls.onDoubleClick({
|
||||
event: e,
|
||||
index: internalState.findItemIndex(song.id),
|
||||
internalState,
|
||||
item: song,
|
||||
itemType: LibraryItem.SONG,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isSongsLoading || albumSongs.length === 0) return;
|
||||
internalState.setSelected([song]);
|
||||
playerContext.addToQueueByData(albumSongs, Play.NOW, song.id);
|
||||
},
|
||||
[albumSongs, internalState, isSongsLoading, playerContext, song],
|
||||
[
|
||||
albumSongs,
|
||||
controls,
|
||||
internalState,
|
||||
isSongsLoading,
|
||||
onSongRowDoubleClick,
|
||||
playerContext,
|
||||
song,
|
||||
],
|
||||
);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
@@ -610,7 +656,9 @@ const RowContent = memo(
|
||||
index,
|
||||
internalState,
|
||||
isMutatingFavorite,
|
||||
onSongRowDoubleClick,
|
||||
registerSongs,
|
||||
songsByAlbumId,
|
||||
trackColumns,
|
||||
trackTableSize,
|
||||
}: RowContentProps) => {
|
||||
@@ -622,8 +670,10 @@ const RowContent = memo(
|
||||
return (data?.[index] as Album | undefined) || undefined;
|
||||
}, [data, getItem, index]);
|
||||
|
||||
const useClientSideSongs = Boolean(songsByAlbumId);
|
||||
|
||||
const songListQuery = useMemo(() => {
|
||||
if (!item?.id || !item?._serverId) return null;
|
||||
if (useClientSideSongs || !item?.id || !item?._serverId) return null;
|
||||
return {
|
||||
query: {
|
||||
albumIds: [item.id],
|
||||
@@ -634,7 +684,7 @@ const RowContent = memo(
|
||||
},
|
||||
serverId: item?._serverId || '',
|
||||
};
|
||||
}, [item]);
|
||||
}, [item, useClientSideSongs]);
|
||||
|
||||
const { data: songListData, isLoading: isSongsQueryLoading } = useQuery({
|
||||
enabled: !!songListQuery,
|
||||
@@ -646,8 +696,17 @@ const RowContent = memo(
|
||||
}),
|
||||
});
|
||||
|
||||
const songItems = songListData?.items;
|
||||
const isSongsLoading = !!item && isSongsQueryLoading && !songItems?.length;
|
||||
const songItemsFromQuery = songListData?.items;
|
||||
const songItemsFromClient = useMemo(() => {
|
||||
const rowSongs = (item as { _playlistSongs?: Song[] })?._playlistSongs;
|
||||
if (rowSongs?.length) return rowSongs;
|
||||
if (!songsByAlbumId || !item?.id) return undefined;
|
||||
return songsByAlbumId[item.id];
|
||||
}, [item, songsByAlbumId]);
|
||||
|
||||
const songItems = useClientSideSongs ? songItemsFromClient : songItemsFromQuery;
|
||||
const isSongsLoading =
|
||||
!useClientSideSongs && !!item && isSongsQueryLoading && !songItemsFromQuery?.length;
|
||||
|
||||
const songs = useMemo(() => {
|
||||
return (
|
||||
@@ -705,6 +764,7 @@ const RowContent = memo(
|
||||
isMutatingFavorite={isMutatingFavorite}
|
||||
isSongsLoading={isSongsLoading}
|
||||
key={song.id}
|
||||
onSongRowDoubleClick={onSongRowDoubleClick}
|
||||
rowIndex={rowIndex}
|
||||
size={trackTableSize}
|
||||
song={song as Song}
|
||||
@@ -729,6 +789,7 @@ const RowContent = memo(
|
||||
prev.isMutatingFavorite === next.isMutatingFavorite &&
|
||||
prev.controls === next.controls &&
|
||||
prev.registerSongs === next.registerSongs &&
|
||||
prev.songsByAlbumId === next.songsByAlbumId &&
|
||||
prev.trackColumns === next.trackColumns &&
|
||||
prev.trackTableSize === next.trackTableSize,
|
||||
);
|
||||
@@ -1113,10 +1174,14 @@ export const ItemDetailList = ({
|
||||
getItem,
|
||||
itemCount: externalItemCount,
|
||||
items,
|
||||
listKey = ItemListKey.ALBUM,
|
||||
onColumnReordered,
|
||||
onColumnResized,
|
||||
onRangeChanged,
|
||||
onScrollEnd,
|
||||
onSongRowDoubleClick,
|
||||
overrideControls,
|
||||
songsByAlbumId,
|
||||
tableId = DEFAULT_DETAIL_TABLE_ID,
|
||||
}: ItemDetailListProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -1127,6 +1192,7 @@ export const ItemDetailList = ({
|
||||
const controls = useDefaultItemListControls({
|
||||
onColumnReordered,
|
||||
onColumnResized,
|
||||
overrides: overrideControls,
|
||||
});
|
||||
const isMutatingCreateFavorite = useIsMutatingCreateFavorite();
|
||||
const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite();
|
||||
@@ -1172,7 +1238,7 @@ export const ItemDetailList = ({
|
||||
|
||||
const internalState = useItemListState(getDataFn, extractRowIdSong);
|
||||
|
||||
const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.ALBUM]?.detail);
|
||||
const tableConfig = useSettingsStore((state) => state.lists[listKey]?.detail);
|
||||
const trackColumns = useMemo((): ItemTableListColumnConfig[] => {
|
||||
const raw = tableConfig?.columns;
|
||||
if (raw && raw.length > 0) {
|
||||
@@ -1263,8 +1329,10 @@ export const ItemDetailList = ({
|
||||
getItem,
|
||||
internalState,
|
||||
isMutatingFavorite,
|
||||
onSongRowDoubleClick,
|
||||
queryClient,
|
||||
registerSongs,
|
||||
songsByAlbumId,
|
||||
trackColumns,
|
||||
trackTableSize,
|
||||
}),
|
||||
@@ -1279,8 +1347,10 @@ export const ItemDetailList = ({
|
||||
getItem,
|
||||
internalState,
|
||||
isMutatingFavorite,
|
||||
onSongRowDoubleClick,
|
||||
queryClient,
|
||||
registerSongs,
|
||||
songsByAlbumId,
|
||||
trackColumns,
|
||||
trackTableSize,
|
||||
],
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
export type ListDisplayMode = LibraryItem.ALBUM | LibraryItem.SONG;
|
||||
|
||||
interface ListContextProps {
|
||||
customFilters?: Record<string, unknown>;
|
||||
displayMode?: ListDisplayMode;
|
||||
id?: string;
|
||||
isSidebarOpen?: boolean;
|
||||
isSmartPlaylist?: boolean;
|
||||
itemCount?: number;
|
||||
listData?: unknown[];
|
||||
listKey?: ItemListKey;
|
||||
mode?: 'edit' | 'view';
|
||||
pageKey: ItemListKey | string;
|
||||
setDisplayMode?: (displayMode: ListDisplayMode) => void;
|
||||
setIsSidebarOpen?: (isSidebarOpen: boolean) => void;
|
||||
setItemCount?: (itemCount: number) => void;
|
||||
setListData?: (items: unknown[]) => void;
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import { AnimatedPage } from '/@/renderer/features/shared/components/animated-pa
|
||||
import { JsonPreview } from '/@/renderer/features/shared/components/json-preview';
|
||||
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { PlaylistTarget, useCurrentServer, usePlaylistTarget } from '/@/renderer/store';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
@@ -29,7 +29,7 @@ import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { ServerType, SongListSort } from '/@/shared/types/domain-types';
|
||||
import { LibraryItem, ServerType, SongListSort } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
interface PlaylistQueryEditorProps {
|
||||
@@ -154,14 +154,17 @@ const PlaylistQueryEditor = ({
|
||||
}, [detailQuery?.data?.rules?.order, detailQuery?.data?.rules?.sort]);
|
||||
|
||||
return (
|
||||
<div className="query-editor-container">
|
||||
<Stack gap={0} h="100%" mah="30dvh" p="md" w="100%">
|
||||
<Group justify="space-between" pb="md" wrap="nowrap">
|
||||
<div
|
||||
className="query-editor-container"
|
||||
style={{ borderTop: '1px solid var(--theme-colors-border)' }}
|
||||
>
|
||||
<Stack gap={0} h="100%" mah="30dvh" p="sm" w="100%">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<Button
|
||||
leftSection={
|
||||
<Icon
|
||||
icon={isQueryBuilderExpanded ? 'arrowUpS' : 'arrowDownS'}
|
||||
icon={isQueryBuilderExpanded ? 'arrowDownS' : 'arrowUpS'}
|
||||
size="lg"
|
||||
/>
|
||||
}
|
||||
@@ -396,6 +399,12 @@ const PlaylistDetailSongListRoute = () => {
|
||||
setIsQueryBuilderExpanded(true);
|
||||
};
|
||||
|
||||
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[]>([]);
|
||||
const [mode, setMode] = useState<'edit' | 'view'>('view');
|
||||
@@ -403,17 +412,19 @@ const PlaylistDetailSongListRoute = () => {
|
||||
const providerValue = useMemo(() => {
|
||||
return {
|
||||
customFilters: undefined,
|
||||
displayMode,
|
||||
id: playlistId,
|
||||
isSmartPlaylist,
|
||||
itemCount,
|
||||
listData,
|
||||
listKey,
|
||||
mode,
|
||||
pageKey: ItemListKey.PLAYLIST_SONG,
|
||||
pageKey: listKey,
|
||||
setItemCount,
|
||||
setListData,
|
||||
setMode,
|
||||
};
|
||||
}, [playlistId, isSmartPlaylist, itemCount, listData, mode]);
|
||||
}, [playlistId, isSmartPlaylist, displayMode, listKey, itemCount, listData, mode]);
|
||||
|
||||
return (
|
||||
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
||||
@@ -429,6 +440,10 @@ const PlaylistDetailSongListRoute = () => {
|
||||
onDelete={() => openDeletePlaylistModal()}
|
||||
onToggleQueryBuilder={handleToggleShowQueryBuilder}
|
||||
/>
|
||||
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
<PlaylistDetailSongListContent />
|
||||
</Suspense>
|
||||
{(isSmartPlaylist || showQueryBuilder) && (
|
||||
<PlaylistQueryEditor
|
||||
createPlaylistMutation={createPlaylistMutation}
|
||||
@@ -441,9 +456,6 @@ const PlaylistDetailSongListRoute = () => {
|
||||
queryBuilderRef={queryBuilderRef}
|
||||
/>
|
||||
)}
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
<PlaylistDetailSongListContent />
|
||||
</Suspense>
|
||||
</ListContext.Provider>
|
||||
</AnimatedPage>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,75 @@
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
|
||||
import { NDSongQueryFields } from '/@/shared/api/navidrome/navidrome-types';
|
||||
import { Album, LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||
import { QueryBuilderGroup } from '/@/shared/types/types';
|
||||
|
||||
export type PlaylistAlbumRow = Album & { _playlistSongs?: Song[] };
|
||||
|
||||
export function playlistSongsToAlbums(songs: Song[]): PlaylistAlbumRow[] {
|
||||
if (songs.length === 0) return [];
|
||||
|
||||
const rows: PlaylistAlbumRow[] = [];
|
||||
let group: Song[] = [songs[0]];
|
||||
let prevAlbumId = songs[0].albumId;
|
||||
|
||||
const pushRow = (song: Song, groupSongs: Song[]) => {
|
||||
rows.push({
|
||||
_itemType: LibraryItem.ALBUM,
|
||||
_playlistSongs: groupSongs,
|
||||
_serverId: song._serverId,
|
||||
_serverType: song._serverType,
|
||||
albumArtistName: song.albumArtistName,
|
||||
albumArtists: song.albumArtists,
|
||||
artists: song.artists,
|
||||
comment: song.comment,
|
||||
createdAt: song.createdAt,
|
||||
duration: null,
|
||||
explicitStatus: song.explicitStatus,
|
||||
genres: song.genres,
|
||||
id: song.albumId,
|
||||
imageId: song.imageId,
|
||||
imageUrl: song.imageUrl,
|
||||
isCompilation: song.compilation,
|
||||
lastPlayedAt: song.lastPlayedAt,
|
||||
mbzId: null,
|
||||
mbzReleaseGroupId: null,
|
||||
name: song.album ?? '',
|
||||
originalDate: null,
|
||||
originalYear: null,
|
||||
participants: song.participants,
|
||||
playCount: null,
|
||||
recordLabels: [],
|
||||
releaseDate: song.releaseDate,
|
||||
releaseType: null,
|
||||
releaseTypes: [],
|
||||
releaseYear: song.releaseYear,
|
||||
size: null,
|
||||
songCount: null,
|
||||
sortName: song.album ?? '',
|
||||
tags: song.tags,
|
||||
updatedAt: song.updatedAt,
|
||||
userFavorite: false,
|
||||
userRating: null,
|
||||
version: null,
|
||||
});
|
||||
};
|
||||
|
||||
for (let i = 1; i < songs.length; i++) {
|
||||
const song = songs[i];
|
||||
if (song.albumId === prevAlbumId) {
|
||||
group.push(song);
|
||||
} else {
|
||||
pushRow(group[0], group);
|
||||
group = [song];
|
||||
prevAlbumId = song.albumId;
|
||||
}
|
||||
}
|
||||
pushRow(group[0], group);
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
export const parseQueryBuilderChildren = (groups: QueryBuilderGroup[], data: any[]) => {
|
||||
if (groups.length === 0) {
|
||||
return data;
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
.top-right {
|
||||
position: absolute;
|
||||
top: var(--theme-spacing-lg);
|
||||
right: var(--theme-spacing-md);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.library-header {
|
||||
position: relative;
|
||||
display: grid;
|
||||
@@ -56,6 +63,52 @@
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
&.compact {
|
||||
min-height: unset;
|
||||
padding: var(--theme-spacing-md) var(--theme-spacing-xs);
|
||||
|
||||
:global(.item-image-placeholder) {
|
||||
width: 250px !important;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 250px !important;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
@container (min-width: $mantine-breakpoint-sm) {
|
||||
grid-template-columns: 200px minmax(0, 1fr);
|
||||
min-height: unset;
|
||||
padding: var(--theme-spacing-md) var(--theme-spacing-sm);
|
||||
|
||||
.image {
|
||||
width: 200px !important;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
:global(.item-image-placeholder) {
|
||||
width: 200px !important;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: $mantine-breakpoint-lg) {
|
||||
grid-template-columns: 200px minmax(0, 1fr);
|
||||
padding: var(--theme-spacing-md) var(--theme-spacing-md);
|
||||
|
||||
.image {
|
||||
width: 200px !important;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
:global(.item-image-placeholder) {
|
||||
width: 200px !important;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-section {
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Play } from '/@/shared/types/types';
|
||||
|
||||
interface LibraryHeaderProps {
|
||||
children?: ReactNode;
|
||||
compact?: boolean;
|
||||
containerClassName?: string;
|
||||
imagePlaceholderUrl?: null | string;
|
||||
imageUrl?: null | string;
|
||||
@@ -45,11 +46,20 @@ interface LibraryHeaderProps {
|
||||
};
|
||||
loading?: boolean;
|
||||
title: string;
|
||||
topRight?: ReactNode;
|
||||
}
|
||||
|
||||
export const LibraryHeader = forwardRef(
|
||||
(
|
||||
{ children, containerClassName, imageUrl, item, title }: LibraryHeaderProps,
|
||||
{
|
||||
children,
|
||||
compact,
|
||||
containerClassName,
|
||||
imageUrl,
|
||||
item,
|
||||
title,
|
||||
topRight,
|
||||
}: LibraryHeaderProps,
|
||||
ref: Ref<HTMLDivElement>,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -125,7 +135,15 @@ export const LibraryHeader = forwardRef(
|
||||
}, [item.explicitStatus, item.imageId, item.type]);
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.libraryHeader, containerClassName)} ref={ref}>
|
||||
<div
|
||||
className={clsx(
|
||||
styles.libraryHeader,
|
||||
containerClassName,
|
||||
compact && styles.compact,
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
{topRight && <div className={styles.topRight}>{topRight}</div>}
|
||||
<div
|
||||
className={styles.imageSection}
|
||||
onClick={() => {
|
||||
|
||||
@@ -224,6 +224,11 @@ export const CLIENT_SIDE_ALBUM_FILTERS = [
|
||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.ALBUM_ARTIST,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.ID,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
|
||||
@@ -295,6 +300,11 @@ const ALBUM_LIST_FILTERS: Partial<
|
||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.ALBUM_ARTIST,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.ID,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.communityRating', { postProcess: 'titleCase' }),
|
||||
@@ -337,6 +347,11 @@ const ALBUM_LIST_FILTERS: Partial<
|
||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.ALBUM_ARTIST,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.ID,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
|
||||
@@ -399,6 +414,11 @@ const ALBUM_LIST_FILTERS: Partial<
|
||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.ALBUM_ARTIST,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
|
||||
value: AlbumListSort.ID,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
|
||||
|
||||
@@ -152,6 +152,8 @@ const DiscordLinkTypeSchema = z.enum(['last_fm', 'musicbrainz', 'musicbrainz_las
|
||||
|
||||
const GenreTargetSchema = z.enum(['album', 'track']);
|
||||
|
||||
const PlaylistTargetSchema = z.enum(['album', 'track']);
|
||||
|
||||
const SideQueueTypeSchema = z.enum(['sideDrawerQueue', 'sideQueue']);
|
||||
|
||||
const SidebarPanelTypeSchema = z.enum(['queue', 'lyrics', 'visualizer']);
|
||||
@@ -458,6 +460,7 @@ export const GeneralSettingsSchema = z.object({
|
||||
playButtonBehavior: z.nativeEnum(Play),
|
||||
playerbarOpenDrawer: z.boolean(),
|
||||
playerbarSlider: PlayerbarSliderSchema,
|
||||
playlistTarget: PlaylistTargetSchema,
|
||||
resume: z.boolean(),
|
||||
showLyricsInSidebar: z.boolean(),
|
||||
showRatings: z.boolean(),
|
||||
@@ -775,6 +778,11 @@ export enum PlayerbarSliderType {
|
||||
WAVEFORM = 'waveform',
|
||||
}
|
||||
|
||||
export enum PlaylistTarget {
|
||||
ALBUM = 'album',
|
||||
TRACK = 'track',
|
||||
}
|
||||
|
||||
export enum SidebarItem {
|
||||
ALBUMS = 'Albums',
|
||||
ARTISTS = 'Artists',
|
||||
@@ -829,6 +837,7 @@ export interface SettingsSlice extends z.infer<typeof SettingsStateSchema> {
|
||||
setHomeItems: (item: SortableItem<HomeItem>[]) => void;
|
||||
setList: (type: ItemListKey, data: DeepPartial<ItemListSettings>) => void;
|
||||
setPlaybackFilters: (filters: PlayerFilter[]) => void;
|
||||
setPlaylistBehavior: (target: PlaylistTarget) => void;
|
||||
setSettings: (data: DeepPartial<SettingsState>) => void;
|
||||
setSidebarItems: (items: SidebarItemType[]) => void;
|
||||
setTable: (type: ItemListKey, data: DataTableProps) => void;
|
||||
@@ -1039,6 +1048,7 @@ const initialState: SettingsState = {
|
||||
barWidth: 2,
|
||||
type: PlayerbarSliderType.SLIDER,
|
||||
},
|
||||
playlistTarget: PlaylistTarget.TRACK,
|
||||
resume: true,
|
||||
showLyricsInSidebar: true,
|
||||
showRatings: true,
|
||||
@@ -1175,6 +1185,83 @@ const initialState: SettingsState = {
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
[ItemListKey.PLAYLIST_ALBUM]: {
|
||||
detail: {
|
||||
columns: pickTableColumns({
|
||||
autoSizeColumns: [],
|
||||
columns: SONG_TABLE_COLUMNS,
|
||||
columnWidths: {
|
||||
[TableColumn.ACTIONS]: 60,
|
||||
[TableColumn.DURATION]: 100,
|
||||
[TableColumn.TITLE]: 400,
|
||||
[TableColumn.TRACK_NUMBER]: 50,
|
||||
[TableColumn.USER_FAVORITE]: 60,
|
||||
},
|
||||
enabledColumns: [
|
||||
TableColumn.TRACK_NUMBER,
|
||||
TableColumn.TITLE,
|
||||
TableColumn.DURATION,
|
||||
TableColumn.USER_FAVORITE,
|
||||
TableColumn.ACTIONS,
|
||||
],
|
||||
}),
|
||||
enableAlternateRowColors: false,
|
||||
enableHeader: true,
|
||||
enableHorizontalBorders: false,
|
||||
enableRowHoverHighlight: true,
|
||||
enableVerticalBorders: false,
|
||||
size: 'compact',
|
||||
},
|
||||
display: ListDisplayType.GRID,
|
||||
grid: {
|
||||
itemGap: 'sm',
|
||||
itemsPerRow: 6,
|
||||
itemsPerRowEnabled: false,
|
||||
rows: pickGridRows({
|
||||
alignLeftColumns: [
|
||||
TableColumn.TITLE,
|
||||
TableColumn.ALBUM_ARTIST,
|
||||
TableColumn.YEAR,
|
||||
],
|
||||
columns: ALBUM_TABLE_COLUMNS,
|
||||
enabledColumns: [TableColumn.TITLE, TableColumn.ALBUM_ARTIST, TableColumn.YEAR],
|
||||
pickColumns: [
|
||||
TableColumn.TITLE,
|
||||
TableColumn.DURATION,
|
||||
TableColumn.ALBUM_ARTIST,
|
||||
TableColumn.BIT_RATE,
|
||||
TableColumn.BPM,
|
||||
TableColumn.DATE_ADDED,
|
||||
TableColumn.GENRE,
|
||||
TableColumn.PLAY_COUNT,
|
||||
TableColumn.SONG_COUNT,
|
||||
TableColumn.RELEASE_DATE,
|
||||
TableColumn.LAST_PLAYED,
|
||||
TableColumn.YEAR,
|
||||
],
|
||||
}),
|
||||
size: 'default',
|
||||
},
|
||||
itemsPerPage: 100,
|
||||
pagination: ListPaginationType.INFINITE,
|
||||
table: {
|
||||
autoFitColumns: true,
|
||||
columns: ALBUM_TABLE_COLUMNS.map((column) => ({
|
||||
align: column.align,
|
||||
autoSize: column.autoSize,
|
||||
id: column.value,
|
||||
isEnabled: column.isEnabled,
|
||||
pinned: column.pinned,
|
||||
width: column.width,
|
||||
})),
|
||||
enableAlternateRowColors: false,
|
||||
enableHeader: true,
|
||||
enableHorizontalBorders: false,
|
||||
enableRowHoverHighlight: true,
|
||||
enableVerticalBorders: false,
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
[LibraryItem.ALBUM]: {
|
||||
detail: {
|
||||
columns: pickTableColumns({
|
||||
@@ -1808,6 +1895,11 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
||||
state.playback.filters = filters;
|
||||
});
|
||||
},
|
||||
setPlaylistBehavior: (target: PlaylistTarget) => {
|
||||
set((state) => {
|
||||
state.general.playlistTarget = target;
|
||||
});
|
||||
},
|
||||
setSettings: (data) => {
|
||||
set((state) => {
|
||||
deepMergeIntoState(state, data);
|
||||
@@ -2218,6 +2310,9 @@ export const usePlayerbarSlider = () =>
|
||||
|
||||
export const useGenreTarget = () => useSettingsStore((store) => store.general.genreTarget, shallow);
|
||||
|
||||
export const usePlaylistTarget = () =>
|
||||
useSettingsStore((store) => store.general.playlistTarget, shallow);
|
||||
|
||||
export const useLanguage = () => useSettingsStore((state) => state.general.language, shallow);
|
||||
|
||||
export const useAccent = () => useSettingsStore((state) => state.general.accent, shallow);
|
||||
|
||||
+82
-17
@@ -151,7 +151,7 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
||||
results = orderBy(
|
||||
results,
|
||||
[(v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, 'asc', 'asc'],
|
||||
[order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -159,7 +159,7 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
||||
results = orderBy(
|
||||
results,
|
||||
[(v) => v.albumArtists[0]?.name.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, order, 'asc', 'asc'],
|
||||
[order, order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -167,32 +167,54 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
||||
results = orderBy(
|
||||
results,
|
||||
[(v) => v.artistName?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, order, 'asc', 'asc'],
|
||||
[order, order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.BPM:
|
||||
results = orderBy(results, ['bpm'], [order]);
|
||||
results = orderBy(
|
||||
results,
|
||||
['bpm', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.CHANNELS:
|
||||
results = orderBy(results, ['channels'], [order]);
|
||||
results = orderBy(
|
||||
results,
|
||||
['channels', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.COMMENT:
|
||||
results = orderBy(
|
||||
results,
|
||||
['comment', 'discNumber', 'trackNumber'],
|
||||
[order, order, 'asc', 'asc'],
|
||||
['comment', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.DURATION:
|
||||
results = orderBy(results, ['duration'], [order]);
|
||||
results = orderBy(
|
||||
results,
|
||||
['duration', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.FAVORITED:
|
||||
results = orderBy(results, ['userFavorite', (v) => v.name.toLowerCase()], [order]);
|
||||
results = orderBy(
|
||||
results,
|
||||
[
|
||||
'userFavorite',
|
||||
(v) => v.name.toLowerCase(),
|
||||
(v) => v.album?.toLowerCase(),
|
||||
'discNumber',
|
||||
'trackNumber',
|
||||
],
|
||||
[order, order, order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.GENRE:
|
||||
@@ -204,7 +226,7 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
||||
'discNumber',
|
||||
'trackNumber',
|
||||
],
|
||||
[order, order, 'asc', 'asc'],
|
||||
[order, order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -217,11 +239,19 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
||||
break;
|
||||
|
||||
case SongListSort.NAME:
|
||||
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
|
||||
results = orderBy(
|
||||
results,
|
||||
[(v) => v.name.toLowerCase(), (v) => v.album?.toLowerCase()],
|
||||
[order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.PLAY_COUNT:
|
||||
results = orderBy(results, ['playCount'], [order]);
|
||||
results = orderBy(
|
||||
results,
|
||||
['playCount', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.RANDOM:
|
||||
@@ -229,19 +259,51 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
||||
break;
|
||||
|
||||
case SongListSort.RATING:
|
||||
results = orderBy(results, ['userRating', (v) => v.name.toLowerCase()], [order]);
|
||||
results = orderBy(
|
||||
results,
|
||||
[
|
||||
'userRating',
|
||||
(v) => v.name.toLowerCase(),
|
||||
(v) => v.album?.toLowerCase(),
|
||||
'discNumber',
|
||||
'trackNumber',
|
||||
],
|
||||
[order, order, order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.RECENTLY_ADDED:
|
||||
results = orderBy(results, ['createdAt'], [order]);
|
||||
results = orderBy(
|
||||
results,
|
||||
[
|
||||
(v) => {
|
||||
const x = v.createdAt;
|
||||
if (x == null) return null;
|
||||
const d = new Date(x);
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
||||
},
|
||||
(v) => v.album?.toLowerCase(),
|
||||
'discNumber',
|
||||
'trackNumber',
|
||||
],
|
||||
[order, order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.RECENTLY_PLAYED:
|
||||
results = orderBy(results, ['lastPlayedAt'], [order]);
|
||||
results = orderBy(
|
||||
results,
|
||||
['lastPlayedAt', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.RELEASE_DATE:
|
||||
results = orderBy(results, ['releaseDate'], [order]);
|
||||
results = orderBy(
|
||||
results,
|
||||
['releaseDate', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.SORT_NAME:
|
||||
@@ -252,7 +314,7 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
||||
results = orderBy(
|
||||
results,
|
||||
['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
|
||||
[order, 'asc', 'asc', 'asc'],
|
||||
[order, order, order, order],
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -404,6 +466,9 @@ export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder:
|
||||
case AlbumListSort.FAVORITED:
|
||||
results = orderBy(results, ['starred'], [order]);
|
||||
break;
|
||||
case AlbumListSort.ID:
|
||||
results = sortOrder === SortOrder.DESC ? [...results].reverse() : results;
|
||||
break;
|
||||
case AlbumListSort.NAME:
|
||||
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
|
||||
break;
|
||||
|
||||
@@ -466,6 +466,7 @@ export enum AlbumListSort {
|
||||
DURATION = 'duration',
|
||||
EXPLICIT_STATUS = 'explicitStatus',
|
||||
FAVORITED = 'favorited',
|
||||
ID = 'id',
|
||||
NAME = 'name',
|
||||
PLAY_COUNT = 'playCount',
|
||||
RANDOM = 'random',
|
||||
@@ -521,6 +522,7 @@ export const albumListSortMap: AlbumListSortMap = {
|
||||
duration: undefined,
|
||||
explicitStatus: undefined,
|
||||
favorited: undefined,
|
||||
id: undefined,
|
||||
name: JFAlbumListSort.NAME,
|
||||
playCount: JFAlbumListSort.PLAY_COUNT,
|
||||
random: JFAlbumListSort.RANDOM,
|
||||
@@ -540,6 +542,7 @@ export const albumListSortMap: AlbumListSortMap = {
|
||||
duration: NDAlbumListSort.DURATION,
|
||||
explicitStatus: NDAlbumListSort.EXPLICIT_STATUS,
|
||||
favorited: NDAlbumListSort.STARRED,
|
||||
id: undefined,
|
||||
name: NDAlbumListSort.NAME,
|
||||
playCount: NDAlbumListSort.PLAY_COUNT,
|
||||
random: NDAlbumListSort.RANDOM,
|
||||
@@ -560,6 +563,7 @@ export const albumListSortMap: AlbumListSortMap = {
|
||||
duration: undefined,
|
||||
explicitStatus: undefined,
|
||||
favorited: undefined,
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
playCount: undefined,
|
||||
random: undefined,
|
||||
|
||||
@@ -26,6 +26,7 @@ export enum ItemListKey {
|
||||
GENRE_ALBUM = 'genreAlbum',
|
||||
GENRE_SONG = 'genreSong',
|
||||
PLAYLIST = LibraryItem.PLAYLIST,
|
||||
PLAYLIST_ALBUM = 'playlistAlbum',
|
||||
PLAYLIST_SONG = LibraryItem.PLAYLIST_SONG,
|
||||
QUEUE_SONG = LibraryItem.QUEUE_SONG,
|
||||
RADIO = 'radio',
|
||||
|
||||
Reference in New Issue
Block a user