mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
Support playlists
This commit is contained in:
@@ -535,7 +535,6 @@ const getPlaylistSongList = async (args: PlaylistSongListArgs): Promise<SongList
|
|||||||
query: {
|
query: {
|
||||||
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId',
|
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId',
|
||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
Limit: query.limit,
|
|
||||||
SortBy: query.sortBy ? songListSortMap.jellyfin[query.sortBy] : undefined,
|
SortBy: query.sortBy ? songListSortMap.jellyfin[query.sortBy] : undefined,
|
||||||
SortOrder: query.sortOrder ? sortOrderMap.jellyfin[query.sortOrder] : undefined,
|
SortOrder: query.sortOrder ? sortOrderMap.jellyfin[query.sortOrder] : undefined,
|
||||||
StartIndex: 0,
|
StartIndex: 0,
|
||||||
@@ -549,7 +548,7 @@ const getPlaylistSongList = async (args: PlaylistSongListArgs): Promise<SongList
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
|
items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
|
||||||
startIndex: query.startIndex,
|
startIndex: 0,
|
||||||
totalRecordCount: res.body.TotalRecordCount,
|
totalRecordCount: res.body.TotalRecordCount,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ const createPlaylist = async (args: CreatePlaylistArgs): Promise<CreatePlaylistR
|
|||||||
body: {
|
body: {
|
||||||
comment: body.comment,
|
comment: body.comment,
|
||||||
name: body.name,
|
name: body.name,
|
||||||
public: body._custom?.navidrome?.public,
|
public: body.public,
|
||||||
rules: body._custom?.navidrome?.rules,
|
rules: body._custom?.navidrome?.rules,
|
||||||
sync: body._custom?.navidrome?.sync,
|
sync: body._custom?.navidrome?.sync,
|
||||||
},
|
},
|
||||||
@@ -410,12 +410,11 @@ const getPlaylistSongList = async (
|
|||||||
id: query.id,
|
id: query.id,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
_end: query.startIndex + (query.limit || 0),
|
|
||||||
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : 'ASC',
|
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : 'ASC',
|
||||||
_sort: query.sortBy
|
_sort: query.sortBy
|
||||||
? songListSortMap.navidrome[query.sortBy]
|
? songListSortMap.navidrome[query.sortBy]
|
||||||
: ndType._enum.songList.ID,
|
: ndType._enum.songList.ID,
|
||||||
_start: query.startIndex,
|
_start: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -425,7 +424,7 @@ const getPlaylistSongList = async (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
items: res.body.data.map((item) => ndNormalize.song(item, apiClientProps.server, '')),
|
items: res.body.data.map((item) => ndNormalize.song(item, apiClientProps.server, '')),
|
||||||
startIndex: query?.startIndex || 0,
|
startIndex: 0,
|
||||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import orderBy from 'lodash/orderBy';
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import shuffle from 'lodash/shuffle';
|
||||||
import filter from 'lodash/filter';
|
import filter from 'lodash/filter';
|
||||||
|
import reverse from 'lodash/reverse';
|
||||||
import md5 from 'md5';
|
import md5 from 'md5';
|
||||||
import { fsLog } from '/@/logger';
|
import { fsLog } from '/@/logger';
|
||||||
import { subsonicApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
import { subsonicApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||||
@@ -13,6 +15,7 @@ import {
|
|||||||
GenreListSort,
|
GenreListSort,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
PlaylistListSort,
|
PlaylistListSort,
|
||||||
|
SongListSort,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { randomString } from '/@/renderer/utils';
|
import { randomString } from '/@/renderer/utils';
|
||||||
|
|
||||||
@@ -566,6 +569,25 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
totalRecordCount: res.body['subsonic-response'].musicFolders.musicFolder.length,
|
totalRecordCount: res.body['subsonic-response'].musicFolders.musicFolder.length,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getPlaylistDetail: async (args) => {
|
||||||
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
|
const res = await subsonicApiClient(apiClientProps).getPlaylist({
|
||||||
|
query: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
fsLog.error('Failed to get playlist detail');
|
||||||
|
throw new Error('Failed to get playlist detail');
|
||||||
|
}
|
||||||
|
|
||||||
|
return subsonicNormalize.playlist(
|
||||||
|
res.body['subsonic-response'].playlist,
|
||||||
|
apiClientProps.server,
|
||||||
|
);
|
||||||
|
},
|
||||||
getPlaylistList: async (args) => {
|
getPlaylistList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
||||||
@@ -619,7 +641,7 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
getPlaylistListCount: async (args) => {
|
getPlaylistListCount: async (args) => {
|
||||||
const { apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
const res = await subsonicApiClient(apiClientProps).getPlaylists({});
|
const res = await subsonicApiClient(apiClientProps).getPlaylists({});
|
||||||
|
|
||||||
@@ -628,8 +650,127 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
throw new Error('Failed to get playlist list count');
|
throw new Error('Failed to get playlist list count');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.searchTerm) {
|
||||||
|
const searchResults = filter(
|
||||||
|
res.body['subsonic-response'].playlists.playlist,
|
||||||
|
(playlist) => {
|
||||||
|
return playlist.name.toLowerCase().includes(query.searchTerm!.toLowerCase());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return searchResults.length;
|
||||||
|
}
|
||||||
|
|
||||||
return res.body['subsonic-response'].playlists.playlist.length;
|
return res.body['subsonic-response'].playlists.playlist.length;
|
||||||
},
|
},
|
||||||
|
getPlaylistSongList: async (args) => {
|
||||||
|
const { query, apiClientProps } = args;
|
||||||
|
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
|
||||||
|
|
||||||
|
const res = await subsonicApiClient(apiClientProps).getPlaylist({
|
||||||
|
query: {
|
||||||
|
id: query.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
fsLog.error('Failed to get playlist song list');
|
||||||
|
throw new Error('Failed to get playlist song list');
|
||||||
|
}
|
||||||
|
|
||||||
|
let results = res.body['subsonic-response'].playlist.entry || [];
|
||||||
|
|
||||||
|
if (query.searchTerm) {
|
||||||
|
const searchResults = filter(results, (entry) => {
|
||||||
|
return entry.title.toLowerCase().includes(query.searchTerm!.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
results = searchResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.sortBy) {
|
||||||
|
switch (query.sortBy) {
|
||||||
|
case SongListSort.ALBUM:
|
||||||
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
[(v) => v.album?.toLowerCase(), 'discNumber', 'track'],
|
||||||
|
[sortOrder, 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case SongListSort.ALBUM_ARTIST:
|
||||||
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['albumArtist', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
|
||||||
|
[sortOrder, sortOrder, 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case SongListSort.ARTIST:
|
||||||
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['artist', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
|
||||||
|
[sortOrder, sortOrder, 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case SongListSort.DURATION:
|
||||||
|
results = orderBy(results, ['duration'], [sortOrder]);
|
||||||
|
break;
|
||||||
|
case SongListSort.FAVORITED:
|
||||||
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['starred', (v) => v.title.toLowerCase()],
|
||||||
|
[sortOrder],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case SongListSort.GENRE:
|
||||||
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['genre', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
|
||||||
|
[sortOrder, sortOrder, 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case SongListSort.ID:
|
||||||
|
if (sortOrder === 'desc') {
|
||||||
|
results = reverse(results);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SongListSort.NAME:
|
||||||
|
results = orderBy(results, [(v) => v.title.toLowerCase()], [sortOrder]);
|
||||||
|
break;
|
||||||
|
case SongListSort.PLAY_COUNT:
|
||||||
|
results = orderBy(results, ['playCount'], [sortOrder]);
|
||||||
|
break;
|
||||||
|
case SongListSort.RANDOM:
|
||||||
|
results = shuffle(results);
|
||||||
|
break;
|
||||||
|
case SongListSort.RATING:
|
||||||
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['userRating', (v) => v.title.toLowerCase()],
|
||||||
|
[sortOrder],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case SongListSort.RECENTLY_ADDED:
|
||||||
|
results = orderBy(results, ['created'], [sortOrder]);
|
||||||
|
break;
|
||||||
|
case SongListSort.YEAR:
|
||||||
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['year', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
|
||||||
|
[sortOrder, 'asc', 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: results?.map((song) => subsonicNormalize.song(song, apiClientProps.server, '')),
|
||||||
|
startIndex: 0,
|
||||||
|
totalRecordCount: results?.length || 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
getRandomSongList: async (args) => {
|
getRandomSongList: async (args) => {
|
||||||
const { query, apiClientProps } = args;
|
const { query, apiClientProps } = args;
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ const playlist = z.object({
|
|||||||
coverArt: z.string().optional(),
|
coverArt: z.string().optional(),
|
||||||
created: z.string(),
|
created: z.string(),
|
||||||
duration: z.number(),
|
duration: z.number(),
|
||||||
entry: z.array(song),
|
entry: z.array(song).optional(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
owner: z.string(),
|
owner: z.string(),
|
||||||
|
|||||||
@@ -815,6 +815,7 @@ export type CreatePlaylistBody = {
|
|||||||
};
|
};
|
||||||
comment?: string;
|
comment?: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
public?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CreatePlaylistArgs = { body: CreatePlaylistBody; serverId?: string } & BaseEndpointArgs;
|
export type CreatePlaylistArgs = { body: CreatePlaylistBody; serverId?: string } & BaseEndpointArgs;
|
||||||
@@ -935,10 +936,9 @@ export type PlaylistSongListResponse = BasePaginatedResponse<Song[]> | null | un
|
|||||||
|
|
||||||
export type PlaylistSongListQuery = {
|
export type PlaylistSongListQuery = {
|
||||||
id: string;
|
id: string;
|
||||||
limit?: number;
|
searchTerm?: string;
|
||||||
sortBy?: SongListSort;
|
sortBy: SongListSort;
|
||||||
sortOrder?: SortOrder;
|
sortOrder: SortOrder;
|
||||||
startIndex: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PlaylistSongListArgs = { query: PlaylistSongListQuery } & BaseEndpointArgs;
|
export type PlaylistSongListArgs = { query: PlaylistSongListQuery } & BaseEndpointArgs;
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export const getPlaylistSongsById = async (args: {
|
|||||||
id,
|
id,
|
||||||
sortBy: SongListSort.ID,
|
sortBy: SongListSort.ID,
|
||||||
sortOrder: SortOrder.ASC,
|
sortOrder: SortOrder.ASC,
|
||||||
startIndex: 0,
|
|
||||||
...query,
|
...query,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,7 +138,9 @@ export const getGenreSongsById = async (args: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
data.items.push(...res!.items);
|
data.items.push(...res!.items);
|
||||||
data.totalRecordCount += res!.totalRecordCount;
|
if (data.totalRecordCount) {
|
||||||
|
data.totalRecordCount += res!.totalRecordCount || 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -136,7 +136,6 @@ export const AddToPlaylistContextModal = ({
|
|||||||
if (values.skipDuplicates) {
|
if (values.skipDuplicates) {
|
||||||
const query = {
|
const query = {
|
||||||
id: playlistId,
|
id: playlistId,
|
||||||
startIndex: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId, query);
|
const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId, query);
|
||||||
@@ -151,7 +150,11 @@ export const AddToPlaylistContextModal = ({
|
|||||||
server,
|
server,
|
||||||
signal,
|
signal,
|
||||||
},
|
},
|
||||||
query: { id: playlistId, startIndex: 0 },
|
query: {
|
||||||
|
id: playlistId,
|
||||||
|
sortBy: SongListSort.ID,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
|||||||
},
|
},
|
||||||
comment: '',
|
comment: '',
|
||||||
name: '',
|
name: '',
|
||||||
|
public: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const [isSmartPlaylist, setIsSmartPlaylist] = useState(false);
|
const [isSmartPlaylist, setIsSmartPlaylist] = useState(false);
|
||||||
@@ -86,7 +87,8 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPublicDisplayed = server?.type === ServerType.NAVIDROME;
|
const isPublicDisplayed =
|
||||||
|
server?.type === ServerType.NAVIDROME || server?.type === ServerType.SUBSONIC;
|
||||||
const isSubmitDisabled = !form.values.name || mutation.isLoading;
|
const isSubmitDisabled = !form.values.name || mutation.isLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -115,7 +117,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
|||||||
context: 'public',
|
context: 'public',
|
||||||
postProcess: 'titleCase',
|
postProcess: 'titleCase',
|
||||||
})}
|
})}
|
||||||
{...form.getInputProps('_custom.navidrome.public', {
|
{...form.getInputProps('public', {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,25 +2,15 @@ import type {
|
|||||||
BodyScrollEvent,
|
BodyScrollEvent,
|
||||||
ColDef,
|
ColDef,
|
||||||
GridReadyEvent,
|
GridReadyEvent,
|
||||||
IDatasource,
|
|
||||||
PaginationChangedEvent,
|
PaginationChangedEvent,
|
||||||
RowDoubleClickedEvent,
|
RowDoubleClickedEvent,
|
||||||
} from '@ag-grid-community/core';
|
} from '@ag-grid-community/core';
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { MutableRefObject, useCallback, useMemo } from 'react';
|
import { MutableRefObject, useCallback, useMemo } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { api } from '/@/renderer/api';
|
import { LibraryItem, QueueSong, Song } from '/@/renderer/api/types';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
|
||||||
import {
|
|
||||||
LibraryItem,
|
|
||||||
PlaylistSongListQuery,
|
|
||||||
QueueSong,
|
|
||||||
SongListSort,
|
|
||||||
SortOrder,
|
|
||||||
} from '/@/renderer/api/types';
|
|
||||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
||||||
import { TablePagination, VirtualTable, getColumnDefs } from '/@/renderer/components/virtual-table';
|
import { TablePagination, VirtualTable, getColumnDefs } from '/@/renderer/components/virtual-table';
|
||||||
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
|
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
|
||||||
@@ -31,7 +21,7 @@ import {
|
|||||||
} from '/@/renderer/features/context-menu/context-menu-items';
|
} from '/@/renderer/features/context-menu/context-menu-items';
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
||||||
import { usePlaylistSongList } from '/@/renderer/features/playlists/queries/playlist-song-list-query';
|
import { useAppFocus } from '/@/renderer/hooks';
|
||||||
import {
|
import {
|
||||||
useCurrentServer,
|
useCurrentServer,
|
||||||
useCurrentSong,
|
useCurrentSong,
|
||||||
@@ -43,26 +33,19 @@ import {
|
|||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
import { ListDisplayType } from '/@/renderer/types';
|
import { ListDisplayType } from '/@/renderer/types';
|
||||||
import { useAppFocus } from '/@/renderer/hooks';
|
|
||||||
|
|
||||||
interface PlaylistDetailContentProps {
|
interface PlaylistDetailContentProps {
|
||||||
|
songs: Song[];
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailContentProps) => {
|
export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetailContentProps) => {
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const status = useCurrentStatus();
|
const status = useCurrentStatus();
|
||||||
const isFocused = useAppFocus();
|
const isFocused = useAppFocus();
|
||||||
const currentSong = useCurrentSong();
|
const currentSong = useCurrentSong();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const page = usePlaylistDetailStore();
|
const page = usePlaylistDetailStore();
|
||||||
const filters: Partial<PlaylistSongListQuery> = useMemo(() => {
|
|
||||||
return {
|
|
||||||
sortBy: page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID,
|
|
||||||
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC,
|
|
||||||
};
|
|
||||||
}, [page?.table.id, playlistId]);
|
|
||||||
|
|
||||||
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
|
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
|
||||||
|
|
||||||
@@ -82,15 +65,6 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
|
|||||||
|
|
||||||
const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED;
|
const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED;
|
||||||
|
|
||||||
const checkPlaylistList = usePlaylistSongList({
|
|
||||||
query: {
|
|
||||||
id: playlistId,
|
|
||||||
limit: 1,
|
|
||||||
startIndex: 0,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columnDefs: ColDef[] = useMemo(
|
const columnDefs: ColDef[] = useMemo(
|
||||||
() => getColumnDefs(page.table.columns, false, 'generic'),
|
() => getColumnDefs(page.table.columns, false, 'generic'),
|
||||||
[page.table.columns],
|
[page.table.columns],
|
||||||
@@ -98,44 +72,9 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
|
|||||||
|
|
||||||
const onGridReady = useCallback(
|
const onGridReady = useCallback(
|
||||||
(params: GridReadyEvent) => {
|
(params: GridReadyEvent) => {
|
||||||
const dataSource: IDatasource = {
|
|
||||||
getRows: async (params) => {
|
|
||||||
const limit = params.endRow - params.startRow;
|
|
||||||
const startIndex = params.startRow;
|
|
||||||
|
|
||||||
const query: PlaylistSongListQuery = {
|
|
||||||
id: playlistId,
|
|
||||||
limit,
|
|
||||||
startIndex,
|
|
||||||
...filters,
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryKey = queryKeys.playlists.songList(
|
|
||||||
server?.id || '',
|
|
||||||
playlistId,
|
|
||||||
query,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!server) return;
|
|
||||||
|
|
||||||
const songsRes = await queryClient.fetchQuery(queryKey, async ({ signal }) =>
|
|
||||||
api.controller.getPlaylistSongList({
|
|
||||||
apiClientProps: {
|
|
||||||
server,
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
query,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
params.successCallback(songsRes?.items || [], songsRes?.totalRecordCount || 0);
|
|
||||||
},
|
|
||||||
rowCount: undefined,
|
|
||||||
};
|
|
||||||
params.api.setDatasource(dataSource);
|
|
||||||
params.api?.ensureIndexVisible(pagination.scrollOffset, 'top');
|
params.api?.ensureIndexVisible(pagination.scrollOffset, 'top');
|
||||||
},
|
},
|
||||||
[filters, pagination.scrollOffset, playlistId, queryClient, server],
|
[pagination.scrollOffset],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleGridSizeChange = () => {
|
const handleGridSizeChange = () => {
|
||||||
@@ -249,13 +188,13 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
|
|||||||
status,
|
status,
|
||||||
}}
|
}}
|
||||||
getRowId={(data) => data.data.uniqueId}
|
getRowId={(data) => data.data.uniqueId}
|
||||||
infiniteInitialRowCount={checkPlaylistList.data?.totalRecordCount || 100}
|
|
||||||
pagination={isPaginationEnabled}
|
pagination={isPaginationEnabled}
|
||||||
paginationAutoPageSize={isPaginationEnabled}
|
paginationAutoPageSize={isPaginationEnabled}
|
||||||
paginationPageSize={pagination.itemsPerPage || 100}
|
paginationPageSize={pagination.itemsPerPage || 100}
|
||||||
rowClassRules={rowClassRules}
|
rowClassRules={rowClassRules}
|
||||||
|
rowData={songs}
|
||||||
rowHeight={page.table.rowHeight || 40}
|
rowHeight={page.table.rowHeight || 40}
|
||||||
rowModelType="infinite"
|
rowModelType="clientSide"
|
||||||
onBodyScrollEnd={handleScroll}
|
onBodyScrollEnd={handleScroll}
|
||||||
onCellContextMenu={handleContextMenu}
|
onCellContextMenu={handleContextMenu}
|
||||||
onColumnMoved={handleColumnChange}
|
onColumnMoved={handleColumnChange}
|
||||||
|
|||||||
+93
-72
@@ -1,53 +1,50 @@
|
|||||||
import { useCallback, ChangeEvent, MutableRefObject, MouseEvent } from 'react';
|
|
||||||
import { IDatasource } from '@ag-grid-community/core';
|
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { Divider, Flex, Group, Stack } from '@mantine/core';
|
import { Divider, Flex, Group, Stack } from '@mantine/core';
|
||||||
import { closeAllModals, openModal } from '@mantine/modals';
|
import { closeAllModals, openModal } from '@mantine/modals';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
RiMoreFill,
|
|
||||||
RiSettings3Fill,
|
|
||||||
RiPlayFill,
|
|
||||||
RiAddCircleFill,
|
|
||||||
RiAddBoxFill,
|
RiAddBoxFill,
|
||||||
RiEditFill,
|
RiAddCircleFill,
|
||||||
RiDeleteBinFill,
|
RiDeleteBinFill,
|
||||||
|
RiEditFill,
|
||||||
|
RiMoreFill,
|
||||||
|
RiPlayFill,
|
||||||
RiRefreshLine,
|
RiRefreshLine,
|
||||||
|
RiSettings3Fill,
|
||||||
} from 'react-icons/ri';
|
} from 'react-icons/ri';
|
||||||
import { api } from '/@/renderer/api';
|
import { useNavigate, useParams } from 'react-router';
|
||||||
|
import i18n from '/@/i18n/i18n';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { LibraryItem, PlaylistSongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types';
|
import { LibraryItem, PlaylistSongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
|
||||||
Button,
|
Button,
|
||||||
Slider,
|
ConfirmModal,
|
||||||
|
DropdownMenu,
|
||||||
MultiSelect,
|
MultiSelect,
|
||||||
|
Slider,
|
||||||
Switch,
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
ConfirmModal,
|
|
||||||
toast,
|
toast,
|
||||||
} from '/@/renderer/components';
|
} from '/@/renderer/components';
|
||||||
|
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
|
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
|
||||||
|
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
|
||||||
|
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
||||||
|
import { OrderToggleButton } from '/@/renderer/features/shared';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import {
|
import {
|
||||||
useCurrentServer,
|
useCurrentServer,
|
||||||
SongListFilter,
|
|
||||||
usePlaylistDetailStore,
|
usePlaylistDetailStore,
|
||||||
useSetPlaylistDetailFilters,
|
useSetPlaylistDetailFilters,
|
||||||
useSetPlaylistDetailTable,
|
useSetPlaylistDetailTable,
|
||||||
useSetPlaylistStore,
|
useSetPlaylistStore,
|
||||||
useSetPlaylistTablePagination,
|
useSetPlaylistTablePagination,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { ListDisplayType, ServerType, Play, TableColumn } from '/@/renderer/types';
|
import { ListDisplayType, Play, ServerType, TableColumn } from '/@/renderer/types';
|
||||||
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
|
||||||
import { useParams, useNavigate } from 'react-router';
|
|
||||||
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
|
||||||
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
|
|
||||||
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
|
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
|
||||||
import { OrderToggleButton } from '/@/renderer/features/shared';
|
|
||||||
import i18n from '/@/i18n/i18n';
|
|
||||||
|
|
||||||
const FILTERS = {
|
const FILTERS = {
|
||||||
jellyfin: [
|
jellyfin: [
|
||||||
@@ -150,7 +147,7 @@ const FILTERS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
defaultOrder: SortOrder.ASC,
|
defaultOrder: SortOrder.ASC,
|
||||||
name: i18n.t('filter.playCount', { postProcess: 'titleCase' }),
|
name: i18n.t('filter.genre', { postProcess: 'titleCase' }),
|
||||||
value: SongListSort.GENRE,
|
value: SongListSort.GENRE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -184,6 +181,68 @@ const FILTERS = {
|
|||||||
value: SongListSort.YEAR,
|
value: SongListSort.YEAR,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
subsonic: [
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.album', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.ALBUM,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.ALBUM_ARTIST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.ARTIST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.DURATION,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.FAVORITED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.genre', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.GENRE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.NAME,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.RATING,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.RECENTLY_ADDED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.RECENTLY_PLAYED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.YEAR,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PlaylistDetailSongListHeaderFiltersProps {
|
interface PlaylistDetailSongListHeaderFiltersProps {
|
||||||
@@ -228,56 +287,18 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
setTable({ rowHeight: e });
|
setTable({ rowHeight: e });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterChange = useCallback(
|
const handleFilterChange = useCallback(async () => {
|
||||||
async (filters: SongListFilter) => {
|
tableRef.current?.api.redrawRows();
|
||||||
const dataSource: IDatasource = {
|
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||||
getRows: async (params) => {
|
|
||||||
const limit = params.endRow - params.startRow;
|
|
||||||
const startIndex = params.startRow;
|
|
||||||
|
|
||||||
const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId, {
|
if (page.display === ListDisplayType.TABLE_PAGINATED) {
|
||||||
id: playlistId,
|
setPagination({ data: { currentPage: 0 } });
|
||||||
limit,
|
}
|
||||||
startIndex,
|
}, [tableRef, page.display, setPagination]);
|
||||||
...filters,
|
|
||||||
});
|
|
||||||
|
|
||||||
const songsRes = await queryClient.fetchQuery(
|
|
||||||
queryKey,
|
|
||||||
async ({ signal }) =>
|
|
||||||
api.controller.getPlaylistSongList({
|
|
||||||
apiClientProps: {
|
|
||||||
server,
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
id: playlistId,
|
|
||||||
limit,
|
|
||||||
startIndex,
|
|
||||||
...filters,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{ cacheTime: 1000 * 60 * 1 },
|
|
||||||
);
|
|
||||||
|
|
||||||
params.successCallback(songsRes?.items || [], songsRes?.totalRecordCount || 0);
|
|
||||||
},
|
|
||||||
rowCount: undefined,
|
|
||||||
};
|
|
||||||
tableRef.current?.api.setDatasource(dataSource);
|
|
||||||
tableRef.current?.api.purgeInfiniteCache();
|
|
||||||
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
|
||||||
|
|
||||||
if (page.display === ListDisplayType.TABLE_PAGINATED) {
|
|
||||||
setPagination({ data: { currentPage: 0 } });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[tableRef, page.display, server, playlistId, queryClient, setPagination],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
queryClient.invalidateQueries(queryKeys.albums.list(server?.id || ''));
|
queryClient.invalidateQueries(queryKeys.albums.list(server?.id || ''));
|
||||||
handleFilterChange({ ...page?.table.id[playlistId].filter, ...filters });
|
handleFilterChange();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetSortBy = useCallback(
|
const handleSetSortBy = useCallback(
|
||||||
@@ -288,20 +309,20 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
(f) => f.value === e.currentTarget.value,
|
(f) => f.value === e.currentTarget.value,
|
||||||
)?.defaultOrder;
|
)?.defaultOrder;
|
||||||
|
|
||||||
const updatedFilters = setFilter(playlistId, {
|
setFilter(playlistId, {
|
||||||
sortBy: e.currentTarget.value as SongListSort,
|
sortBy: e.currentTarget.value as SongListSort,
|
||||||
sortOrder: sortOrder || SortOrder.ASC,
|
sortOrder: sortOrder || SortOrder.ASC,
|
||||||
});
|
});
|
||||||
|
|
||||||
handleFilterChange(updatedFilters);
|
handleFilterChange();
|
||||||
},
|
},
|
||||||
[handleFilterChange, playlistId, server?.type, setFilter],
|
[handleFilterChange, playlistId, server?.type, setFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleToggleSortOrder = useCallback(() => {
|
const handleToggleSortOrder = useCallback(() => {
|
||||||
const newSortOrder = filters.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
const newSortOrder = filters.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
||||||
const updatedFilters = setFilter(playlistId, { sortOrder: newSortOrder });
|
setFilter(playlistId, { sortOrder: newSortOrder });
|
||||||
handleFilterChange(updatedFilters);
|
handleFilterChange();
|
||||||
}, [filters.sortOrder, handleFilterChange, playlistId, setFilter]);
|
}, [filters.sortOrder, handleFilterChange, playlistId, setFilter]);
|
||||||
|
|
||||||
const handleSetViewType = useCallback(
|
const handleSetViewType = useCallback(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { MutableRefObject } from 'react';
|
import { MutableRefObject } from 'react';
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { Stack } from '@mantine/core';
|
import { Flex, Stack } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
import { LibraryItem } from '/@/renderer/api/types';
|
||||||
@@ -45,23 +45,30 @@ export const PlaylistDetailSongListHeader = ({
|
|||||||
return (
|
return (
|
||||||
<Stack spacing={0}>
|
<Stack spacing={0}>
|
||||||
<PageHeader backgroundColor="var(--titlebar-bg)">
|
<PageHeader backgroundColor="var(--titlebar-bg)">
|
||||||
<LibraryHeaderBar>
|
<Flex
|
||||||
<LibraryHeaderBar.PlayButton onClick={() => handlePlay(playButtonBehavior)} />
|
justify="space-between"
|
||||||
<LibraryHeaderBar.Title>{detailQuery?.data?.name}</LibraryHeaderBar.Title>
|
w="100%"
|
||||||
<Paper
|
>
|
||||||
fw="600"
|
<LibraryHeaderBar>
|
||||||
px="1rem"
|
<LibraryHeaderBar.PlayButton
|
||||||
py="0.3rem"
|
onClick={() => handlePlay(playButtonBehavior)}
|
||||||
radius="sm"
|
/>
|
||||||
>
|
<LibraryHeaderBar.Title>{detailQuery?.data?.name}</LibraryHeaderBar.Title>
|
||||||
{itemCount === null || itemCount === undefined ? (
|
<Paper
|
||||||
<SpinnerIcon />
|
fw="600"
|
||||||
) : (
|
px="1rem"
|
||||||
itemCount
|
py="0.3rem"
|
||||||
)}
|
radius="sm"
|
||||||
</Paper>
|
>
|
||||||
{isSmartPlaylist && <Badge size="lg">{t('entity.smartPlaylist')}</Badge>}
|
{itemCount === null || itemCount === undefined ? (
|
||||||
</LibraryHeaderBar>
|
<SpinnerIcon />
|
||||||
|
) : (
|
||||||
|
itemCount
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
{isSmartPlaylist && <Badge size="lg">{t('entity.smartPlaylist')}</Badge>}
|
||||||
|
</LibraryHeaderBar>
|
||||||
|
</Flex>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Paper p="1rem">
|
<Paper p="1rem">
|
||||||
<PlaylistDetailSongListHeaderFilters
|
<PlaylistDetailSongListHeaderFilters
|
||||||
|
|||||||
@@ -139,28 +139,20 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
|
|
||||||
const page = usePlaylistDetailStore();
|
const page = usePlaylistDetailStore();
|
||||||
const filters: Partial<PlaylistSongListQuery> = {
|
const filters: Partial<PlaylistSongListQuery> = {
|
||||||
sortBy: page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID,
|
sortBy: page?.table.id[playlistId]?.filter?.sortBy,
|
||||||
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC,
|
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder,
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemCountCheck = usePlaylistSongList({
|
const { data } = usePlaylistSongList({
|
||||||
options: {
|
|
||||||
cacheTime: 1000 * 60 * 60 * 2,
|
|
||||||
staleTime: 1000 * 60 * 60 * 2,
|
|
||||||
},
|
|
||||||
query: {
|
query: {
|
||||||
id: playlistId,
|
id: playlistId,
|
||||||
limit: 1,
|
sortBy: filters.sortBy || SongListSort.ID,
|
||||||
startIndex: 0,
|
sortOrder: filters.sortOrder || SortOrder.ASC,
|
||||||
...filters,
|
|
||||||
},
|
},
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemCount =
|
const itemCount = data?.items.length;
|
||||||
itemCountCheck.data?.totalRecordCount === null
|
|
||||||
? undefined
|
|
||||||
: itemCountCheck.data?.totalRecordCount;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
||||||
@@ -206,7 +198,10 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<PlaylistDetailSongListContent tableRef={tableRef} />
|
<PlaylistDetailSongListContent
|
||||||
|
songs={data?.items || []}
|
||||||
|
tableRef={tableRef}
|
||||||
|
/>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user