mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
maintain song order in album view
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,45 +2,25 @@ import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
|
|||||||
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
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 { 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 { ItemListHandle } from '/@/renderer/components/item-list/types';
|
||||||
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
|
||||||
import {
|
|
||||||
DefaultItemControlProps,
|
|
||||||
ItemControls,
|
|
||||||
ItemListHandle,
|
|
||||||
} from '/@/renderer/components/item-list/types';
|
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
|
||||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
import { 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 { usePlaylistTrackList } from '/@/renderer/features/playlists/hooks/use-playlist-track-list';
|
||||||
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
|
import { useCurrentServer, useListSettings } from '/@/renderer/store';
|
||||||
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
|
||||||
import { useCurrentServer, useGeneralSettings, useListSettings } from '/@/renderer/store';
|
|
||||||
import { sortAlbumList } from '/@/shared/api/utils';
|
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import {
|
import {
|
||||||
Album,
|
|
||||||
AlbumListSort,
|
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
PlaylistSongListQuery,
|
PlaylistSongListQuery,
|
||||||
PlaylistSongListResponse,
|
PlaylistSongListResponse,
|
||||||
Song,
|
Song,
|
||||||
SortOrder,
|
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import {
|
import {
|
||||||
ItemListKey,
|
ItemListKey,
|
||||||
ListDisplayType,
|
ListDisplayType,
|
||||||
ListPaginationType,
|
ListPaginationType,
|
||||||
Play,
|
|
||||||
TableColumn,
|
TableColumn,
|
||||||
} from '/@/shared/types/types';
|
} from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -303,237 +283,6 @@ export const PlaylistDetailSongListEdit = ({ data }: { data: PlaylistSongListRes
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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 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 { sortBy } = useSortByFilter<AlbumListSort>(AlbumListSort.ID, ItemListKey.PLAYLIST_ALBUM);
|
|
||||||
const { sortOrder } = useSortOrderFilter(SortOrder.ASC, ItemListKey.PLAYLIST_ALBUM);
|
|
||||||
|
|
||||||
const albums = useMemo(() => playlistSongsToAlbums(data?.items ?? []), [data?.items]);
|
|
||||||
const sortedAlbums = useMemo(
|
|
||||||
() =>
|
|
||||||
sortAlbumList(
|
|
||||||
albums,
|
|
||||||
(sortBy as AlbumListSort) ?? AlbumListSort.ID,
|
|
||||||
sortOrder ?? SortOrder.ASC,
|
|
||||||
),
|
|
||||||
[albums, sortBy, 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();
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Track view: view mode uses centralized list derivation; edit mode uses local reorder state. */
|
/** Track view: view mode uses centralized list derivation; edit mode uses local reorder state. */
|
||||||
const PlaylistDetailTrackView = ({ data }: { data: PlaylistSongListResponse }) => {
|
const PlaylistDetailTrackView = ({ data }: { data: PlaylistSongListResponse }) => {
|
||||||
const { isSmartPlaylist, mode } = useListContext();
|
const { isSmartPlaylist, mode } = useListContext();
|
||||||
|
|||||||
+13
-32
@@ -35,7 +35,7 @@ import { Group } from '/@/shared/components/group/group';
|
|||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
|
||||||
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
||||||
import { AlbumListSort, LibraryItem, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
import { LibraryItem, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface PlaylistDetailSongListHeaderFiltersProps {
|
interface PlaylistDetailSongListHeaderFiltersProps {
|
||||||
@@ -103,37 +103,18 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
{toggleChoice}
|
{toggleChoice}
|
||||||
</Button>
|
</Button>
|
||||||
<Divider orientation="vertical" />
|
<Divider orientation="vertical" />
|
||||||
{isAlbumMode ? (
|
<ListSortByDropdown
|
||||||
<>
|
defaultSortByValue={SongListSort.ID}
|
||||||
<ListSortByDropdown
|
disabled={isEditMode}
|
||||||
defaultSortByValue={AlbumListSort.ID}
|
itemType={LibraryItem.PLAYLIST_SONG}
|
||||||
disabled={isEditMode}
|
listKey={ItemListKey.PLAYLIST_SONG}
|
||||||
itemType={LibraryItem.ALBUM}
|
/>
|
||||||
listKey={listKey}
|
<Divider orientation="vertical" />
|
||||||
/>
|
<ListSortOrderToggleButton
|
||||||
<Divider orientation="vertical" />
|
defaultSortOrder={SortOrder.ASC}
|
||||||
<ListSortOrderToggleButton
|
disabled={isEditMode}
|
||||||
defaultSortOrder={SortOrder.ASC}
|
listKey={ItemListKey.PLAYLIST_SONG}
|
||||||
disabled={isEditMode}
|
/>
|
||||||
listKey={listKey}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ListSortByDropdown
|
|
||||||
defaultSortByValue={SongListSort.ID}
|
|
||||||
disabled={isEditMode}
|
|
||||||
itemType={LibraryItem.PLAYLIST_SONG}
|
|
||||||
listKey={listKey}
|
|
||||||
/>
|
|
||||||
<Divider orientation="vertical" />
|
|
||||||
<ListSortOrderToggleButton
|
|
||||||
defaultSortOrder={SortOrder.ASC}
|
|
||||||
disabled={isEditMode}
|
|
||||||
listKey={listKey}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!collapsed && <ListSearchInput />}
|
{!collapsed && <ListSearchInput />}
|
||||||
<ListRefreshButton disabled={isEditMode} listKey={listKey} />
|
<ListRefreshButton disabled={isEditMode} listKey={listKey} />
|
||||||
<MoreButton onClick={handleMore} />
|
<MoreButton onClick={handleMore} />
|
||||||
|
|||||||
@@ -1,8 +1,75 @@
|
|||||||
import { nanoid } from 'nanoid/non-secure';
|
import { nanoid } from 'nanoid/non-secure';
|
||||||
|
|
||||||
import { NDSongQueryFields } from '/@/shared/api/navidrome/navidrome-types';
|
import { NDSongQueryFields } from '/@/shared/api/navidrome/navidrome-types';
|
||||||
|
import { Album, LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||||
import { QueryBuilderGroup } from '/@/shared/types/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[]) => {
|
export const parseQueryBuilderChildren = (groups: QueryBuilderGroup[], data: any[]) => {
|
||||||
if (groups.length === 0) {
|
if (groups.length === 0) {
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
Reference in New Issue
Block a user