mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +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 {
|
import {
|
||||||
InternetProviderLyricSearchResponse,
|
InternetProviderLyricSearchResponse,
|
||||||
@@ -11,7 +11,7 @@ export const orderSearchResults = (args: {
|
|||||||
}) => {
|
}) => {
|
||||||
const { params, results } = args;
|
const { params, results } = args;
|
||||||
|
|
||||||
const options: Fuse.IFuseOptions<InternetProviderLyricSearchResponse> = {
|
const options: IFuseOptions<InternetProviderLyricSearchResponse> = {
|
||||||
fieldNormWeight: 1,
|
fieldNormWeight: 1,
|
||||||
includeScore: true,
|
includeScore: true,
|
||||||
keys: [
|
keys: [
|
||||||
|
|||||||
@@ -301,7 +301,10 @@ export const queryKeys: Record<
|
|||||||
},
|
},
|
||||||
root: (serverId: string) => [serverId, 'playlists'] as const,
|
root: (serverId: string) => [serverId, 'playlists'] as const,
|
||||||
songList: (serverId: string, id?: string) => {
|
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;
|
return [serverId, 'playlists', 'songList'] as const;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
ssType,
|
ssType,
|
||||||
SubsonicExtensions,
|
SubsonicExtensions,
|
||||||
} from '/@/shared/api/subsonic/subsonic-types';
|
} from '/@/shared/api/subsonic/subsonic-types';
|
||||||
|
import { sortAlbumArtistList, sortAlbumList, sortSongList } from '/@/shared/api/utils';
|
||||||
import {
|
import {
|
||||||
AlbumListSort,
|
AlbumListSort,
|
||||||
GenreListSort,
|
GenreListSort,
|
||||||
@@ -21,10 +22,7 @@ import {
|
|||||||
LibraryItem,
|
LibraryItem,
|
||||||
PlaylistListSort,
|
PlaylistListSort,
|
||||||
Song,
|
Song,
|
||||||
sortAlbumArtistList,
|
|
||||||
sortAlbumList,
|
|
||||||
SortOrder,
|
SortOrder,
|
||||||
sortSongList,
|
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ServerFeatures } from '/@/shared/types/features-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 { AddToQueueType, usePlayerActions } from '/@/renderer/store';
|
||||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||||
|
import { sortSongsByFetchedOrder } from '/@/shared/api/utils';
|
||||||
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
||||||
import { ConfirmModal } from '/@/shared/components/modal/modal';
|
import { ConfirmModal } from '/@/shared/components/modal/modal';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
@@ -30,7 +31,6 @@ import {
|
|||||||
SongListResponse,
|
SongListResponse,
|
||||||
SongListSort,
|
SongListSort,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
sortSongsByFetchedOrder,
|
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { Play, PlayerRepeat, PlayerShuffle } from '/@/shared/types/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 { api } from '/@/renderer/api';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { sortSongList } from '/@/shared/api/utils';
|
||||||
import {
|
import {
|
||||||
PlaylistSongListQuery,
|
PlaylistSongListQuery,
|
||||||
PlaylistSongListQueryClientSide,
|
PlaylistSongListQueryClientSide,
|
||||||
@@ -11,7 +12,6 @@ import {
|
|||||||
SongListResponse,
|
SongListResponse,
|
||||||
SongListSort,
|
SongListSort,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
sortSongList,
|
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export const getPlaylistSongsById = async (args: {
|
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 { closeAllModals, openModal } from '@mantine/modals';
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import debounce from 'lodash/debounce';
|
import { useCallback } from 'react';
|
||||||
import { ChangeEvent, MouseEvent, useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, useParams } from 'react-router';
|
import { useNavigate, useParams } from 'react-router';
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
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 { 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 { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
|
||||||
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
|
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||||
import { OrderToggleButton } from '/@/renderer/features/shared/components/order-toggle-button';
|
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||||
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
|
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||||
|
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import {
|
import { useCurrentServerId } from '/@/renderer/store';
|
||||||
useCurrentServer,
|
|
||||||
usePlaylistDetailStore,
|
|
||||||
useSetPlaylistDetailFilters,
|
|
||||||
useSetPlaylistStore,
|
|
||||||
} from '/@/renderer/store';
|
|
||||||
import { Button } from '/@/shared/components/button/button';
|
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
|
||||||
import { ConfirmModal } from '/@/shared/components/modal/modal';
|
import { ConfirmModal } from '/@/shared/components/modal/modal';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { ServerType, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
import { LibraryItem, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||||
import { ListDisplayType, Play } from '/@/shared/types/types';
|
import { ItemListKey, Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
const FILTERS = {
|
export const PlaylistDetailSongListHeaderFilters = () => {
|
||||||
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) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
const navigate = useNavigate();
|
const serverId = useCurrentServerId();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const server = useCurrentServer();
|
const navigate = useNavigate();
|
||||||
const setPage = useSetPlaylistStore();
|
|
||||||
const setFilter = useSetPlaylistDetailFilters();
|
const detailQuery = useQuery(playlistsQueries.detail({ query: { id: playlistId }, serverId }));
|
||||||
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 detailQuery = useQuery(
|
|
||||||
playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
|
|
||||||
);
|
|
||||||
const isSmartPlaylist = detailQuery.data?.rules;
|
const isSmartPlaylist = detailQuery.data?.rules;
|
||||||
|
|
||||||
const { ref, ...cq } = useContainerQuery();
|
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 deletePlaylistMutation = useDeletePlaylist({});
|
||||||
|
|
||||||
const handleDeletePlaylist = useCallback(() => {
|
const handleDeletePlaylist = useCallback(() => {
|
||||||
@@ -332,99 +75,100 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||||||
return (
|
return (
|
||||||
<Flex justify="space-between">
|
<Flex justify="space-between">
|
||||||
<Group gap="sm" ref={ref} w="100%">
|
<Group gap="sm" ref={ref} w="100%">
|
||||||
<DropdownMenu position="bottom-start">
|
<ListSortByDropdown
|
||||||
<DropdownMenu.Target>
|
defaultSortByValue={SongListSort.ID}
|
||||||
<Button
|
itemType={LibraryItem.PLAYLIST_SONG}
|
||||||
tooltip={{
|
listKey={ItemListKey.PLAYLIST_SONG}
|
||||||
label: t('page.playlist.reorder', { postProcess: 'sentenceCase' }),
|
/>
|
||||||
}}
|
<Divider orientation="vertical" />
|
||||||
variant="subtle"
|
<ListSortOrderToggleButton
|
||||||
>
|
defaultSortOrder={SortOrder.ASC}
|
||||||
{sortByLabel}
|
listKey={ItemListKey.PLAYLIST_SONG}
|
||||||
</Button>
|
/>
|
||||||
</DropdownMenu.Target>
|
<ListRefreshButton listKey={ItemListKey.PLAYLIST_SONG} />
|
||||||
<DropdownMenu.Dropdown>
|
</Group>
|
||||||
{FILTERS[server?.type as keyof typeof FILTERS].map((filter) => (
|
<Group gap="sm" wrap="nowrap">
|
||||||
<DropdownMenu.Item
|
<ListConfigMenu
|
||||||
isSelected={filter.value === sortBy}
|
listKey={ItemListKey.PLAYLIST_SONG}
|
||||||
key={`filter-${filter.name}`}
|
tableColumnsData={PLAYLIST_SONG_TABLE_COLUMNS}
|
||||||
onClick={handleSetSortBy}
|
|
||||||
value={filter.value}
|
|
||||||
>
|
|
||||||
{filter.name}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
))}
|
|
||||||
</DropdownMenu.Dropdown>
|
|
||||||
</DropdownMenu>
|
|
||||||
|
|
||||||
<Divider orientation="vertical" />
|
|
||||||
<OrderToggleButton
|
|
||||||
onToggle={handleToggleSortOrder}
|
|
||||||
sortOrder={sortOrder || SortOrder.ASC}
|
|
||||||
/>
|
/>
|
||||||
<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></Group>
|
|
||||||
</Flex>
|
</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 { useQuery } from '@tanstack/react-query';
|
||||||
import { MutableRefObject } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
|
||||||
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
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 { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||||
import { PlaylistDetailSongListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header-filters';
|
import { PlaylistDetailSongListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header-filters';
|
||||||
import { FilterBar } from '/@/renderer/features/shared/components/filter-bar';
|
import { FilterBar } from '/@/renderer/features/shared/components/filter-bar';
|
||||||
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-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 { useCurrentServer } from '/@/renderer/store';
|
||||||
import { formatDurationString } from '/@/renderer/utils';
|
import { formatDurationString } from '/@/renderer/utils';
|
||||||
import { Badge } from '/@/shared/components/badge/badge';
|
import { Badge } from '/@/shared/components/badge/badge';
|
||||||
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
interface PlaylistDetailHeaderProps {
|
export const PlaylistDetailSongListHeader = () => {
|
||||||
handlePlay: (playType: Play) => void;
|
|
||||||
handleToggleShowQueryBuilder: () => void;
|
|
||||||
itemCount?: number;
|
|
||||||
tableRef: MutableRefObject<ItemListHandle | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PlaylistDetailSongListHeader = ({
|
|
||||||
handlePlay,
|
|
||||||
handleToggleShowQueryBuilder,
|
|
||||||
itemCount,
|
|
||||||
tableRef,
|
|
||||||
}: PlaylistDetailHeaderProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
|
const { itemCount } = useListContext();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const detailQuery = useQuery(
|
const detailQuery = useQuery(
|
||||||
playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
|
playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
|
||||||
@@ -60,13 +48,10 @@ export const PlaylistDetailSongListHeader = ({
|
|||||||
</Badge>
|
</Badge>
|
||||||
{isSmartPlaylist && <Badge size="lg">{t('entity.smartPlaylist')}</Badge>}
|
{isSmartPlaylist && <Badge size="lg">{t('entity.smartPlaylist')}</Badge>}
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
|
<ListSearchInput />
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<PlaylistDetailSongListHeaderFilters
|
<PlaylistDetailSongListHeaderFilters />
|
||||||
handlePlay={handlePlay}
|
|
||||||
handleToggleShowQueryBuilder={handleToggleShowQueryBuilder}
|
|
||||||
tableRef={tableRef}
|
|
||||||
/>
|
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
</Stack>
|
</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 { closeAllModals, openModal } from '@mantine/modals';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
import { useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { generatePath, useNavigate, useParams } from 'react-router';
|
import { generatePath, useNavigate, useParams } from 'react-router';
|
||||||
|
|
||||||
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
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 { 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 { PlaylistDetailSongListHeader } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header';
|
||||||
import { PlaylistQueryBuilder } from '/@/renderer/features/playlists/components/playlist-query-builder';
|
import { PlaylistQueryBuilder } from '/@/renderer/features/playlists/components/playlist-query-builder';
|
||||||
import { SaveAsPlaylistForm } from '/@/renderer/features/playlists/components/save-as-playlist-form';
|
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 { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
|
||||||
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useCurrentServer, usePlaylistDetailStore } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
import { Box } from '/@/shared/components/box/box';
|
import { Box } from '/@/shared/components/box/box';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { ServerType, SongListSort, SortOrder, sortSongList } from '/@/shared/types/domain-types';
|
import { ServerType, SongListSort } from '/@/shared/types/domain-types';
|
||||||
import { Play } from '/@/shared/types/types';
|
import { ItemListKey, Play } from '/@/shared/types/types';
|
||||||
|
|
||||||
const PlaylistDetailSongListRoute = () => {
|
const PlaylistDetailSongListRoute = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -145,8 +147,6 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
setIsQueryBuilderExpanded(true);
|
setIsQueryBuilderExpanded(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const page = usePlaylistDetailStore();
|
|
||||||
|
|
||||||
const playlistSongs = useQuery(
|
const playlistSongs = useQuery(
|
||||||
playlistsQueries.songList({
|
playlistsQueries.songList({
|
||||||
query: {
|
query: {
|
||||||
@@ -156,38 +156,38 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterSortedSongs = useMemo(() => {
|
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
|
||||||
const items = playlistSongs.data?.items;
|
|
||||||
|
|
||||||
if (items) {
|
const handlePlay = (_play: Play) => {
|
||||||
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) => {
|
|
||||||
// handlePlayQueueAdd?.({
|
// handlePlayQueueAdd?.({
|
||||||
// byData: filterSortedSongs,
|
// byData: filterSortedSongs,
|
||||||
// playType: play,
|
// 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 (
|
return (
|
||||||
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
||||||
|
<ListContext.Provider value={providerValue}>
|
||||||
<LibraryContainer>
|
<LibraryContainer>
|
||||||
<PlaylistDetailSongListHeader
|
<PlaylistDetailSongListHeader
|
||||||
handlePlay={handlePlay}
|
handlePlay={handlePlay}
|
||||||
@@ -221,15 +221,18 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
onSaveAs={handleSaveAs}
|
onSaveAs={handleSaveAs}
|
||||||
playlistId={playlistId}
|
playlistId={playlistId}
|
||||||
query={detailQuery?.data?.rules}
|
query={detailQuery?.data?.rules}
|
||||||
sortBy={detailQuery?.data?.rules?.sort || SongListSort.ALBUM}
|
sortBy={
|
||||||
|
detailQuery?.data?.rules?.sort || SongListSort.ALBUM
|
||||||
|
}
|
||||||
sortOrder={detailQuery?.data?.rules?.order || 'asc'}
|
sortOrder={detailQuery?.data?.rules?.order || 'asc'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
{/* <PlaylistDetailSongListContent songs={filterSortedSongs} tableRef={tableRef} /> */}
|
<PlaylistDetailSongListContent />
|
||||||
</LibraryContainer>
|
</LibraryContainer>
|
||||||
|
</ListContext.Provider>
|
||||||
</AnimatedPage>
|
</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<
|
const ALBUM_LIST_FILTERS: Partial<
|
||||||
Record<ServerType, Array<{ defaultOrder: SortOrder; name: string; value: string }>>
|
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<
|
const ALBUM_ARTIST_LIST_FILTERS: Partial<
|
||||||
Record<ServerType, Array<{ defaultOrder: SortOrder; name: string; value: string }>>
|
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.ARTIST]: ARTIST_LIST_FILTERS,
|
||||||
[LibraryItem.GENRE]: GENRE_LIST_FILTERS,
|
[LibraryItem.GENRE]: GENRE_LIST_FILTERS,
|
||||||
[LibraryItem.PLAYLIST]: PLAYLIST_LIST_FILTERS,
|
[LibraryItem.PLAYLIST]: PLAYLIST_LIST_FILTERS,
|
||||||
|
[LibraryItem.PLAYLIST_SONG]: PLAYLIST_SONG_LIST_FILTERS,
|
||||||
[LibraryItem.SONG]: SONG_LIST_FILTERS,
|
[LibraryItem.SONG]: SONG_LIST_FILTERS,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { parseAsString, useQueryState } from 'nuqs';
|
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 { useListFilterPersistence } from '/@/renderer/features/shared/hooks/use-list-filter-persistence';
|
||||||
|
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
import { SortOrder } from '/@/shared/types/domain-types';
|
import { SortOrder } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ArtistListResponse,
|
ArtistListResponse,
|
||||||
FavoriteArgs,
|
FavoriteArgs,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
|
PlaylistSongListResponse,
|
||||||
Song,
|
Song,
|
||||||
SongDetailResponse,
|
SongDetailResponse,
|
||||||
} from '/@/shared/types/domain-types';
|
} 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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return previousQueries;
|
return previousQueries;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const restoreFavoriteQueryData = (
|
export const restoreFavoriteQueryData = (
|
||||||
queryClient: QueryClient,
|
queryClient: QueryClient,
|
||||||
previousQueries: PreviousQueryData[],
|
previousQueries: PreviousQueryData[],
|
||||||
|
|||||||
@@ -192,13 +192,6 @@ export const createFuseForLibraryItem = <T extends FuseSearchableItem>(
|
|||||||
},
|
},
|
||||||
name: 'albumArtists',
|
name: 'albumArtists',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
getFn: (item) => {
|
|
||||||
const s = item as QueueSong | Song;
|
|
||||||
return s.genres?.map((genre) => genre.name).join(' ') || '';
|
|
||||||
},
|
|
||||||
name: 'genres',
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+284
-1
@@ -1,10 +1,24 @@
|
|||||||
import { AxiosHeaders } from 'axios';
|
import { AxiosHeaders } from 'axios';
|
||||||
import isElectron from 'is-electron';
|
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 semverCoerce from 'semver/functions/coerce';
|
||||||
import semverGte from 'semver/functions/gte';
|
import semverGte from 'semver/functions/gte';
|
||||||
import { z } from 'zod';
|
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';
|
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
|
// 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 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 {
|
import {
|
||||||
JFAlbumArtistListSort,
|
JFAlbumArtistListSort,
|
||||||
JFAlbumListSort,
|
JFAlbumListSort,
|
||||||
@@ -1434,273 +1430,3 @@ type BaseEndpointArgsWithServer = {
|
|||||||
signal?: AbortSignal;
|
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.ARTIST]: DragTarget.ARTIST,
|
||||||
[LibraryItem.GENRE]: DragTarget.GENRE,
|
[LibraryItem.GENRE]: DragTarget.GENRE,
|
||||||
[LibraryItem.PLAYLIST]: DragTarget.PLAYLIST,
|
[LibraryItem.PLAYLIST]: DragTarget.PLAYLIST,
|
||||||
|
[LibraryItem.PLAYLIST_SONG]: DragTarget.SONG,
|
||||||
[LibraryItem.QUEUE_SONG]: DragTarget.QUEUE_SONG,
|
[LibraryItem.QUEUE_SONG]: DragTarget.QUEUE_SONG,
|
||||||
[LibraryItem.SONG]: DragTarget.SONG,
|
[LibraryItem.SONG]: DragTarget.SONG,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user