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.DURATION]: undefined,
|
||||||
[AlbumListSort.EXPLICIT_STATUS]: undefined,
|
[AlbumListSort.EXPLICIT_STATUS]: undefined,
|
||||||
[AlbumListSort.FAVORITED]: AlbumListSortType.STARRED,
|
[AlbumListSort.FAVORITED]: AlbumListSortType.STARRED,
|
||||||
|
[AlbumListSort.ID]: undefined,
|
||||||
[AlbumListSort.NAME]: AlbumListSortType.ALPHABETICAL_BY_NAME,
|
[AlbumListSort.NAME]: AlbumListSortType.ALPHABETICAL_BY_NAME,
|
||||||
[AlbumListSort.PLAY_COUNT]: AlbumListSortType.FREQUENT,
|
[AlbumListSort.PLAY_COUNT]: AlbumListSortType.FREQUENT,
|
||||||
[AlbumListSort.RANDOM]: AlbumListSortType.RANDOM,
|
[AlbumListSort.RANDOM]: AlbumListSortType.RANDOM,
|
||||||
|
|||||||
@@ -244,8 +244,6 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
|||||||
const playType = (meta?.playType as Play) || Play.NOW;
|
const playType = (meta?.playType as Play) || Play.NOW;
|
||||||
const singleSongOnly = meta?.singleSongOnly === true;
|
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[];
|
let songsToAdd: Song[];
|
||||||
if (
|
if (
|
||||||
singleSongOnly ||
|
singleSongOnly ||
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ interface ItemDetailListProps {
|
|||||||
internalState?: ItemListStateActions;
|
internalState?: ItemListStateActions;
|
||||||
itemCount?: number;
|
itemCount?: number;
|
||||||
items?: unknown[];
|
items?: unknown[];
|
||||||
|
listKey?: ItemListKey;
|
||||||
onColumnReordered?: (
|
onColumnReordered?: (
|
||||||
columnIdFrom: TableColumn,
|
columnIdFrom: TableColumn,
|
||||||
columnIdTo: TableColumn,
|
columnIdTo: TableColumn,
|
||||||
@@ -92,8 +93,15 @@ interface ItemDetailListProps {
|
|||||||
onColumnResized?: (columnId: TableColumn, width: number) => void;
|
onColumnResized?: (columnId: TableColumn, width: number) => void;
|
||||||
onRangeChanged?: (range: { startIndex: number; stopIndex: number }) => Promise<void> | void;
|
onRangeChanged?: (range: { startIndex: number; stopIndex: number }) => Promise<void> | void;
|
||||||
onScrollEnd?: (rowIndex: number) => void;
|
onScrollEnd?: (rowIndex: number) => void;
|
||||||
|
onSongRowDoubleClick?: (params: {
|
||||||
|
index: number;
|
||||||
|
internalState: ItemListStateActions;
|
||||||
|
item: Song;
|
||||||
|
}) => void;
|
||||||
|
overrideControls?: Partial<ItemControls>;
|
||||||
rowHeight?: number;
|
rowHeight?: number;
|
||||||
scrollOffset?: number;
|
scrollOffset?: number;
|
||||||
|
songsByAlbumId?: Record<string, Song[]>;
|
||||||
tableId?: string;
|
tableId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +117,13 @@ interface RowData {
|
|||||||
getItem?: (index: number) => unknown;
|
getItem?: (index: number) => unknown;
|
||||||
internalState: ItemListStateActions;
|
internalState: ItemListStateActions;
|
||||||
isMutatingFavorite: boolean;
|
isMutatingFavorite: boolean;
|
||||||
|
onSongRowDoubleClick?: (params: {
|
||||||
|
index: number;
|
||||||
|
internalState: ItemListStateActions;
|
||||||
|
item: Song;
|
||||||
|
}) => void;
|
||||||
registerSongs: (albumId: string, songs: Song[]) => void;
|
registerSongs: (albumId: string, songs: Song[]) => void;
|
||||||
|
songsByAlbumId?: Record<string, Song[]>;
|
||||||
trackColumns: ItemTableListColumnConfig[];
|
trackColumns: ItemTableListColumnConfig[];
|
||||||
trackTableSize: 'compact' | 'default' | 'large';
|
trackTableSize: 'compact' | 'default' | 'large';
|
||||||
}
|
}
|
||||||
@@ -126,6 +140,11 @@ interface TrackRowProps {
|
|||||||
internalState: ItemListStateActions;
|
internalState: ItemListStateActions;
|
||||||
isMutatingFavorite: boolean;
|
isMutatingFavorite: boolean;
|
||||||
isSongsLoading?: boolean;
|
isSongsLoading?: boolean;
|
||||||
|
onSongRowDoubleClick?: (params: {
|
||||||
|
index: number;
|
||||||
|
internalState: ItemListStateActions;
|
||||||
|
item: Song;
|
||||||
|
}) => void;
|
||||||
rowIndex: number;
|
rowIndex: number;
|
||||||
size: 'compact' | 'default' | 'large';
|
size: 'compact' | 'default' | 'large';
|
||||||
song: Song;
|
song: Song;
|
||||||
@@ -147,6 +166,7 @@ const TrackRow = memo(
|
|||||||
internalState,
|
internalState,
|
||||||
isMutatingFavorite,
|
isMutatingFavorite,
|
||||||
isSongsLoading,
|
isSongsLoading,
|
||||||
|
onSongRowDoubleClick,
|
||||||
rowIndex,
|
rowIndex,
|
||||||
size,
|
size,
|
||||||
song,
|
song,
|
||||||
@@ -167,11 +187,37 @@ const TrackRow = memo(
|
|||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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;
|
if (isSongsLoading || albumSongs.length === 0) return;
|
||||||
internalState.setSelected([song]);
|
internalState.setSelected([song]);
|
||||||
playerContext.addToQueueByData(albumSongs, Play.NOW, song.id);
|
playerContext.addToQueueByData(albumSongs, Play.NOW, song.id);
|
||||||
},
|
},
|
||||||
[albumSongs, internalState, isSongsLoading, playerContext, song],
|
[
|
||||||
|
albumSongs,
|
||||||
|
controls,
|
||||||
|
internalState,
|
||||||
|
isSongsLoading,
|
||||||
|
onSongRowDoubleClick,
|
||||||
|
playerContext,
|
||||||
|
song,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRowClick = useCallback(
|
const handleRowClick = useCallback(
|
||||||
@@ -610,7 +656,9 @@ const RowContent = memo(
|
|||||||
index,
|
index,
|
||||||
internalState,
|
internalState,
|
||||||
isMutatingFavorite,
|
isMutatingFavorite,
|
||||||
|
onSongRowDoubleClick,
|
||||||
registerSongs,
|
registerSongs,
|
||||||
|
songsByAlbumId,
|
||||||
trackColumns,
|
trackColumns,
|
||||||
trackTableSize,
|
trackTableSize,
|
||||||
}: RowContentProps) => {
|
}: RowContentProps) => {
|
||||||
@@ -622,8 +670,10 @@ const RowContent = memo(
|
|||||||
return (data?.[index] as Album | undefined) || undefined;
|
return (data?.[index] as Album | undefined) || undefined;
|
||||||
}, [data, getItem, index]);
|
}, [data, getItem, index]);
|
||||||
|
|
||||||
|
const useClientSideSongs = Boolean(songsByAlbumId);
|
||||||
|
|
||||||
const songListQuery = useMemo(() => {
|
const songListQuery = useMemo(() => {
|
||||||
if (!item?.id || !item?._serverId) return null;
|
if (useClientSideSongs || !item?.id || !item?._serverId) return null;
|
||||||
return {
|
return {
|
||||||
query: {
|
query: {
|
||||||
albumIds: [item.id],
|
albumIds: [item.id],
|
||||||
@@ -634,7 +684,7 @@ const RowContent = memo(
|
|||||||
},
|
},
|
||||||
serverId: item?._serverId || '',
|
serverId: item?._serverId || '',
|
||||||
};
|
};
|
||||||
}, [item]);
|
}, [item, useClientSideSongs]);
|
||||||
|
|
||||||
const { data: songListData, isLoading: isSongsQueryLoading } = useQuery({
|
const { data: songListData, isLoading: isSongsQueryLoading } = useQuery({
|
||||||
enabled: !!songListQuery,
|
enabled: !!songListQuery,
|
||||||
@@ -646,8 +696,17 @@ const RowContent = memo(
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const songItems = songListData?.items;
|
const songItemsFromQuery = songListData?.items;
|
||||||
const isSongsLoading = !!item && isSongsQueryLoading && !songItems?.length;
|
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(() => {
|
const songs = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@@ -705,6 +764,7 @@ const RowContent = memo(
|
|||||||
isMutatingFavorite={isMutatingFavorite}
|
isMutatingFavorite={isMutatingFavorite}
|
||||||
isSongsLoading={isSongsLoading}
|
isSongsLoading={isSongsLoading}
|
||||||
key={song.id}
|
key={song.id}
|
||||||
|
onSongRowDoubleClick={onSongRowDoubleClick}
|
||||||
rowIndex={rowIndex}
|
rowIndex={rowIndex}
|
||||||
size={trackTableSize}
|
size={trackTableSize}
|
||||||
song={song as Song}
|
song={song as Song}
|
||||||
@@ -729,6 +789,7 @@ const RowContent = memo(
|
|||||||
prev.isMutatingFavorite === next.isMutatingFavorite &&
|
prev.isMutatingFavorite === next.isMutatingFavorite &&
|
||||||
prev.controls === next.controls &&
|
prev.controls === next.controls &&
|
||||||
prev.registerSongs === next.registerSongs &&
|
prev.registerSongs === next.registerSongs &&
|
||||||
|
prev.songsByAlbumId === next.songsByAlbumId &&
|
||||||
prev.trackColumns === next.trackColumns &&
|
prev.trackColumns === next.trackColumns &&
|
||||||
prev.trackTableSize === next.trackTableSize,
|
prev.trackTableSize === next.trackTableSize,
|
||||||
);
|
);
|
||||||
@@ -1113,10 +1174,14 @@ export const ItemDetailList = ({
|
|||||||
getItem,
|
getItem,
|
||||||
itemCount: externalItemCount,
|
itemCount: externalItemCount,
|
||||||
items,
|
items,
|
||||||
|
listKey = ItemListKey.ALBUM,
|
||||||
onColumnReordered,
|
onColumnReordered,
|
||||||
onColumnResized,
|
onColumnResized,
|
||||||
onRangeChanged,
|
onRangeChanged,
|
||||||
onScrollEnd,
|
onScrollEnd,
|
||||||
|
onSongRowDoubleClick,
|
||||||
|
overrideControls,
|
||||||
|
songsByAlbumId,
|
||||||
tableId = DEFAULT_DETAIL_TABLE_ID,
|
tableId = DEFAULT_DETAIL_TABLE_ID,
|
||||||
}: ItemDetailListProps) => {
|
}: ItemDetailListProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -1127,6 +1192,7 @@ export const ItemDetailList = ({
|
|||||||
const controls = useDefaultItemListControls({
|
const controls = useDefaultItemListControls({
|
||||||
onColumnReordered,
|
onColumnReordered,
|
||||||
onColumnResized,
|
onColumnResized,
|
||||||
|
overrides: overrideControls,
|
||||||
});
|
});
|
||||||
const isMutatingCreateFavorite = useIsMutatingCreateFavorite();
|
const isMutatingCreateFavorite = useIsMutatingCreateFavorite();
|
||||||
const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite();
|
const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite();
|
||||||
@@ -1172,7 +1238,7 @@ export const ItemDetailList = ({
|
|||||||
|
|
||||||
const internalState = useItemListState(getDataFn, extractRowIdSong);
|
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 trackColumns = useMemo((): ItemTableListColumnConfig[] => {
|
||||||
const raw = tableConfig?.columns;
|
const raw = tableConfig?.columns;
|
||||||
if (raw && raw.length > 0) {
|
if (raw && raw.length > 0) {
|
||||||
@@ -1263,8 +1329,10 @@ export const ItemDetailList = ({
|
|||||||
getItem,
|
getItem,
|
||||||
internalState,
|
internalState,
|
||||||
isMutatingFavorite,
|
isMutatingFavorite,
|
||||||
|
onSongRowDoubleClick,
|
||||||
queryClient,
|
queryClient,
|
||||||
registerSongs,
|
registerSongs,
|
||||||
|
songsByAlbumId,
|
||||||
trackColumns,
|
trackColumns,
|
||||||
trackTableSize,
|
trackTableSize,
|
||||||
}),
|
}),
|
||||||
@@ -1279,8 +1347,10 @@ export const ItemDetailList = ({
|
|||||||
getItem,
|
getItem,
|
||||||
internalState,
|
internalState,
|
||||||
isMutatingFavorite,
|
isMutatingFavorite,
|
||||||
|
onSongRowDoubleClick,
|
||||||
queryClient,
|
queryClient,
|
||||||
registerSongs,
|
registerSongs,
|
||||||
|
songsByAlbumId,
|
||||||
trackColumns,
|
trackColumns,
|
||||||
trackTableSize,
|
trackTableSize,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export type ListDisplayMode = LibraryItem.ALBUM | LibraryItem.SONG;
|
||||||
|
|
||||||
interface ListContextProps {
|
interface ListContextProps {
|
||||||
customFilters?: Record<string, unknown>;
|
customFilters?: Record<string, unknown>;
|
||||||
|
displayMode?: ListDisplayMode;
|
||||||
id?: string;
|
id?: string;
|
||||||
isSidebarOpen?: boolean;
|
isSidebarOpen?: boolean;
|
||||||
isSmartPlaylist?: boolean;
|
isSmartPlaylist?: boolean;
|
||||||
itemCount?: number;
|
itemCount?: number;
|
||||||
listData?: unknown[];
|
listData?: unknown[];
|
||||||
|
listKey?: ItemListKey;
|
||||||
mode?: 'edit' | 'view';
|
mode?: 'edit' | 'view';
|
||||||
pageKey: ItemListKey | string;
|
pageKey: ItemListKey | string;
|
||||||
|
setDisplayMode?: (displayMode: ListDisplayMode) => void;
|
||||||
setIsSidebarOpen?: (isSidebarOpen: boolean) => void;
|
setIsSidebarOpen?: (isSidebarOpen: boolean) => void;
|
||||||
setItemCount?: (itemCount: number) => void;
|
setItemCount?: (itemCount: number) => void;
|
||||||
setListData?: (items: unknown[]) => 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 { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useParams } from 'react-router';
|
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 { 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 { 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 { useCurrentServer, useListSettings } from '/@/renderer/store';
|
import { useCurrentServer, useListSettings } from '/@/renderer/store';
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import { PlaylistSongListQuery, PlaylistSongListResponse } from '/@/shared/types/domain-types';
|
import {
|
||||||
import { ItemListKey, ListDisplayType, TableColumn } from '/@/shared/types/types';
|
LibraryItem,
|
||||||
|
PlaylistSongListQuery,
|
||||||
|
PlaylistSongListResponse,
|
||||||
|
Song,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
import {
|
||||||
|
ItemListKey,
|
||||||
|
ListDisplayType,
|
||||||
|
ListPaginationType,
|
||||||
|
TableColumn,
|
||||||
|
} from '/@/shared/types/types';
|
||||||
|
|
||||||
const PlaylistDetailSongListTable = lazy(() =>
|
const PlaylistDetailSongListTable = lazy(() =>
|
||||||
import('/@/renderer/features/playlists/components/playlist-detail-song-list-table').then(
|
import('/@/renderer/features/playlists/components/playlist-detail-song-list-table').then(
|
||||||
@@ -38,7 +51,6 @@ const PlaylistDetailSongListGrid = lazy(() =>
|
|||||||
export const PlaylistDetailSongListContent = () => {
|
export const PlaylistDetailSongListContent = () => {
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { setItemCount } = useListContext();
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const playlistSongsQuery = useSuspenseQuery(
|
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(() => {
|
useEffect(() => {
|
||||||
const handleRefresh = async (payload: { key: string }) => {
|
const handleRefresh = async (payload: { key: string }) => {
|
||||||
if (payload.key !== ItemListKey.PLAYLIST_SONG) {
|
if (
|
||||||
|
payload.key !== ItemListKey.PLAYLIST_SONG &&
|
||||||
|
payload.key !== ItemListKey.PLAYLIST_ALBUM
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +87,7 @@ export const PlaylistDetailSongListContent = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
eventEmitter.off('ITEM_LIST_REFRESH', handleRefresh);
|
eventEmitter.off('ITEM_LIST_REFRESH', handleRefresh);
|
||||||
};
|
};
|
||||||
}, [playlistId, queryClient, server.id]);
|
}, [playlistId, queryClient, server?.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Spinner container />}>
|
<Suspense fallback={<Spinner container />}>
|
||||||
@@ -92,13 +98,36 @@ export const PlaylistDetailSongListContent = () => {
|
|||||||
|
|
||||||
export type OverridePlaylistSongListQuery = Omit<Partial<PlaylistSongListQuery>, 'id'>;
|
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 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) {
|
switch (display) {
|
||||||
case ListDisplayType.GRID: {
|
case ListDisplayType.GRID: {
|
||||||
return <PlaylistDetailSongListGrid data={data} serverId={server.id} />;
|
return (
|
||||||
|
<PlaylistDetailSongListGrid
|
||||||
|
data={data}
|
||||||
|
items={items}
|
||||||
|
serverId={server.id}
|
||||||
|
{...paginationProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
case ListDisplayType.TABLE: {
|
case ListDisplayType.TABLE: {
|
||||||
return (
|
return (
|
||||||
@@ -111,8 +140,10 @@ export const PlaylistDetailSongListView = ({ data }: { data: PlaylistSongListRes
|
|||||||
enableHorizontalBorders={table.enableHorizontalBorders}
|
enableHorizontalBorders={table.enableHorizontalBorders}
|
||||||
enableRowHoverHighlight={table.enableRowHoverHighlight}
|
enableRowHoverHighlight={table.enableRowHoverHighlight}
|
||||||
enableVerticalBorders={table.enableVerticalBorders}
|
enableVerticalBorders={table.enableVerticalBorders}
|
||||||
|
items={items}
|
||||||
serverId={server.id}
|
serverId={server.id}
|
||||||
size={table.size}
|
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();
|
const { isSmartPlaylist, mode } = useListContext();
|
||||||
|
|
||||||
if (isSmartPlaylist) {
|
if (isSmartPlaylist) {
|
||||||
return <PlaylistDetailSongListView data={data} />;
|
return <PlaylistDetailTrackViewContent data={data} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (mode) {
|
if (mode === 'edit') {
|
||||||
case 'edit':
|
return <PlaylistDetailSongListEdit data={data} />;
|
||||||
return <PlaylistDetailSongListEdit data={data} />;
|
|
||||||
case 'view':
|
|
||||||
return <PlaylistDetailSongListView data={data} />;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
|
||||||
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
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 { 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 { ItemListGridComponentProps } from '/@/renderer/components/item-list/types';
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
|
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
|
||||||
@@ -15,40 +16,52 @@ import {
|
|||||||
LibraryItem,
|
LibraryItem,
|
||||||
PlaylistSongListQuery,
|
PlaylistSongListQuery,
|
||||||
PlaylistSongListResponse,
|
PlaylistSongListResponse,
|
||||||
|
Song,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface PlaylistDetailSongListGridProps
|
interface PlaylistDetailSongListGridProps
|
||||||
extends Omit<ItemListGridComponentProps<PlaylistSongListQuery>, 'query'> {
|
extends Omit<ItemListGridComponentProps<PlaylistSongListQuery>, 'query'> {
|
||||||
|
currentPage?: number;
|
||||||
data: PlaylistSongListResponse;
|
data: PlaylistSongListResponse;
|
||||||
|
items?: Song[];
|
||||||
|
itemsPerPage?: number;
|
||||||
|
onPageChange?: (page: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaylistDetailSongListGrid = forwardRef<any, PlaylistDetailSongListGridProps>(
|
export const PlaylistDetailSongListGrid = forwardRef<any, PlaylistDetailSongListGridProps>(
|
||||||
({ data, saveScrollOffset = true }) => {
|
({
|
||||||
|
currentPage,
|
||||||
|
data,
|
||||||
|
items: itemsProp,
|
||||||
|
itemsPerPage,
|
||||||
|
onPageChange,
|
||||||
|
saveScrollOffset = true,
|
||||||
|
}) => {
|
||||||
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
|
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
|
||||||
enabled: saveScrollOffset,
|
enabled: saveScrollOffset,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { searchTerm } = useSearchTermFilter();
|
const { searchTerm } = useSearchTermFilter();
|
||||||
const { query } = usePlaylistSongListFilters();
|
const { query } = usePlaylistSongListFilters();
|
||||||
const { setListData } = useListContext();
|
|
||||||
|
|
||||||
const songData = useMemo(() => {
|
|
||||||
let items = data?.items || [];
|
|
||||||
|
|
||||||
|
const songDataFromData = useMemo(() => {
|
||||||
|
let list = data?.items || [];
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
items = searchLibraryItems(items, searchTerm, LibraryItem.SONG);
|
list = searchLibraryItems(list, searchTerm, LibraryItem.SONG);
|
||||||
return items;
|
return list;
|
||||||
}
|
}
|
||||||
|
return sortSongList(list, query.sortBy, query.sortOrder);
|
||||||
return sortSongList(items, query.sortBy, query.sortOrder);
|
|
||||||
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
|
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
|
||||||
|
|
||||||
|
const { setListData } = useListContext();
|
||||||
|
const songData = itemsProp ?? songDataFromData;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (setListData) {
|
if (itemsProp == null && setListData) {
|
||||||
setListData(songData);
|
setListData(songDataFromData);
|
||||||
}
|
}
|
||||||
}, [songData, setListData]);
|
}, [itemsProp, songDataFromData, setListData]);
|
||||||
|
|
||||||
const gridProps = useListSettings(ItemListKey.PLAYLIST_SONG).grid;
|
const gridProps = useListSettings(ItemListKey.PLAYLIST_SONG).grid;
|
||||||
|
|
||||||
@@ -59,9 +72,22 @@ export const PlaylistDetailSongListGrid = forwardRef<any, PlaylistDetailSongList
|
|||||||
);
|
);
|
||||||
const { enableGridMultiSelect } = useGeneralSettings();
|
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
|
<ItemGridList
|
||||||
data={songData}
|
data={dataToRender}
|
||||||
enableMultiSelect={enableGridMultiSelect}
|
enableMultiSelect={enableGridMultiSelect}
|
||||||
gap={gridProps.itemGap}
|
gap={gridProps.itemGap}
|
||||||
initialTop={{
|
initialTop={{
|
||||||
@@ -75,5 +101,21 @@ export const PlaylistDetailSongListGrid = forwardRef<any, PlaylistDetailSongList
|
|||||||
size={gridProps.size}
|
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 { useParams } from 'react-router';
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
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 { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||||
import { ListDisplayTypeToggleButton } from '/@/renderer/features/shared/components/list-display-type-toggle-button';
|
import { ListDisplayTypeToggleButton } from '/@/renderer/features/shared/components/list-display-type-toggle-button';
|
||||||
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-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 { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||||
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
||||||
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
|
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
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 { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
@@ -37,8 +45,10 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
isSmartPlaylist,
|
isSmartPlaylist,
|
||||||
}: PlaylistDetailSongListHeaderFiltersProps) => {
|
}: PlaylistDetailSongListHeaderFiltersProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { mode, setMode } = useListContext();
|
const { listKey: listKeyFromContext, mode, setMode } = useListContext();
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
|
const playlistTarget = usePlaylistTarget();
|
||||||
|
const { setPlaylistBehavior } = useSettingsStoreActions();
|
||||||
const serverId = useCurrentServerId();
|
const serverId = useCurrentServerId();
|
||||||
|
|
||||||
const detailQuery = useQuery(playlistsQueries.detail({ query: { id: playlistId }, serverId }));
|
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 { ref: containerRef, ...breakpoints } = useContainerQuery();
|
||||||
|
|
||||||
const isViewEditMode = !isSmartPlaylist && breakpoints.isSm;
|
const isViewEditMode = !isSmartPlaylist && (breakpoints.isSm || isAlbumMode);
|
||||||
const isEditMode = mode === 'edit';
|
const isEditMode = mode === 'edit';
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useLocalStorage<boolean>({
|
const [collapsed, setCollapsed] = useLocalStorage<boolean>({
|
||||||
@@ -68,6 +94,14 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
return (
|
return (
|
||||||
<Flex justify="space-between" ref={containerRef}>
|
<Flex justify="space-between" ref={containerRef}>
|
||||||
<Group gap="sm" w="100%">
|
<Group gap="sm" w="100%">
|
||||||
|
<Button
|
||||||
|
leftSection={<Icon icon="arrowLeftRight" />}
|
||||||
|
onClick={handleToggleDisplayMode}
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
{toggleChoice}
|
||||||
|
</Button>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
<ListSortByDropdown
|
<ListSortByDropdown
|
||||||
defaultSortByValue={SongListSort.ID}
|
defaultSortByValue={SongListSort.ID}
|
||||||
disabled={isEditMode}
|
disabled={isEditMode}
|
||||||
@@ -80,8 +114,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
disabled={isEditMode}
|
disabled={isEditMode}
|
||||||
listKey={ItemListKey.PLAYLIST_SONG}
|
listKey={ItemListKey.PLAYLIST_SONG}
|
||||||
/>
|
/>
|
||||||
{!collapsed && <ListSearchInput />}
|
<ListRefreshButton disabled={isEditMode} listKey={listKey} />
|
||||||
<ListRefreshButton disabled={isEditMode} listKey={ItemListKey.PLAYLIST_SONG} />
|
|
||||||
<MoreButton onClick={handleMore} />
|
<MoreButton onClick={handleMore} />
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
@@ -109,11 +142,25 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
variant="subtle"
|
variant="subtle"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ListDisplayTypeToggleButton listKey={ItemListKey.PLAYLIST_SONG} />
|
<ListDisplayTypeToggleButton enableDetail={isAlbumMode} listKey={listKey} />
|
||||||
<ListConfigMenu
|
{isAlbumMode ? (
|
||||||
listKey={ItemListKey.PLAYLIST_SONG}
|
<ListConfigMenu
|
||||||
tableColumnsData={PLAYLIST_SONG_TABLE_COLUMNS}
|
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>
|
</Group>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export const PlaylistDetailSongListHeader = ({
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
) : (
|
) : (
|
||||||
<LibraryHeader
|
<LibraryHeader
|
||||||
|
compact
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
item={{
|
item={{
|
||||||
imageId: detailQuery?.data?.imageId,
|
imageId: detailQuery?.data?.imageId,
|
||||||
@@ -101,6 +102,7 @@ export const PlaylistDetailSongListHeader = ({
|
|||||||
type: LibraryItem.PLAYLIST,
|
type: LibraryItem.PLAYLIST,
|
||||||
}}
|
}}
|
||||||
title={detailQuery?.data?.name || ''}
|
title={detailQuery?.data?.name || ''}
|
||||||
|
topRight={<ListSearchInput />}
|
||||||
>
|
>
|
||||||
<LibraryHeaderMenu
|
<LibraryHeaderMenu
|
||||||
onPlay={(type) => handlePlay(type)}
|
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 { 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 { 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 { 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 { 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 { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||||
import { ItemControls, ItemListTableComponentProps } from '/@/renderer/components/item-list/types';
|
import { ItemControls, ItemListTableComponentProps } from '/@/renderer/components/item-list/types';
|
||||||
@@ -24,7 +25,11 @@ import { ItemListKey, Play } from '/@/shared/types/types';
|
|||||||
|
|
||||||
interface PlaylistDetailSongListTableProps
|
interface PlaylistDetailSongListTableProps
|
||||||
extends Omit<ItemListTableComponentProps<PlaylistSongListQuery>, 'query'> {
|
extends Omit<ItemListTableComponentProps<PlaylistSongListQuery>, 'query'> {
|
||||||
|
currentPage?: number;
|
||||||
data: PlaylistSongListResponse;
|
data: PlaylistSongListResponse;
|
||||||
|
items?: Song[];
|
||||||
|
itemsPerPage?: number;
|
||||||
|
onPageChange?: (page: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongListTableProps>(
|
export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongListTableProps>(
|
||||||
@@ -32,6 +37,7 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
|||||||
{
|
{
|
||||||
autoFitColumns = false,
|
autoFitColumns = false,
|
||||||
columns,
|
columns,
|
||||||
|
currentPage,
|
||||||
data,
|
data,
|
||||||
enableAlternateRowColors = false,
|
enableAlternateRowColors = false,
|
||||||
enableHeader = true,
|
enableHeader = true,
|
||||||
@@ -39,6 +45,9 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
|||||||
enableRowHoverHighlight = true,
|
enableRowHoverHighlight = true,
|
||||||
enableSelection = true,
|
enableSelection = true,
|
||||||
enableVerticalBorders = false,
|
enableVerticalBorders = false,
|
||||||
|
items: itemsProp,
|
||||||
|
itemsPerPage,
|
||||||
|
onPageChange,
|
||||||
saveScrollOffset = true,
|
saveScrollOffset = true,
|
||||||
size = 'default',
|
size = 'default',
|
||||||
},
|
},
|
||||||
@@ -58,24 +67,24 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
|||||||
|
|
||||||
const { searchTerm } = useSearchTermFilter();
|
const { searchTerm } = useSearchTermFilter();
|
||||||
const { query } = usePlaylistSongListFilters();
|
const { query } = usePlaylistSongListFilters();
|
||||||
const { setListData } = useListContext();
|
|
||||||
|
|
||||||
const songData = useMemo(() => {
|
|
||||||
let items = data?.items || [];
|
|
||||||
|
|
||||||
|
const songDataFromData = useMemo(() => {
|
||||||
|
let list = data?.items || [];
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
items = searchLibraryItems(items, searchTerm, LibraryItem.SONG);
|
list = searchLibraryItems(list, searchTerm, LibraryItem.SONG);
|
||||||
return items;
|
return list;
|
||||||
}
|
}
|
||||||
|
return sortSongList(list, query.sortBy, query.sortOrder);
|
||||||
return sortSongList(items, query.sortBy, query.sortOrder);
|
|
||||||
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
|
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
|
||||||
|
|
||||||
|
const { setListData } = useListContext();
|
||||||
|
const songData = itemsProp ?? songDataFromData;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (setListData) {
|
if (itemsProp == null && setListData) {
|
||||||
setListData(songData);
|
setListData(songDataFromData);
|
||||||
}
|
}
|
||||||
}, [songData, setListData]);
|
}, [itemsProp, songDataFromData, setListData]);
|
||||||
|
|
||||||
const player = usePlayer();
|
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
|
<ItemTableList
|
||||||
activeRowId={currentSong?.id}
|
activeRowId={currentSong?.id}
|
||||||
autoFitColumns={autoFitColumns}
|
autoFitColumns={autoFitColumns}
|
||||||
CellComponent={ItemTableListColumn}
|
CellComponent={ItemTableListColumn}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={songData}
|
data={dataToRender}
|
||||||
enableAlternateRowColors={enableAlternateRowColors}
|
enableAlternateRowColors={enableAlternateRowColors}
|
||||||
enableExpansion={false}
|
enableExpansion={false}
|
||||||
enableHeader={enableHeader}
|
enableHeader={enableHeader}
|
||||||
@@ -136,6 +158,22 @@ export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongLis
|
|||||||
size={size}
|
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 { JsonPreview } from '/@/renderer/features/shared/components/json-preview';
|
||||||
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
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 { Button } from '/@/shared/components/button/button';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
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 { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
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';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
interface PlaylistQueryEditorProps {
|
interface PlaylistQueryEditorProps {
|
||||||
@@ -154,14 +154,17 @@ const PlaylistQueryEditor = ({
|
|||||||
}, [detailQuery?.data?.rules?.order, detailQuery?.data?.rules?.sort]);
|
}, [detailQuery?.data?.rules?.order, detailQuery?.data?.rules?.sort]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="query-editor-container">
|
<div
|
||||||
<Stack gap={0} h="100%" mah="30dvh" p="md" w="100%">
|
className="query-editor-container"
|
||||||
<Group justify="space-between" pb="md" wrap="nowrap">
|
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">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<Button
|
<Button
|
||||||
leftSection={
|
leftSection={
|
||||||
<Icon
|
<Icon
|
||||||
icon={isQueryBuilderExpanded ? 'arrowUpS' : 'arrowDownS'}
|
icon={isQueryBuilderExpanded ? 'arrowDownS' : 'arrowUpS'}
|
||||||
size="lg"
|
size="lg"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -396,6 +399,12 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
setIsQueryBuilderExpanded(true);
|
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 [itemCount, setItemCount] = useState<number | undefined>(undefined);
|
||||||
const [listData, setListData] = useState<unknown[]>([]);
|
const [listData, setListData] = useState<unknown[]>([]);
|
||||||
const [mode, setMode] = useState<'edit' | 'view'>('view');
|
const [mode, setMode] = useState<'edit' | 'view'>('view');
|
||||||
@@ -403,17 +412,19 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
const providerValue = useMemo(() => {
|
const providerValue = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
customFilters: undefined,
|
customFilters: undefined,
|
||||||
|
displayMode,
|
||||||
id: playlistId,
|
id: playlistId,
|
||||||
isSmartPlaylist,
|
isSmartPlaylist,
|
||||||
itemCount,
|
itemCount,
|
||||||
listData,
|
listData,
|
||||||
|
listKey,
|
||||||
mode,
|
mode,
|
||||||
pageKey: ItemListKey.PLAYLIST_SONG,
|
pageKey: listKey,
|
||||||
setItemCount,
|
setItemCount,
|
||||||
setListData,
|
setListData,
|
||||||
setMode,
|
setMode,
|
||||||
};
|
};
|
||||||
}, [playlistId, isSmartPlaylist, itemCount, listData, mode]);
|
}, [playlistId, isSmartPlaylist, displayMode, listKey, itemCount, listData, mode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
||||||
@@ -429,6 +440,10 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
onDelete={() => openDeletePlaylistModal()}
|
onDelete={() => openDeletePlaylistModal()}
|
||||||
onToggleQueryBuilder={handleToggleShowQueryBuilder}
|
onToggleQueryBuilder={handleToggleShowQueryBuilder}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Suspense fallback={<Spinner container />}>
|
||||||
|
<PlaylistDetailSongListContent />
|
||||||
|
</Suspense>
|
||||||
{(isSmartPlaylist || showQueryBuilder) && (
|
{(isSmartPlaylist || showQueryBuilder) && (
|
||||||
<PlaylistQueryEditor
|
<PlaylistQueryEditor
|
||||||
createPlaylistMutation={createPlaylistMutation}
|
createPlaylistMutation={createPlaylistMutation}
|
||||||
@@ -441,9 +456,6 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
queryBuilderRef={queryBuilderRef}
|
queryBuilderRef={queryBuilderRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Suspense fallback={<Spinner container />}>
|
|
||||||
<PlaylistDetailSongListContent />
|
|
||||||
</Suspense>
|
|
||||||
</ListContext.Provider>
|
</ListContext.Provider>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
.top-right {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--theme-spacing-lg);
|
||||||
|
right: var(--theme-spacing-md);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
.library-header {
|
.library-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -56,6 +63,52 @@
|
|||||||
height: 250px;
|
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 {
|
.image-section {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { Play } from '/@/shared/types/types';
|
|||||||
|
|
||||||
interface LibraryHeaderProps {
|
interface LibraryHeaderProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
compact?: boolean;
|
||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
imagePlaceholderUrl?: null | string;
|
imagePlaceholderUrl?: null | string;
|
||||||
imageUrl?: null | string;
|
imageUrl?: null | string;
|
||||||
@@ -45,11 +46,20 @@ interface LibraryHeaderProps {
|
|||||||
};
|
};
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
|
topRight?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LibraryHeader = forwardRef(
|
export const LibraryHeader = forwardRef(
|
||||||
(
|
(
|
||||||
{ children, containerClassName, imageUrl, item, title }: LibraryHeaderProps,
|
{
|
||||||
|
children,
|
||||||
|
compact,
|
||||||
|
containerClassName,
|
||||||
|
imageUrl,
|
||||||
|
item,
|
||||||
|
title,
|
||||||
|
topRight,
|
||||||
|
}: LibraryHeaderProps,
|
||||||
ref: Ref<HTMLDivElement>,
|
ref: Ref<HTMLDivElement>,
|
||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -125,7 +135,15 @@ export const LibraryHeader = forwardRef(
|
|||||||
}, [item.explicitStatus, item.imageId, item.type]);
|
}, [item.explicitStatus, item.imageId, item.type]);
|
||||||
|
|
||||||
return (
|
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
|
<div
|
||||||
className={styles.imageSection}
|
className={styles.imageSection}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -224,6 +224,11 @@ export const CLIENT_SIDE_ALBUM_FILTERS = [
|
|||||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||||
value: AlbumListSort.ALBUM_ARTIST,
|
value: AlbumListSort.ALBUM_ARTIST,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.ID,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
defaultOrder: SortOrder.DESC,
|
defaultOrder: SortOrder.DESC,
|
||||||
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
|
||||||
@@ -295,6 +300,11 @@ const ALBUM_LIST_FILTERS: Partial<
|
|||||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||||
value: AlbumListSort.ALBUM_ARTIST,
|
value: AlbumListSort.ALBUM_ARTIST,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.ID,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
defaultOrder: SortOrder.DESC,
|
defaultOrder: SortOrder.DESC,
|
||||||
name: i18n.t('filter.communityRating', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.communityRating', { postProcess: 'titleCase' }),
|
||||||
@@ -337,6 +347,11 @@ const ALBUM_LIST_FILTERS: Partial<
|
|||||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||||
value: AlbumListSort.ALBUM_ARTIST,
|
value: AlbumListSort.ALBUM_ARTIST,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.ID,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
defaultOrder: SortOrder.ASC,
|
defaultOrder: SortOrder.ASC,
|
||||||
name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
|
||||||
@@ -399,6 +414,11 @@ const ALBUM_LIST_FILTERS: Partial<
|
|||||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||||
value: AlbumListSort.ALBUM_ARTIST,
|
value: AlbumListSort.ALBUM_ARTIST,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.ID,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
defaultOrder: SortOrder.DESC,
|
defaultOrder: SortOrder.DESC,
|
||||||
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
|
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 GenreTargetSchema = z.enum(['album', 'track']);
|
||||||
|
|
||||||
|
const PlaylistTargetSchema = z.enum(['album', 'track']);
|
||||||
|
|
||||||
const SideQueueTypeSchema = z.enum(['sideDrawerQueue', 'sideQueue']);
|
const SideQueueTypeSchema = z.enum(['sideDrawerQueue', 'sideQueue']);
|
||||||
|
|
||||||
const SidebarPanelTypeSchema = z.enum(['queue', 'lyrics', 'visualizer']);
|
const SidebarPanelTypeSchema = z.enum(['queue', 'lyrics', 'visualizer']);
|
||||||
@@ -458,6 +460,7 @@ export const GeneralSettingsSchema = z.object({
|
|||||||
playButtonBehavior: z.nativeEnum(Play),
|
playButtonBehavior: z.nativeEnum(Play),
|
||||||
playerbarOpenDrawer: z.boolean(),
|
playerbarOpenDrawer: z.boolean(),
|
||||||
playerbarSlider: PlayerbarSliderSchema,
|
playerbarSlider: PlayerbarSliderSchema,
|
||||||
|
playlistTarget: PlaylistTargetSchema,
|
||||||
resume: z.boolean(),
|
resume: z.boolean(),
|
||||||
showLyricsInSidebar: z.boolean(),
|
showLyricsInSidebar: z.boolean(),
|
||||||
showRatings: z.boolean(),
|
showRatings: z.boolean(),
|
||||||
@@ -775,6 +778,11 @@ export enum PlayerbarSliderType {
|
|||||||
WAVEFORM = 'waveform',
|
WAVEFORM = 'waveform',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PlaylistTarget {
|
||||||
|
ALBUM = 'album',
|
||||||
|
TRACK = 'track',
|
||||||
|
}
|
||||||
|
|
||||||
export enum SidebarItem {
|
export enum SidebarItem {
|
||||||
ALBUMS = 'Albums',
|
ALBUMS = 'Albums',
|
||||||
ARTISTS = 'Artists',
|
ARTISTS = 'Artists',
|
||||||
@@ -829,6 +837,7 @@ export interface SettingsSlice extends z.infer<typeof SettingsStateSchema> {
|
|||||||
setHomeItems: (item: SortableItem<HomeItem>[]) => void;
|
setHomeItems: (item: SortableItem<HomeItem>[]) => void;
|
||||||
setList: (type: ItemListKey, data: DeepPartial<ItemListSettings>) => void;
|
setList: (type: ItemListKey, data: DeepPartial<ItemListSettings>) => void;
|
||||||
setPlaybackFilters: (filters: PlayerFilter[]) => void;
|
setPlaybackFilters: (filters: PlayerFilter[]) => void;
|
||||||
|
setPlaylistBehavior: (target: PlaylistTarget) => void;
|
||||||
setSettings: (data: DeepPartial<SettingsState>) => void;
|
setSettings: (data: DeepPartial<SettingsState>) => void;
|
||||||
setSidebarItems: (items: SidebarItemType[]) => void;
|
setSidebarItems: (items: SidebarItemType[]) => void;
|
||||||
setTable: (type: ItemListKey, data: DataTableProps) => void;
|
setTable: (type: ItemListKey, data: DataTableProps) => void;
|
||||||
@@ -1039,6 +1048,7 @@ const initialState: SettingsState = {
|
|||||||
barWidth: 2,
|
barWidth: 2,
|
||||||
type: PlayerbarSliderType.SLIDER,
|
type: PlayerbarSliderType.SLIDER,
|
||||||
},
|
},
|
||||||
|
playlistTarget: PlaylistTarget.TRACK,
|
||||||
resume: true,
|
resume: true,
|
||||||
showLyricsInSidebar: true,
|
showLyricsInSidebar: true,
|
||||||
showRatings: true,
|
showRatings: true,
|
||||||
@@ -1175,6 +1185,83 @@ const initialState: SettingsState = {
|
|||||||
size: 'default',
|
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]: {
|
[LibraryItem.ALBUM]: {
|
||||||
detail: {
|
detail: {
|
||||||
columns: pickTableColumns({
|
columns: pickTableColumns({
|
||||||
@@ -1808,6 +1895,11 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
state.playback.filters = filters;
|
state.playback.filters = filters;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setPlaylistBehavior: (target: PlaylistTarget) => {
|
||||||
|
set((state) => {
|
||||||
|
state.general.playlistTarget = target;
|
||||||
|
});
|
||||||
|
},
|
||||||
setSettings: (data) => {
|
setSettings: (data) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
deepMergeIntoState(state, data);
|
deepMergeIntoState(state, data);
|
||||||
@@ -2218,6 +2310,9 @@ export const usePlayerbarSlider = () =>
|
|||||||
|
|
||||||
export const useGenreTarget = () => useSettingsStore((store) => store.general.genreTarget, shallow);
|
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 useLanguage = () => useSettingsStore((state) => state.general.language, shallow);
|
||||||
|
|
||||||
export const useAccent = () => useSettingsStore((state) => state.general.accent, 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 = orderBy(
|
||||||
results,
|
results,
|
||||||
[(v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
[(v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
[order, 'asc', 'asc'],
|
[order, order, order],
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
|||||||
results = orderBy(
|
results = orderBy(
|
||||||
results,
|
results,
|
||||||
[(v) => v.albumArtists[0]?.name.toLowerCase(), 'discNumber', 'trackNumber'],
|
[(v) => v.albumArtists[0]?.name.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
[order, order, 'asc', 'asc'],
|
[order, order, order, order],
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -167,32 +167,54 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
|||||||
results = orderBy(
|
results = orderBy(
|
||||||
results,
|
results,
|
||||||
[(v) => v.artistName?.toLowerCase(), 'discNumber', 'trackNumber'],
|
[(v) => v.artistName?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
[order, order, 'asc', 'asc'],
|
[order, order, order, order],
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.BPM:
|
case SongListSort.BPM:
|
||||||
results = orderBy(results, ['bpm'], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['bpm', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
|
[order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.CHANNELS:
|
case SongListSort.CHANNELS:
|
||||||
results = orderBy(results, ['channels'], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['channels', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
|
[order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.COMMENT:
|
case SongListSort.COMMENT:
|
||||||
results = orderBy(
|
results = orderBy(
|
||||||
results,
|
results,
|
||||||
['comment', 'discNumber', 'trackNumber'],
|
['comment', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
[order, order, 'asc', 'asc'],
|
[order, order, order, order],
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.DURATION:
|
case SongListSort.DURATION:
|
||||||
results = orderBy(results, ['duration'], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['duration', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
|
[order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.FAVORITED:
|
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;
|
break;
|
||||||
|
|
||||||
case SongListSort.GENRE:
|
case SongListSort.GENRE:
|
||||||
@@ -204,7 +226,7 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
|||||||
'discNumber',
|
'discNumber',
|
||||||
'trackNumber',
|
'trackNumber',
|
||||||
],
|
],
|
||||||
[order, order, 'asc', 'asc'],
|
[order, order, order, order],
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -217,11 +239,19 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.NAME:
|
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;
|
break;
|
||||||
|
|
||||||
case SongListSort.PLAY_COUNT:
|
case SongListSort.PLAY_COUNT:
|
||||||
results = orderBy(results, ['playCount'], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['playCount', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
|
[order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.RANDOM:
|
case SongListSort.RANDOM:
|
||||||
@@ -229,19 +259,51 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.RATING:
|
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;
|
break;
|
||||||
|
|
||||||
case SongListSort.RECENTLY_ADDED:
|
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;
|
break;
|
||||||
|
|
||||||
case SongListSort.RECENTLY_PLAYED:
|
case SongListSort.RECENTLY_PLAYED:
|
||||||
results = orderBy(results, ['lastPlayedAt'], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['lastPlayedAt', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
|
[order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.RELEASE_DATE:
|
case SongListSort.RELEASE_DATE:
|
||||||
results = orderBy(results, ['releaseDate'], [order]);
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['releaseDate', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
|
[order, order, order, order],
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SongListSort.SORT_NAME:
|
case SongListSort.SORT_NAME:
|
||||||
@@ -252,7 +314,7 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor
|
|||||||
results = orderBy(
|
results = orderBy(
|
||||||
results,
|
results,
|
||||||
['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
|
['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
|
||||||
[order, 'asc', 'asc', 'asc'],
|
[order, order, order, order],
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -404,6 +466,9 @@ export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder:
|
|||||||
case AlbumListSort.FAVORITED:
|
case AlbumListSort.FAVORITED:
|
||||||
results = orderBy(results, ['starred'], [order]);
|
results = orderBy(results, ['starred'], [order]);
|
||||||
break;
|
break;
|
||||||
|
case AlbumListSort.ID:
|
||||||
|
results = sortOrder === SortOrder.DESC ? [...results].reverse() : results;
|
||||||
|
break;
|
||||||
case AlbumListSort.NAME:
|
case AlbumListSort.NAME:
|
||||||
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
|
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -466,6 +466,7 @@ export enum AlbumListSort {
|
|||||||
DURATION = 'duration',
|
DURATION = 'duration',
|
||||||
EXPLICIT_STATUS = 'explicitStatus',
|
EXPLICIT_STATUS = 'explicitStatus',
|
||||||
FAVORITED = 'favorited',
|
FAVORITED = 'favorited',
|
||||||
|
ID = 'id',
|
||||||
NAME = 'name',
|
NAME = 'name',
|
||||||
PLAY_COUNT = 'playCount',
|
PLAY_COUNT = 'playCount',
|
||||||
RANDOM = 'random',
|
RANDOM = 'random',
|
||||||
@@ -521,6 +522,7 @@ export const albumListSortMap: AlbumListSortMap = {
|
|||||||
duration: undefined,
|
duration: undefined,
|
||||||
explicitStatus: undefined,
|
explicitStatus: undefined,
|
||||||
favorited: undefined,
|
favorited: undefined,
|
||||||
|
id: undefined,
|
||||||
name: JFAlbumListSort.NAME,
|
name: JFAlbumListSort.NAME,
|
||||||
playCount: JFAlbumListSort.PLAY_COUNT,
|
playCount: JFAlbumListSort.PLAY_COUNT,
|
||||||
random: JFAlbumListSort.RANDOM,
|
random: JFAlbumListSort.RANDOM,
|
||||||
@@ -540,6 +542,7 @@ export const albumListSortMap: AlbumListSortMap = {
|
|||||||
duration: NDAlbumListSort.DURATION,
|
duration: NDAlbumListSort.DURATION,
|
||||||
explicitStatus: NDAlbumListSort.EXPLICIT_STATUS,
|
explicitStatus: NDAlbumListSort.EXPLICIT_STATUS,
|
||||||
favorited: NDAlbumListSort.STARRED,
|
favorited: NDAlbumListSort.STARRED,
|
||||||
|
id: undefined,
|
||||||
name: NDAlbumListSort.NAME,
|
name: NDAlbumListSort.NAME,
|
||||||
playCount: NDAlbumListSort.PLAY_COUNT,
|
playCount: NDAlbumListSort.PLAY_COUNT,
|
||||||
random: NDAlbumListSort.RANDOM,
|
random: NDAlbumListSort.RANDOM,
|
||||||
@@ -560,6 +563,7 @@ export const albumListSortMap: AlbumListSortMap = {
|
|||||||
duration: undefined,
|
duration: undefined,
|
||||||
explicitStatus: undefined,
|
explicitStatus: undefined,
|
||||||
favorited: undefined,
|
favorited: undefined,
|
||||||
|
id: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
playCount: undefined,
|
playCount: undefined,
|
||||||
random: undefined,
|
random: undefined,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export enum ItemListKey {
|
|||||||
GENRE_ALBUM = 'genreAlbum',
|
GENRE_ALBUM = 'genreAlbum',
|
||||||
GENRE_SONG = 'genreSong',
|
GENRE_SONG = 'genreSong',
|
||||||
PLAYLIST = LibraryItem.PLAYLIST,
|
PLAYLIST = LibraryItem.PLAYLIST,
|
||||||
|
PLAYLIST_ALBUM = 'playlistAlbum',
|
||||||
PLAYLIST_SONG = LibraryItem.PLAYLIST_SONG,
|
PLAYLIST_SONG = LibraryItem.PLAYLIST_SONG,
|
||||||
QUEUE_SONG = LibraryItem.QUEUE_SONG,
|
QUEUE_SONG = LibraryItem.QUEUE_SONG,
|
||||||
RADIO = 'radio',
|
RADIO = 'radio',
|
||||||
|
|||||||
Reference in New Issue
Block a user