mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
add initial playlist reimplementation
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import Fuse from 'fuse.js';
|
||||
import Fuse, { IFuseOptions } from 'fuse.js';
|
||||
|
||||
import {
|
||||
InternetProviderLyricSearchResponse,
|
||||
@@ -11,7 +11,7 @@ export const orderSearchResults = (args: {
|
||||
}) => {
|
||||
const { params, results } = args;
|
||||
|
||||
const options: Fuse.IFuseOptions<InternetProviderLyricSearchResponse> = {
|
||||
const options: IFuseOptions<InternetProviderLyricSearchResponse> = {
|
||||
fieldNormWeight: 1,
|
||||
includeScore: true,
|
||||
keys: [
|
||||
|
||||
@@ -301,7 +301,10 @@ export const queryKeys: Record<
|
||||
},
|
||||
root: (serverId: string) => [serverId, 'playlists'] as const,
|
||||
songList: (serverId: string, id?: string) => {
|
||||
if (id) return [serverId, 'playlists', id, 'songList'] as const;
|
||||
if (id) {
|
||||
return [serverId, 'playlists', 'songList', id] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'playlists', 'songList'] as const;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
ssType,
|
||||
SubsonicExtensions,
|
||||
} from '/@/shared/api/subsonic/subsonic-types';
|
||||
import { sortAlbumArtistList, sortAlbumList, sortSongList } from '/@/shared/api/utils';
|
||||
import {
|
||||
AlbumListSort,
|
||||
GenreListSort,
|
||||
@@ -21,10 +22,7 @@ import {
|
||||
LibraryItem,
|
||||
PlaylistListSort,
|
||||
Song,
|
||||
sortAlbumArtistList,
|
||||
sortAlbumList,
|
||||
SortOrder,
|
||||
sortSongList,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ServerFeatures } from '/@/shared/types/features-types';
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||
import { AddToQueueType, usePlayerActions } from '/@/renderer/store';
|
||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||
import { sortSongsByFetchedOrder } from '/@/shared/api/utils';
|
||||
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
||||
import { ConfirmModal } from '/@/shared/components/modal/modal';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
@@ -30,7 +31,6 @@ import {
|
||||
SongListResponse,
|
||||
SongListSort,
|
||||
SortOrder,
|
||||
sortSongsByFetchedOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { Play, PlayerRepeat, PlayerShuffle } from '/@/shared/types/types';
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { sortSongList } from '/@/shared/api/utils';
|
||||
import {
|
||||
PlaylistSongListQuery,
|
||||
PlaylistSongListQueryClientSide,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
SongListResponse,
|
||||
SongListSort,
|
||||
SortOrder,
|
||||
sortSongList,
|
||||
} from '/@/shared/types/domain-types';
|
||||
|
||||
export const getPlaylistSongsById = async (args: {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { ItemListSettings, useCurrentServer, useListSettings } from '/@/renderer/store';
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
import { PlaylistSongListQuery } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, ListDisplayType } from '/@/shared/types/types';
|
||||
|
||||
const PlaylistDetailSongListTable = lazy(() =>
|
||||
import('/@/renderer/features/playlists/components/playlist-detail-song-list-table').then(
|
||||
(module) => ({
|
||||
default: module.PlaylistDetailSongListTable,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
export const PlaylistDetailSongListContent = () => {
|
||||
const { display, grid, itemsPerPage, pagination, table } = useListSettings(
|
||||
ItemListKey.PLAYLIST_SONG,
|
||||
);
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
<PlaylistDetailSongListView
|
||||
display={display}
|
||||
grid={grid}
|
||||
itemsPerPage={itemsPerPage}
|
||||
pagination={pagination}
|
||||
playlistId={playlistId}
|
||||
table={table}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export type OverridePlaylistSongListQuery = Omit<Partial<PlaylistSongListQuery>, 'id'>;
|
||||
|
||||
export const PlaylistDetailSongListView = ({
|
||||
display,
|
||||
playlistId,
|
||||
table,
|
||||
}: ItemListSettings & {
|
||||
playlistId: string;
|
||||
}) => {
|
||||
const server = useCurrentServer();
|
||||
|
||||
switch (display) {
|
||||
case ListDisplayType.TABLE: {
|
||||
return (
|
||||
<PlaylistDetailSongListTable
|
||||
autoFitColumns={table.autoFitColumns}
|
||||
columns={table.columns}
|
||||
enableAlternateRowColors={table.enableAlternateRowColors}
|
||||
enableHorizontalBorders={table.enableHorizontalBorders}
|
||||
enableRowHoverHighlight={table.enableRowHoverHighlight}
|
||||
enableVerticalBorders={table.enableVerticalBorders}
|
||||
playlistId={playlistId}
|
||||
serverId={server.id}
|
||||
size={table.size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
+108
-364
@@ -1,299 +1,42 @@
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { ChangeEvent, MouseEvent, useCallback } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { PLAYLIST_SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
|
||||
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
|
||||
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
|
||||
import { OrderToggleButton } from '/@/renderer/features/shared/components/order-toggle-button';
|
||||
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import {
|
||||
useCurrentServer,
|
||||
usePlaylistDetailStore,
|
||||
useSetPlaylistDetailFilters,
|
||||
useSetPlaylistStore,
|
||||
} from '/@/renderer/store';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { useCurrentServerId } from '/@/renderer/store';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { ConfirmModal } from '/@/shared/components/modal/modal';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { ServerType, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||
import { ListDisplayType, Play } from '/@/shared/types/types';
|
||||
import { LibraryItem, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, Play } from '/@/shared/types/types';
|
||||
|
||||
const FILTERS = {
|
||||
jellyfin: [
|
||||
{
|
||||
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.ASC,
|
||||
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.DURATION,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.playCount', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.PLAY_COUNT,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.NAME,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.RANDOM,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.RECENTLY_ADDED,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.RECENTLY_PLAYED,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.releaseDate', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.RELEASE_DATE,
|
||||
},
|
||||
],
|
||||
navidrome: [
|
||||
{
|
||||
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.bpm', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.BPM,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('common.channel', { count: 2, postProcess: 'titleCase' }),
|
||||
value: SongListSort.CHANNELS,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.comment', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.COMMENT,
|
||||
},
|
||||
{
|
||||
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.playCount', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.PLAY_COUNT,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
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.releaseYear', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.YEAR,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
interface PlaylistDetailSongListHeaderFiltersProps {
|
||||
handlePlay: (playType: Play) => void;
|
||||
handleToggleShowQueryBuilder: () => void;
|
||||
tableRef: any;
|
||||
}
|
||||
|
||||
export const PlaylistDetailSongListHeaderFilters = ({
|
||||
handlePlay,
|
||||
handleToggleShowQueryBuilder,
|
||||
tableRef,
|
||||
}: PlaylistDetailSongListHeaderFiltersProps) => {
|
||||
export const PlaylistDetailSongListHeaderFilters = () => {
|
||||
const { t } = useTranslation();
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const server = useCurrentServer();
|
||||
const setPage = useSetPlaylistStore();
|
||||
const setFilter = useSetPlaylistDetailFilters();
|
||||
const page = usePlaylistDetailStore();
|
||||
const searchTerm = page?.table.id[playlistId]?.filter?.searchTerm;
|
||||
const sortBy = page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID;
|
||||
const sortOrder = page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC;
|
||||
const serverId = useCurrentServerId();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const detailQuery = useQuery(playlistsQueries.detail({ query: { id: playlistId }, serverId }));
|
||||
|
||||
const detailQuery = useQuery(
|
||||
playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
|
||||
);
|
||||
const isSmartPlaylist = detailQuery.data?.rules;
|
||||
|
||||
const { ref, ...cq } = useContainerQuery();
|
||||
|
||||
const sortByLabel =
|
||||
(server?.type &&
|
||||
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === sortBy)?.name) ||
|
||||
'Unknown';
|
||||
|
||||
const handleFilterChange = useCallback(async () => {
|
||||
// tableRef.current?.api.redrawRows();
|
||||
// tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||
// if (page.display === ListDisplayType.TABLE) {
|
||||
// setPagination({ data: { currentPage: 0 } });
|
||||
// }
|
||||
}, []);
|
||||
|
||||
const handleRefresh = () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.playlists.songList(server?.id || '', playlistId),
|
||||
});
|
||||
};
|
||||
|
||||
const handleSetSortBy = useCallback((e: MouseEvent<HTMLButtonElement>) => {}, []);
|
||||
|
||||
const handleToggleSortOrder = useCallback(() => {}, [
|
||||
sortOrder,
|
||||
handleFilterChange,
|
||||
playlistId,
|
||||
setFilter,
|
||||
]);
|
||||
|
||||
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {}, 500);
|
||||
|
||||
const handleSetViewType = useCallback((displayType: ListDisplayType) => {}, [page, setPage]);
|
||||
|
||||
const deletePlaylistMutation = useDeletePlaylist({});
|
||||
|
||||
const handleDeletePlaylist = useCallback(() => {
|
||||
@@ -332,99 +75,100 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
<Group gap="sm" ref={ref} w="100%">
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
tooltip={{
|
||||
label: t('page.playlist.reorder', { postProcess: 'sentenceCase' }),
|
||||
}}
|
||||
variant="subtle"
|
||||
>
|
||||
{sortByLabel}
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{FILTERS[server?.type as keyof typeof FILTERS].map((filter) => (
|
||||
<DropdownMenu.Item
|
||||
isSelected={filter.value === sortBy}
|
||||
key={`filter-${filter.name}`}
|
||||
onClick={handleSetSortBy}
|
||||
value={filter.value}
|
||||
>
|
||||
{filter.name}
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
|
||||
<Divider orientation="vertical" />
|
||||
<OrderToggleButton
|
||||
onToggle={handleToggleSortOrder}
|
||||
sortOrder={sortOrder || SortOrder.ASC}
|
||||
<ListSortByDropdown
|
||||
defaultSortByValue={SongListSort.ID}
|
||||
itemType={LibraryItem.PLAYLIST_SONG}
|
||||
listKey={ItemListKey.PLAYLIST_SONG}
|
||||
/>
|
||||
<Divider orientation="vertical" />
|
||||
<ListSortOrderToggleButton
|
||||
defaultSortOrder={SortOrder.ASC}
|
||||
listKey={ItemListKey.PLAYLIST_SONG}
|
||||
/>
|
||||
<ListRefreshButton listKey={ItemListKey.PLAYLIST_SONG} />
|
||||
</Group>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<ListConfigMenu
|
||||
listKey={ItemListKey.PLAYLIST_SONG}
|
||||
tableColumnsData={PLAYLIST_SONG_TABLE_COLUMNS}
|
||||
/>
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<MoreButton />
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="mediaPlay" />}
|
||||
onClick={() => handlePlay(Play.NOW)}
|
||||
>
|
||||
{t('player.play', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="mediaPlayLast" />}
|
||||
onClick={() => handlePlay(Play.LAST)}
|
||||
>
|
||||
{t('player.addLast', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="mediaPlayNext" />}
|
||||
onClick={() => handlePlay(Play.NEXT)}
|
||||
>
|
||||
{t('player.addNext', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="edit" />}
|
||||
onClick={() =>
|
||||
openUpdatePlaylistModal({
|
||||
playlist: detailQuery.data!,
|
||||
server: server!,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('action.editPlaylist', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="delete" />}
|
||||
onClick={openDeletePlaylistModal}
|
||||
>
|
||||
{t('action.deletePlaylist', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
leftSection={<Icon icon="refresh" />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
{t('action.refresh', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
{server?.type === ServerType.NAVIDROME && !isSmartPlaylist && (
|
||||
<>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item isDanger onClick={handleToggleShowQueryBuilder}>
|
||||
{t('action.toggleSmartPlaylistEditor', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
<SearchInput defaultValue={searchTerm} onChange={handleSearch} />
|
||||
</Group>
|
||||
<Group></Group>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
// const GenreFilterSelection = () => {
|
||||
// const { t } = useTranslation();
|
||||
// const { playlistId } = useParams() as { playlistId: string };
|
||||
// const serverId = useCurrentServerId();
|
||||
|
||||
// const { data } = useQuery(playlistsQueries.songList({ query: { id: playlistId }, serverId }));
|
||||
|
||||
// const genres = useMemo(() => {
|
||||
// const uniqueGenres = new Map<string, string>();
|
||||
|
||||
// data?.items.forEach((song) => {
|
||||
// song.genres.forEach((genre) => {
|
||||
// if (genre.id) {
|
||||
// uniqueGenres.set(genre.id, genre.name);
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
// return Array.from(uniqueGenres.entries()).map(([id, name]) => ({
|
||||
// label: name,
|
||||
// value: id,
|
||||
// }));
|
||||
// }, [data?.items]);
|
||||
|
||||
// return (
|
||||
// <Stack p="md" style={{ background: 'var(--theme-colors-surface)', height: '12rem' }}>
|
||||
// <Text>{t('filter.genre', { postProcess: 'titleCase' })}</Text>
|
||||
// <ScrollArea>
|
||||
// <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
|
||||
// {genres.map((genre) => (
|
||||
// <li key={genre.value}>{genre.label}</li>
|
||||
// ))}
|
||||
// </ul>
|
||||
// </ScrollArea>
|
||||
// </Stack>
|
||||
// );
|
||||
// };
|
||||
|
||||
// const ArtistFilterSelection = () => {
|
||||
// const { t } = useTranslation();
|
||||
// const { playlistId } = useParams() as { playlistId: string };
|
||||
// const serverId = useCurrentServerId();
|
||||
|
||||
// const { data } = useQuery(playlistsQueries.songList({ query: { id: playlistId }, serverId }));
|
||||
|
||||
// const artists = useMemo(() => {
|
||||
// const uniqueArtists = new Map<string, string>();
|
||||
|
||||
// data?.items.forEach((song) => {
|
||||
// song.artists.forEach((artist) => {
|
||||
// if (artist.id) {
|
||||
// uniqueArtists.set(artist.id, artist.name);
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
// return Array.from(uniqueArtists.entries()).map(([id, name]) => ({
|
||||
// label: name,
|
||||
// value: id,
|
||||
// }));
|
||||
// }, [data?.items]);
|
||||
|
||||
// return (
|
||||
// <Stack style={{ height: '12rem' }}>
|
||||
// <Text>{t('filter.artist', { postProcess: 'titleCase' })}</Text>
|
||||
// <ScrollArea>
|
||||
// <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
|
||||
// {artists.map((artist) => (
|
||||
// <li key={artist.value}>{artist.label}</li>
|
||||
// ))}
|
||||
// </ul>
|
||||
// </ScrollArea>
|
||||
// </Stack>
|
||||
// );
|
||||
// };
|
||||
|
||||
@@ -1,37 +1,25 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { MutableRefObject } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
||||
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||
import { PlaylistDetailSongListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header-filters';
|
||||
import { FilterBar } from '/@/renderer/features/shared/components/filter-bar';
|
||||
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
||||
import { ListSearchInput } from '/@/renderer/features/shared/components/list-search-input';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { formatDurationString } from '/@/renderer/utils';
|
||||
import { Badge } from '/@/shared/components/badge/badge';
|
||||
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
interface PlaylistDetailHeaderProps {
|
||||
handlePlay: (playType: Play) => void;
|
||||
handleToggleShowQueryBuilder: () => void;
|
||||
itemCount?: number;
|
||||
tableRef: MutableRefObject<ItemListHandle | null>;
|
||||
}
|
||||
|
||||
export const PlaylistDetailSongListHeader = ({
|
||||
handlePlay,
|
||||
handleToggleShowQueryBuilder,
|
||||
itemCount,
|
||||
tableRef,
|
||||
}: PlaylistDetailHeaderProps) => {
|
||||
export const PlaylistDetailSongListHeader = () => {
|
||||
const { t } = useTranslation();
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const { itemCount } = useListContext();
|
||||
const server = useCurrentServer();
|
||||
const detailQuery = useQuery(
|
||||
playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
|
||||
@@ -60,13 +48,10 @@ export const PlaylistDetailSongListHeader = ({
|
||||
</Badge>
|
||||
{isSmartPlaylist && <Badge size="lg">{t('entity.smartPlaylist')}</Badge>}
|
||||
</LibraryHeaderBar>
|
||||
<ListSearchInput />
|
||||
</PageHeader>
|
||||
<FilterBar>
|
||||
<PlaylistDetailSongListHeaderFilters
|
||||
handlePlay={handlePlay}
|
||||
handleToggleShowQueryBuilder={handleToggleShowQueryBuilder}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<PlaylistDetailSongListHeaderFilters />
|
||||
</FilterBar>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { forwardRef, useMemo } from 'react';
|
||||
|
||||
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
||||
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
||||
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||
import { ItemControls, ItemListTableComponentProps } from '/@/renderer/components/item-list/types';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||
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 { usePlayerSong } from '/@/renderer/store';
|
||||
import { sortSongList } from '/@/shared/api/utils';
|
||||
import { LibraryItem, PlaylistSongListQuery, Song } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, Play } from '/@/shared/types/types';
|
||||
|
||||
interface PlaylistDetailSongListTableProps
|
||||
extends Omit<ItemListTableComponentProps<PlaylistSongListQuery>, 'query'> {
|
||||
playlistId: string;
|
||||
}
|
||||
|
||||
export const PlaylistDetailSongListTable = forwardRef<any, PlaylistDetailSongListTableProps>(
|
||||
(
|
||||
{
|
||||
autoFitColumns = false,
|
||||
columns,
|
||||
enableAlternateRowColors = false,
|
||||
enableHorizontalBorders = false,
|
||||
enableRowHoverHighlight = true,
|
||||
enableSelection = true,
|
||||
enableVerticalBorders = false,
|
||||
playlistId,
|
||||
saveScrollOffset = true,
|
||||
serverId,
|
||||
size = 'default',
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const playlistSongs = useQuery(
|
||||
playlistsQueries.songList({
|
||||
query: { id: playlistId },
|
||||
serverId: serverId,
|
||||
}),
|
||||
);
|
||||
|
||||
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
|
||||
enabled: saveScrollOffset,
|
||||
});
|
||||
|
||||
const { handleColumnReordered } = useItemListColumnReorder({
|
||||
itemListKey: ItemListKey.PLAYLIST_SONG,
|
||||
});
|
||||
|
||||
const { handleColumnResized } = useItemListColumnResize({
|
||||
itemListKey: ItemListKey.PLAYLIST_SONG,
|
||||
});
|
||||
|
||||
const { searchTerm } = useSearchTermFilter();
|
||||
const { query } = usePlaylistSongListFilters();
|
||||
|
||||
const filterSortedSongs = useMemo(() => {
|
||||
let items = playlistSongs.data?.items || [];
|
||||
|
||||
if (searchTerm) {
|
||||
if (searchTerm) {
|
||||
items = searchLibraryItems(items, searchTerm, LibraryItem.SONG);
|
||||
}
|
||||
|
||||
return sortSongList(items, query.sortBy, query.sortOrder);
|
||||
}
|
||||
|
||||
return sortSongList(items, query.sortBy, query.sortOrder);
|
||||
}, [playlistSongs.data?.items, searchTerm, query.sortBy, query.sortOrder]);
|
||||
|
||||
const player = usePlayer();
|
||||
|
||||
const currentSong = usePlayerSong();
|
||||
|
||||
const overrideControls: Partial<ItemControls> = useMemo(() => {
|
||||
return {
|
||||
onDoubleClick: ({ index, internalState, item }) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = internalState?.getData() as Song[];
|
||||
|
||||
if (index !== undefined) {
|
||||
player.addToQueueByData(items, Play.NOW);
|
||||
player.mediaPlayByIndex(index);
|
||||
}
|
||||
},
|
||||
};
|
||||
}, [player]);
|
||||
|
||||
return (
|
||||
<ItemTableList
|
||||
activeRowId={currentSong?.id}
|
||||
autoFitColumns={autoFitColumns}
|
||||
CellComponent={ItemTableListColumn}
|
||||
columns={columns}
|
||||
data={filterSortedSongs}
|
||||
enableAlternateRowColors={enableAlternateRowColors}
|
||||
enableExpansion={false}
|
||||
enableHorizontalBorders={enableHorizontalBorders}
|
||||
enableRowHoverHighlight={enableRowHoverHighlight}
|
||||
enableSelection={enableSelection}
|
||||
enableVerticalBorders={enableVerticalBorders}
|
||||
getRowId="playlistItemId"
|
||||
initialTop={{
|
||||
to: scrollOffset ?? 0,
|
||||
type: 'offset',
|
||||
}}
|
||||
itemType={LibraryItem.PLAYLIST_SONG}
|
||||
onColumnReordered={handleColumnReordered}
|
||||
onColumnResized={handleColumnResized}
|
||||
onScrollEnd={handleOnScrollEnd}
|
||||
overrideControls={overrideControls}
|
||||
ref={ref}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
parseAsArrayOf,
|
||||
parseAsBoolean,
|
||||
parseAsInteger,
|
||||
parseAsJson,
|
||||
parseAsString,
|
||||
useQueryState,
|
||||
} from 'nuqs';
|
||||
|
||||
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
|
||||
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
|
||||
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
||||
import { customFiltersSchema, FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||
import { SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
export const usePlaylistSongListFilters = () => {
|
||||
const { sortBy } = useSortByFilter<SongListSort>(SongListSort.ID, ItemListKey.PLAYLIST_SONG);
|
||||
|
||||
const { sortOrder } = useSortOrderFilter(SortOrder.ASC, ItemListKey.PLAYLIST_SONG);
|
||||
|
||||
const { searchTerm, setSearchTerm } = useSearchTermFilter('');
|
||||
|
||||
const [albumIds, setAlbumIds] = useQueryState(
|
||||
FILTER_KEYS.SONG.ALBUM_IDS,
|
||||
parseAsArrayOf(parseAsString),
|
||||
);
|
||||
|
||||
const [genreId, setGenreId] = useQueryState(
|
||||
FILTER_KEYS.SONG.GENRE_ID,
|
||||
parseAsArrayOf(parseAsString),
|
||||
);
|
||||
|
||||
const [artistIds, setArtistIds] = useQueryState(
|
||||
FILTER_KEYS.SONG.ARTIST_IDS,
|
||||
parseAsArrayOf(parseAsString),
|
||||
);
|
||||
|
||||
const [minYear, setMinYear] = useQueryState(FILTER_KEYS.SONG.MIN_YEAR, parseAsInteger);
|
||||
|
||||
const [maxYear, setMaxYear] = useQueryState(FILTER_KEYS.SONG.MAX_YEAR, parseAsInteger);
|
||||
|
||||
const [favorite, setFavorite] = useQueryState(FILTER_KEYS.SONG.FAVORITE, parseAsBoolean);
|
||||
|
||||
const [custom, setCustom] = useQueryState(
|
||||
FILTER_KEYS.SONG._CUSTOM,
|
||||
parseAsJson(customFiltersSchema),
|
||||
);
|
||||
|
||||
const query = {
|
||||
[FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
|
||||
[FILTER_KEYS.SHARED.SORT_ORDER]: sortOrder ?? undefined,
|
||||
[FILTER_KEYS.SONG._CUSTOM]: custom ?? undefined,
|
||||
[FILTER_KEYS.SONG.ALBUM_IDS]: albumIds ?? undefined,
|
||||
[FILTER_KEYS.SONG.ARTIST_IDS]: artistIds ?? undefined,
|
||||
[FILTER_KEYS.SONG.FAVORITE]: favorite ?? undefined,
|
||||
[FILTER_KEYS.SONG.GENRE_ID]: genreId ?? undefined,
|
||||
[FILTER_KEYS.SONG.MAX_YEAR]: maxYear ?? undefined,
|
||||
[FILTER_KEYS.SONG.MIN_YEAR]: minYear ?? undefined,
|
||||
};
|
||||
|
||||
return {
|
||||
query,
|
||||
setAlbumIds,
|
||||
setArtistIds,
|
||||
setCustom,
|
||||
setFavorite,
|
||||
setGenreId,
|
||||
setMaxYear,
|
||||
setMinYear,
|
||||
setSearchTerm,
|
||||
};
|
||||
};
|
||||
@@ -1,12 +1,14 @@
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { motion } from 'motion/react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generatePath, useNavigate, useParams } from 'react-router';
|
||||
|
||||
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
||||
import { ListContext } from '/@/renderer/context/list-context';
|
||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||
import { PlaylistDetailSongListContent } from '/@/renderer/features/playlists/components/playlist-detail-song-list-content';
|
||||
import { PlaylistDetailSongListHeader } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header';
|
||||
import { PlaylistQueryBuilder } from '/@/renderer/features/playlists/components/playlist-query-builder';
|
||||
import { SaveAsPlaylistForm } from '/@/renderer/features/playlists/components/save-as-playlist-form';
|
||||
@@ -16,14 +18,14 @@ import { AnimatedPage } from '/@/renderer/features/shared/components/animated-pa
|
||||
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
|
||||
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useCurrentServer, usePlaylistDetailStore } from '/@/renderer/store';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Box } from '/@/shared/components/box/box';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { ServerType, SongListSort, SortOrder, sortSongList } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
import { ServerType, SongListSort } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, Play } from '/@/shared/types/types';
|
||||
|
||||
const PlaylistDetailSongListRoute = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -145,8 +147,6 @@ const PlaylistDetailSongListRoute = () => {
|
||||
setIsQueryBuilderExpanded(true);
|
||||
};
|
||||
|
||||
const page = usePlaylistDetailStore();
|
||||
|
||||
const playlistSongs = useQuery(
|
||||
playlistsQueries.songList({
|
||||
query: {
|
||||
@@ -156,80 +156,83 @@ const PlaylistDetailSongListRoute = () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const filterSortedSongs = useMemo(() => {
|
||||
const items = playlistSongs.data?.items;
|
||||
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
|
||||
|
||||
if (items) {
|
||||
const searchTerm = page?.table.id[playlistId]?.filter?.searchTerm;
|
||||
|
||||
if (searchTerm) {
|
||||
// items = searchSongs(items, searchTerm);
|
||||
}
|
||||
|
||||
const sortBy = page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID;
|
||||
const sortOrder = page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC;
|
||||
return sortSongList(items, sortBy, sortOrder);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [playlistSongs.data?.items, page?.table.id, playlistId]);
|
||||
|
||||
const itemCount =
|
||||
typeof playlistSongs.data?.totalRecordCount === 'number'
|
||||
? filterSortedSongs.length
|
||||
: undefined;
|
||||
|
||||
const handlePlay = (play: Play) => {
|
||||
const handlePlay = (_play: Play) => {
|
||||
// handlePlayQueueAdd?.({
|
||||
// byData: filterSortedSongs,
|
||||
// playType: play,
|
||||
// });
|
||||
};
|
||||
|
||||
const providerValue = useMemo(() => {
|
||||
return {
|
||||
customFilters: undefined,
|
||||
id: playlistId,
|
||||
itemCount,
|
||||
pageKey: ItemListKey.PLAYLIST_SONG,
|
||||
setItemCount,
|
||||
};
|
||||
}, [playlistId, itemCount]);
|
||||
|
||||
// Update item count when playlist songs are loaded
|
||||
useEffect(() => {
|
||||
if (
|
||||
playlistSongs.data?.totalRecordCount !== undefined &&
|
||||
playlistSongs.data.totalRecordCount !== null
|
||||
) {
|
||||
setItemCount(playlistSongs.data.totalRecordCount);
|
||||
}
|
||||
}, [playlistSongs.data?.totalRecordCount]);
|
||||
|
||||
return (
|
||||
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
||||
<LibraryContainer>
|
||||
<PlaylistDetailSongListHeader
|
||||
handlePlay={handlePlay}
|
||||
handleToggleShowQueryBuilder={handleToggleShowQueryBuilder}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<ListContext.Provider value={providerValue}>
|
||||
<LibraryContainer>
|
||||
<PlaylistDetailSongListHeader
|
||||
handlePlay={handlePlay}
|
||||
handleToggleShowQueryBuilder={handleToggleShowQueryBuilder}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
|
||||
{(isSmartPlaylist || showQueryBuilder) && (
|
||||
<motion.div>
|
||||
<Box h="100%" mah="35vh" p="md" w="100%">
|
||||
<Group pb="md">
|
||||
<ActionIcon
|
||||
icon={isQueryBuilderExpanded ? 'arrowUpS' : 'arrowDownS'}
|
||||
iconProps={{
|
||||
size: 'md',
|
||||
}}
|
||||
onClick={handleToggleExpand}
|
||||
size="xs"
|
||||
/>
|
||||
<Text>
|
||||
{t('form.queryEditor.title', { postProcess: 'titleCase' })}
|
||||
</Text>
|
||||
</Group>
|
||||
{isQueryBuilderExpanded && (
|
||||
<PlaylistQueryBuilder
|
||||
isSaving={createPlaylistMutation?.isPending}
|
||||
key={JSON.stringify(detailQuery?.data?.rules)}
|
||||
limit={detailQuery?.data?.rules?.limit}
|
||||
onSave={handleSave}
|
||||
onSaveAs={handleSaveAs}
|
||||
playlistId={playlistId}
|
||||
query={detailQuery?.data?.rules}
|
||||
sortBy={detailQuery?.data?.rules?.sort || SongListSort.ALBUM}
|
||||
sortOrder={detailQuery?.data?.rules?.order || 'asc'}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
{/* <PlaylistDetailSongListContent songs={filterSortedSongs} tableRef={tableRef} /> */}
|
||||
</LibraryContainer>
|
||||
{(isSmartPlaylist || showQueryBuilder) && (
|
||||
<motion.div>
|
||||
<Box h="100%" mah="35vh" p="md" w="100%">
|
||||
<Group pb="md">
|
||||
<ActionIcon
|
||||
icon={isQueryBuilderExpanded ? 'arrowUpS' : 'arrowDownS'}
|
||||
iconProps={{
|
||||
size: 'md',
|
||||
}}
|
||||
onClick={handleToggleExpand}
|
||||
size="xs"
|
||||
/>
|
||||
<Text>
|
||||
{t('form.queryEditor.title', { postProcess: 'titleCase' })}
|
||||
</Text>
|
||||
</Group>
|
||||
{isQueryBuilderExpanded && (
|
||||
<PlaylistQueryBuilder
|
||||
isSaving={createPlaylistMutation?.isPending}
|
||||
key={JSON.stringify(detailQuery?.data?.rules)}
|
||||
limit={detailQuery?.data?.rules?.limit}
|
||||
onSave={handleSave}
|
||||
onSaveAs={handleSaveAs}
|
||||
playlistId={playlistId}
|
||||
query={detailQuery?.data?.rules}
|
||||
sortBy={
|
||||
detailQuery?.data?.rules?.sort || SongListSort.ALBUM
|
||||
}
|
||||
sortOrder={detailQuery?.data?.rules?.order || 'asc'}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</motion.div>
|
||||
)}
|
||||
<PlaylistDetailSongListContent />
|
||||
</LibraryContainer>
|
||||
</ListContext.Provider>
|
||||
</AnimatedPage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -64,6 +64,89 @@ export const ListSortByDropdown = ({
|
||||
);
|
||||
};
|
||||
|
||||
const CLIENT_SIDE_SONG_FILTERS = [
|
||||
{
|
||||
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.bpm', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.BPM,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('common.channel', { count: 2, postProcess: 'titleCase' }),
|
||||
value: SongListSort.CHANNELS,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.comment', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.COMMENT,
|
||||
},
|
||||
{
|
||||
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.playCount', { postProcess: 'titleCase' }),
|
||||
value: SongListSort.PLAY_COUNT,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
const ALBUM_LIST_FILTERS: Partial<
|
||||
Record<ServerType, Array<{ defaultOrder: SortOrder; name: string; value: string }>>
|
||||
> = {
|
||||
@@ -361,6 +444,14 @@ const SONG_LIST_FILTERS: Partial<
|
||||
],
|
||||
};
|
||||
|
||||
const PLAYLIST_SONG_LIST_FILTERS: Partial<
|
||||
Record<ServerType, Array<{ defaultOrder: SortOrder; name: string; value: string }>>
|
||||
> = {
|
||||
[ServerType.JELLYFIN]: CLIENT_SIDE_SONG_FILTERS,
|
||||
[ServerType.NAVIDROME]: CLIENT_SIDE_SONG_FILTERS,
|
||||
[ServerType.SUBSONIC]: CLIENT_SIDE_SONG_FILTERS,
|
||||
};
|
||||
|
||||
const ALBUM_ARTIST_LIST_FILTERS: Partial<
|
||||
Record<ServerType, Array<{ defaultOrder: SortOrder; name: string; value: string }>>
|
||||
> = {
|
||||
@@ -626,5 +717,6 @@ const FILTERS: Partial<Record<LibraryItem, any>> = {
|
||||
[LibraryItem.ARTIST]: ARTIST_LIST_FILTERS,
|
||||
[LibraryItem.GENRE]: GENRE_LIST_FILTERS,
|
||||
[LibraryItem.PLAYLIST]: PLAYLIST_LIST_FILTERS,
|
||||
[LibraryItem.PLAYLIST_SONG]: PLAYLIST_SONG_LIST_FILTERS,
|
||||
[LibraryItem.SONG]: SONG_LIST_FILTERS,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
|
||||
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||
import { useListFilterPersistence } from '/@/renderer/features/shared/hooks/use-list-filter-persistence';
|
||||
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { SortOrder } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey } from '/@/shared/types/types';
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
ArtistListResponse,
|
||||
FavoriteArgs,
|
||||
LibraryItem,
|
||||
PlaylistSongListResponse,
|
||||
Song,
|
||||
SongDetailResponse,
|
||||
} from '/@/shared/types/domain-types';
|
||||
@@ -551,13 +552,47 @@ export const applyFavoriteOptimisticUpdates = (
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const playlistSongListQueryKey = queryKeys.playlists.songList(
|
||||
variables.apiClientProps.serverId,
|
||||
);
|
||||
|
||||
const playlistSongListQueries = queryClient.getQueriesData({
|
||||
exact: false,
|
||||
queryKey: playlistSongListQueryKey,
|
||||
});
|
||||
|
||||
if (playlistSongListQueries.length) {
|
||||
playlistSongListQueries.forEach(([queryKey, data]) => {
|
||||
if (data) {
|
||||
previousQueries.push({ data, queryKey });
|
||||
queryClient.setQueryData(
|
||||
queryKey,
|
||||
(prev: PlaylistSongListResponse | undefined) => {
|
||||
if (prev) {
|
||||
return {
|
||||
...prev,
|
||||
items: prev.items.map((item: Song) =>
|
||||
itemIdSet.has(item.id)
|
||||
? { ...item, userFavorite: isFavorite }
|
||||
: item,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return prev;
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return previousQueries;
|
||||
};
|
||||
|
||||
export const restoreFavoriteQueryData = (
|
||||
queryClient: QueryClient,
|
||||
previousQueries: PreviousQueryData[],
|
||||
|
||||
@@ -192,13 +192,6 @@ export const createFuseForLibraryItem = <T extends FuseSearchableItem>(
|
||||
},
|
||||
name: 'albumArtists',
|
||||
},
|
||||
{
|
||||
getFn: (item) => {
|
||||
const s = item as QueueSong | Song;
|
||||
return s.genres?.map((genre) => genre.name).join(' ') || '';
|
||||
},
|
||||
name: 'genres',
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
+284
-1
@@ -1,10 +1,24 @@
|
||||
import { AxiosHeaders } from 'axios';
|
||||
import isElectron from 'is-electron';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import reverse from 'lodash/reverse';
|
||||
import shuffle from 'lodash/shuffle';
|
||||
import semverCoerce from 'semver/functions/coerce';
|
||||
import semverGte from 'semver/functions/gte';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ServerListItem } from '/@/shared/types/domain-types';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
AlbumArtistListSort,
|
||||
AlbumListSort,
|
||||
ArtistListSort,
|
||||
LibraryItem,
|
||||
ServerListItem,
|
||||
Song,
|
||||
SongListSort,
|
||||
SortOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
|
||||
// Since ts-rest client returns a strict response type, we need to add the headers to the body object
|
||||
@@ -113,3 +127,272 @@ export const getClientType = (): string => {
|
||||
};
|
||||
|
||||
export const SEPARATOR_STRING = ' · ';
|
||||
|
||||
export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: SortOrder) => {
|
||||
let results = songs;
|
||||
|
||||
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
|
||||
|
||||
switch (sortBy) {
|
||||
case SongListSort.ALBUM:
|
||||
results = orderBy(
|
||||
results,
|
||||
[(v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, 'asc', 'asc'],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.ALBUM_ARTIST:
|
||||
results = orderBy(
|
||||
results,
|
||||
[(v) => v.albumArtists[0]?.name.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, order, 'asc', 'asc'],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.ARTIST:
|
||||
results = orderBy(
|
||||
results,
|
||||
[(v) => v.artistName?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, order, 'asc', 'asc'],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.BPM:
|
||||
results = orderBy(results, ['bpm'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.CHANNELS:
|
||||
results = orderBy(results, ['channels'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.COMMENT:
|
||||
results = orderBy(results, ['comment'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.DURATION:
|
||||
results = orderBy(results, ['duration'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.FAVORITED:
|
||||
results = orderBy(results, ['userFavorite', (v) => v.name.toLowerCase()], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.GENRE:
|
||||
results = orderBy(
|
||||
results,
|
||||
[
|
||||
(v) => v.genres?.[0]?.name.toLowerCase(),
|
||||
(v) => v.album?.toLowerCase(),
|
||||
'discNumber',
|
||||
'trackNumber',
|
||||
],
|
||||
[order, order, 'asc', 'asc'],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.ID:
|
||||
if (order === 'desc') {
|
||||
results = reverse(results as any);
|
||||
}
|
||||
break;
|
||||
|
||||
case SongListSort.NAME:
|
||||
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.PLAY_COUNT:
|
||||
results = orderBy(results, ['playCount'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.RANDOM:
|
||||
results = shuffle(results);
|
||||
break;
|
||||
|
||||
case SongListSort.RATING:
|
||||
results = orderBy(results, ['userRating', (v) => v.name.toLowerCase()], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.RECENTLY_ADDED:
|
||||
results = orderBy(results, ['createdAt'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.RECENTLY_PLAYED:
|
||||
results = orderBy(results, ['lastPlayedAt'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.RELEASE_DATE:
|
||||
results = orderBy(results, ['releaseDate'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.YEAR:
|
||||
results = orderBy(
|
||||
results,
|
||||
['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
|
||||
[order, 'asc', 'asc', 'asc'],
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export const sortSongsByFetchedOrder = (
|
||||
songs: Song[],
|
||||
fetchedIds: string[],
|
||||
itemType: LibraryItem,
|
||||
): Song[] => {
|
||||
// Group songs by the fetched ID they belong to
|
||||
const songsByFetchedId = new Map<string, Song[]>();
|
||||
|
||||
for (const song of songs) {
|
||||
let matchedId: string | undefined;
|
||||
|
||||
switch (itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
matchedId = fetchedIds.find((id) => song.albumId === id);
|
||||
break;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
matchedId = fetchedIds.find((id) =>
|
||||
song.albumArtists.some((artist) => artist.id === id),
|
||||
);
|
||||
break;
|
||||
case LibraryItem.ARTIST:
|
||||
matchedId = fetchedIds.find((id) =>
|
||||
song.artists.some((artist) => artist.id === id),
|
||||
);
|
||||
break;
|
||||
case LibraryItem.GENRE:
|
||||
matchedId = fetchedIds.find((id) => song.genres.some((genre) => genre.id === id));
|
||||
break;
|
||||
case LibraryItem.PLAYLIST:
|
||||
// For playlists, we might need to track which playlist each song came from
|
||||
// This is a simplified approach - you may need to adjust based on your data structure
|
||||
matchedId = fetchedIds.find((id) => song.playlistItemId === id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (matchedId) {
|
||||
if (!songsByFetchedId.has(matchedId)) {
|
||||
songsByFetchedId.set(matchedId, []);
|
||||
}
|
||||
songsByFetchedId.get(matchedId)!.push(song);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort each group by discNumber and trackNumber
|
||||
for (const [fetchedId, groupSongs] of songsByFetchedId.entries()) {
|
||||
const sortedGroup = orderBy(groupSongs, ['discNumber', 'trackNumber'], ['asc', 'asc']);
|
||||
songsByFetchedId.set(fetchedId, sortedGroup);
|
||||
}
|
||||
|
||||
// Combine groups in the order of fetchedIds
|
||||
const result: Song[] = [];
|
||||
for (const fetchedId of fetchedIds) {
|
||||
const groupSongs = songsByFetchedId.get(fetchedId);
|
||||
if (groupSongs) {
|
||||
result.push(...groupSongs);
|
||||
}
|
||||
}
|
||||
|
||||
// Add any songs that didn't match any fetched ID at the end
|
||||
const matchedIds = new Set(result.map((s) => s.id));
|
||||
const unmatchedSongs = songs.filter((s) => !matchedIds.has(s.id));
|
||||
if (unmatchedSongs.length > 0) {
|
||||
const sortedUnmatched = orderBy(
|
||||
unmatchedSongs,
|
||||
['discNumber', 'trackNumber'],
|
||||
['asc', 'asc'],
|
||||
);
|
||||
result.push(...sortedUnmatched);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const sortAlbumArtistList = (
|
||||
artists: AlbumArtist[],
|
||||
sortBy: AlbumArtistListSort | ArtistListSort,
|
||||
sortOrder: SortOrder,
|
||||
) => {
|
||||
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
|
||||
|
||||
let results = artists;
|
||||
|
||||
switch (sortBy) {
|
||||
case AlbumArtistListSort.ALBUM_COUNT:
|
||||
results = orderBy(artists, ['albumCount', (v) => v.name.toLowerCase()], [order, 'asc']);
|
||||
break;
|
||||
|
||||
case AlbumArtistListSort.FAVORITED:
|
||||
results = orderBy(artists, ['starred'], [order]);
|
||||
break;
|
||||
|
||||
case AlbumArtistListSort.NAME:
|
||||
results = orderBy(artists, [(v) => v.name.toLowerCase()], [order]);
|
||||
break;
|
||||
|
||||
case AlbumArtistListSort.RATING:
|
||||
results = orderBy(artists, ['userRating'], [order]);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder: SortOrder) => {
|
||||
let results = albums;
|
||||
|
||||
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
|
||||
|
||||
switch (sortBy) {
|
||||
case AlbumListSort.ALBUM_ARTIST:
|
||||
results = orderBy(
|
||||
results,
|
||||
['albumArtist', (v) => v.name.toLowerCase()],
|
||||
[order, 'asc'],
|
||||
);
|
||||
break;
|
||||
case AlbumListSort.DURATION:
|
||||
results = orderBy(results, ['duration'], [order]);
|
||||
break;
|
||||
case AlbumListSort.FAVORITED:
|
||||
results = orderBy(results, ['starred'], [order]);
|
||||
break;
|
||||
case AlbumListSort.NAME:
|
||||
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
|
||||
break;
|
||||
case AlbumListSort.PLAY_COUNT:
|
||||
results = orderBy(results, ['playCount'], [order]);
|
||||
break;
|
||||
case AlbumListSort.RANDOM:
|
||||
results = shuffle(results);
|
||||
break;
|
||||
case AlbumListSort.RATING:
|
||||
results = orderBy(results, ['userRating'], [order]);
|
||||
break;
|
||||
case AlbumListSort.RECENTLY_ADDED:
|
||||
results = orderBy(results, ['createdAt'], [order]);
|
||||
break;
|
||||
case AlbumListSort.RECENTLY_PLAYED:
|
||||
results = orderBy(results, ['lastPlayedAt'], [order]);
|
||||
break;
|
||||
case AlbumListSort.SONG_COUNT:
|
||||
results = orderBy(results, ['songCount'], [order]);
|
||||
break;
|
||||
case AlbumListSort.YEAR:
|
||||
results = orderBy(results, ['releaseYear'], [order]);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import reverse from 'lodash/reverse';
|
||||
import shuffle from 'lodash/shuffle';
|
||||
|
||||
import {
|
||||
JFAlbumArtistListSort,
|
||||
JFAlbumListSort,
|
||||
@@ -1434,273 +1430,3 @@ type BaseEndpointArgsWithServer = {
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
};
|
||||
|
||||
export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder: SortOrder) => {
|
||||
let results = albums;
|
||||
|
||||
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
|
||||
|
||||
switch (sortBy) {
|
||||
case AlbumListSort.ALBUM_ARTIST:
|
||||
results = orderBy(
|
||||
results,
|
||||
['albumArtist', (v) => v.name.toLowerCase()],
|
||||
[order, 'asc'],
|
||||
);
|
||||
break;
|
||||
case AlbumListSort.DURATION:
|
||||
results = orderBy(results, ['duration'], [order]);
|
||||
break;
|
||||
case AlbumListSort.FAVORITED:
|
||||
results = orderBy(results, ['starred'], [order]);
|
||||
break;
|
||||
case AlbumListSort.NAME:
|
||||
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
|
||||
break;
|
||||
case AlbumListSort.PLAY_COUNT:
|
||||
results = orderBy(results, ['playCount'], [order]);
|
||||
break;
|
||||
case AlbumListSort.RANDOM:
|
||||
results = shuffle(results);
|
||||
break;
|
||||
case AlbumListSort.RATING:
|
||||
results = orderBy(results, ['userRating'], [order]);
|
||||
break;
|
||||
case AlbumListSort.RECENTLY_ADDED:
|
||||
results = orderBy(results, ['createdAt'], [order]);
|
||||
break;
|
||||
case AlbumListSort.RECENTLY_PLAYED:
|
||||
results = orderBy(results, ['lastPlayedAt'], [order]);
|
||||
break;
|
||||
case AlbumListSort.SONG_COUNT:
|
||||
results = orderBy(results, ['songCount'], [order]);
|
||||
break;
|
||||
case AlbumListSort.YEAR:
|
||||
results = orderBy(results, ['releaseYear'], [order]);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: SortOrder) => {
|
||||
let results = songs;
|
||||
|
||||
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
|
||||
|
||||
switch (sortBy) {
|
||||
case SongListSort.ALBUM:
|
||||
results = orderBy(
|
||||
results,
|
||||
[(v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, 'asc', 'asc'],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.ALBUM_ARTIST:
|
||||
results = orderBy(
|
||||
results,
|
||||
[(v) => v.albumArtists[0]?.name.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, order, 'asc', 'asc'],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.ARTIST:
|
||||
results = orderBy(
|
||||
results,
|
||||
[(v) => v.artistName?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||
[order, order, 'asc', 'asc'],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.BPM:
|
||||
results = orderBy(results, ['bpm'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.CHANNELS:
|
||||
results = orderBy(results, ['channels'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.COMMENT:
|
||||
results = orderBy(results, ['comment'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.DURATION:
|
||||
results = orderBy(results, ['duration'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.FAVORITED:
|
||||
results = orderBy(results, ['userFavorite', (v) => v.name.toLowerCase()], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.GENRE:
|
||||
results = orderBy(
|
||||
results,
|
||||
[
|
||||
(v) => v.genres?.[0]?.name.toLowerCase(),
|
||||
(v) => v.album?.toLowerCase(),
|
||||
'discNumber',
|
||||
'trackNumber',
|
||||
],
|
||||
[order, order, 'asc', 'asc'],
|
||||
);
|
||||
break;
|
||||
|
||||
case SongListSort.ID:
|
||||
if (order === 'desc') {
|
||||
results = reverse(results as any);
|
||||
}
|
||||
break;
|
||||
|
||||
case SongListSort.NAME:
|
||||
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.PLAY_COUNT:
|
||||
results = orderBy(results, ['playCount'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.RANDOM:
|
||||
results = shuffle(results);
|
||||
break;
|
||||
|
||||
case SongListSort.RATING:
|
||||
results = orderBy(results, ['userRating', (v) => v.name.toLowerCase()], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.RECENTLY_ADDED:
|
||||
results = orderBy(results, ['createdAt'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.RECENTLY_PLAYED:
|
||||
results = orderBy(results, ['lastPlayedAt'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.RELEASE_DATE:
|
||||
results = orderBy(results, ['releaseDate'], [order]);
|
||||
break;
|
||||
|
||||
case SongListSort.YEAR:
|
||||
results = orderBy(
|
||||
results,
|
||||
['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
|
||||
[order, 'asc', 'asc', 'asc'],
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
export const sortSongsByFetchedOrder = (
|
||||
songs: Song[],
|
||||
fetchedIds: string[],
|
||||
itemType: LibraryItem,
|
||||
): Song[] => {
|
||||
// Group songs by the fetched ID they belong to
|
||||
const songsByFetchedId = new Map<string, Song[]>();
|
||||
|
||||
for (const song of songs) {
|
||||
let matchedId: string | undefined;
|
||||
|
||||
switch (itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
matchedId = fetchedIds.find((id) => song.albumId === id);
|
||||
break;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
matchedId = fetchedIds.find((id) =>
|
||||
song.albumArtists.some((artist) => artist.id === id),
|
||||
);
|
||||
break;
|
||||
case LibraryItem.ARTIST:
|
||||
matchedId = fetchedIds.find((id) =>
|
||||
song.artists.some((artist) => artist.id === id),
|
||||
);
|
||||
break;
|
||||
case LibraryItem.GENRE:
|
||||
matchedId = fetchedIds.find((id) => song.genres.some((genre) => genre.id === id));
|
||||
break;
|
||||
case LibraryItem.PLAYLIST:
|
||||
// For playlists, we might need to track which playlist each song came from
|
||||
// This is a simplified approach - you may need to adjust based on your data structure
|
||||
matchedId = fetchedIds.find((id) => song.playlistItemId === id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (matchedId) {
|
||||
if (!songsByFetchedId.has(matchedId)) {
|
||||
songsByFetchedId.set(matchedId, []);
|
||||
}
|
||||
songsByFetchedId.get(matchedId)!.push(song);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort each group by discNumber and trackNumber
|
||||
for (const [fetchedId, groupSongs] of songsByFetchedId.entries()) {
|
||||
const sortedGroup = orderBy(groupSongs, ['discNumber', 'trackNumber'], ['asc', 'asc']);
|
||||
songsByFetchedId.set(fetchedId, sortedGroup);
|
||||
}
|
||||
|
||||
// Combine groups in the order of fetchedIds
|
||||
const result: Song[] = [];
|
||||
for (const fetchedId of fetchedIds) {
|
||||
const groupSongs = songsByFetchedId.get(fetchedId);
|
||||
if (groupSongs) {
|
||||
result.push(...groupSongs);
|
||||
}
|
||||
}
|
||||
|
||||
// Add any songs that didn't match any fetched ID at the end
|
||||
const matchedIds = new Set(result.map((s) => s.id));
|
||||
const unmatchedSongs = songs.filter((s) => !matchedIds.has(s.id));
|
||||
if (unmatchedSongs.length > 0) {
|
||||
const sortedUnmatched = orderBy(
|
||||
unmatchedSongs,
|
||||
['discNumber', 'trackNumber'],
|
||||
['asc', 'asc'],
|
||||
);
|
||||
result.push(...sortedUnmatched);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const sortAlbumArtistList = (
|
||||
artists: AlbumArtist[],
|
||||
sortBy: AlbumArtistListSort | ArtistListSort,
|
||||
sortOrder: SortOrder,
|
||||
) => {
|
||||
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
|
||||
|
||||
let results = artists;
|
||||
|
||||
switch (sortBy) {
|
||||
case AlbumArtistListSort.ALBUM_COUNT:
|
||||
results = orderBy(artists, ['albumCount', (v) => v.name.toLowerCase()], [order, 'asc']);
|
||||
break;
|
||||
|
||||
case AlbumArtistListSort.FAVORITED:
|
||||
results = orderBy(artists, ['starred'], [order]);
|
||||
break;
|
||||
|
||||
case AlbumArtistListSort.NAME:
|
||||
results = orderBy(artists, [(v) => v.name.toLowerCase()], [order]);
|
||||
break;
|
||||
|
||||
case AlbumArtistListSort.RATING:
|
||||
results = orderBy(artists, ['userRating'], [order]);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ export const DragTargetMap = {
|
||||
[LibraryItem.ARTIST]: DragTarget.ARTIST,
|
||||
[LibraryItem.GENRE]: DragTarget.GENRE,
|
||||
[LibraryItem.PLAYLIST]: DragTarget.PLAYLIST,
|
||||
[LibraryItem.PLAYLIST_SONG]: DragTarget.SONG,
|
||||
[LibraryItem.QUEUE_SONG]: DragTarget.QUEUE_SONG,
|
||||
[LibraryItem.SONG]: DragTarget.SONG,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user