mirror of
https://github.com/antebudimir/feishin.git
synced 2025-12-31 18:13:31 +00:00
add artist list
This commit is contained in:
parent
14e9f6ac41
commit
e84a4b20bc
22 changed files with 1369 additions and 192 deletions
|
|
@ -99,6 +99,12 @@ export const controller: GeneralController = {
|
|||
getAlbumListCount(args) {
|
||||
return apiController('getAlbumListCount', args.apiClientProps.server?.type)?.(args);
|
||||
},
|
||||
getArtistList(args) {
|
||||
return apiController('getArtistList', args.apiClientProps.server?.type)?.(args);
|
||||
},
|
||||
getArtistListCount(args) {
|
||||
return apiController('getArtistListCount', args.apiClientProps.server?.type)?.(args);
|
||||
},
|
||||
getDownloadUrl(args) {
|
||||
return apiController('getDownloadUrl', args.apiClientProps.server?.type)?.(args);
|
||||
},
|
||||
|
|
@ -126,6 +132,9 @@ export const controller: GeneralController = {
|
|||
getRandomSongList(args) {
|
||||
return apiController('getRandomSongList', args.apiClientProps.server?.type)?.(args);
|
||||
},
|
||||
getRoles(args) {
|
||||
return apiController('getRoles', args.apiClientProps.server?.type)?.(args);
|
||||
},
|
||||
getServerInfo(args) {
|
||||
return apiController('getServerInfo', args.apiClientProps.server?.type)?.(args);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -152,7 +152,6 @@ export const JellyfinController: ControllerEndpoint = {
|
|||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await jfApiClient(apiClientProps).deletePlaylist({
|
||||
body: null,
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
|
|
@ -331,6 +330,41 @@ export const JellyfinController: ControllerEndpoint = {
|
|||
apiClientProps,
|
||||
query: { ...query, limit: 1, startIndex: 0 },
|
||||
}).then((result) => result!.totalRecordCount!),
|
||||
getArtistList: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await jfApiClient(apiClientProps).getArtistList({
|
||||
query: {
|
||||
Fields: 'Genres, DateCreated, ExternalUrls, Overview',
|
||||
ImageTypeLimit: 1,
|
||||
Limit: query.limit,
|
||||
ParentId: query.musicFolderId,
|
||||
Recursive: true,
|
||||
SearchTerm: query.searchTerm,
|
||||
SortBy: albumArtistListSortMap.jellyfin[query.sortBy] || 'SortName,Name',
|
||||
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
||||
StartIndex: query.startIndex,
|
||||
UserId: apiClientProps.server?.userId || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get album artist list');
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.Items.map((item) =>
|
||||
jfNormalize.albumArtist(item, apiClientProps.server),
|
||||
),
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: res.body.TotalRecordCount,
|
||||
};
|
||||
},
|
||||
getArtistListCount: async ({ apiClientProps, query }) =>
|
||||
JellyfinController.getArtistList({
|
||||
apiClientProps,
|
||||
query: { ...query, limit: 1, startIndex: 0 },
|
||||
}).then((result) => result!.totalRecordCount!),
|
||||
getDownloadUrl: (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
|
|
@ -559,6 +593,7 @@ export const JellyfinController: ControllerEndpoint = {
|
|||
totalRecordCount: res.body.Items.length || 0,
|
||||
};
|
||||
},
|
||||
getRoles: async () => [],
|
||||
getServerInfo: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
|
|
@ -775,7 +810,6 @@ export const JellyfinController: ControllerEndpoint = {
|
|||
const { apiClientProps, query } = args;
|
||||
|
||||
const res = await jfApiClient(apiClientProps).movePlaylistItem({
|
||||
body: null,
|
||||
params: {
|
||||
itemId: query.trackId,
|
||||
newIdx: query.endingIndex.toString(),
|
||||
|
|
@ -794,7 +828,6 @@ export const JellyfinController: ControllerEndpoint = {
|
|||
|
||||
for (const chunk of chunks) {
|
||||
const res = await jfApiClient(apiClientProps).removeFromPlaylist({
|
||||
body: null,
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -30,6 +30,22 @@ const VERSION_INFO: VersionInfo = [
|
|||
['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
|
||||
];
|
||||
|
||||
const NAVIDROME_ROLES: Array<string | { label: string; value: string }> = [
|
||||
{ label: 'all artists', value: '' },
|
||||
'arranger',
|
||||
'artist',
|
||||
'composer',
|
||||
'conductor',
|
||||
'director',
|
||||
'djmixer',
|
||||
'engineer',
|
||||
'lyricist',
|
||||
'mixer',
|
||||
'performer',
|
||||
'producer',
|
||||
'remixer',
|
||||
];
|
||||
|
||||
const excludeMissing = (server: ServerListItem | null) => {
|
||||
if (hasFeature(server, ServerFeature.BFR)) {
|
||||
return { missing: false };
|
||||
|
|
@ -105,7 +121,6 @@ export const NavidromeController: ControllerEndpoint = {
|
|||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await ndApiClient(apiClientProps).deletePlaylist({
|
||||
body: null,
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
|
|
@ -261,6 +276,47 @@ export const NavidromeController: ControllerEndpoint = {
|
|||
apiClientProps,
|
||||
query: { ...query, limit: 1, startIndex: 0 },
|
||||
}).then((result) => result!.totalRecordCount!),
|
||||
getArtistList: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
|
||||
query: {
|
||||
_end: query.startIndex + (query.limit || 0),
|
||||
_order: sortOrderMap.navidrome[query.sortOrder],
|
||||
_sort: albumArtistListSortMap.navidrome[query.sortBy],
|
||||
_start: query.startIndex,
|
||||
name: query.searchTerm,
|
||||
...query._custom?.navidrome,
|
||||
role: query.role,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get artist list');
|
||||
}
|
||||
|
||||
return {
|
||||
items: res.body.data.map((albumArtist) =>
|
||||
// Navidrome native API will return only external URL small/medium/large
|
||||
// image URL. Set large image to undefined to force `albumArtist` to use
|
||||
// /rest/getCoverArt.view?id=ar-...
|
||||
ndNormalize.albumArtist(
|
||||
{
|
||||
...albumArtist,
|
||||
largeImageUrl: undefined,
|
||||
},
|
||||
apiClientProps.server,
|
||||
),
|
||||
),
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: Number(res.body.headers.get('x-total-count') || 0),
|
||||
};
|
||||
},
|
||||
getArtistListCount: async ({ apiClientProps, query }) =>
|
||||
NavidromeController.getArtistList({
|
||||
apiClientProps,
|
||||
query: { ...query, limit: 1, startIndex: 0 },
|
||||
}).then((result) => result!.totalRecordCount!),
|
||||
getDownloadUrl: SubsonicController.getDownloadUrl,
|
||||
getGenreList: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
|
@ -369,6 +425,7 @@ export const NavidromeController: ControllerEndpoint = {
|
|||
};
|
||||
},
|
||||
getRandomSongList: SubsonicController.getRandomSongList,
|
||||
getRoles: async () => NAVIDROME_ROLES,
|
||||
getServerInfo: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
|
|
@ -564,7 +621,6 @@ export const NavidromeController: ControllerEndpoint = {
|
|||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await ndApiClient(apiClientProps).removeFromPlaylist({
|
||||
body: null,
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -231,6 +231,9 @@ export const queryKeys: Record<
|
|||
return [serverId, 'playlists', 'songList'] as const;
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
list: (serverId: string) => [serverId, 'roles'] as const,
|
||||
},
|
||||
search: {
|
||||
list: (serverId: string, query?: SearchQuery) => {
|
||||
if (query) return [serverId, 'search', 'list', query] as const;
|
||||
|
|
|
|||
|
|
@ -515,6 +515,51 @@ export const SubsonicController: ControllerEndpoint = {
|
|||
|
||||
return totalRecordCount;
|
||||
},
|
||||
getArtistList: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getArtists({
|
||||
query: {
|
||||
musicFolderId: query.musicFolderId,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get artist list');
|
||||
}
|
||||
|
||||
let artists = (res.body.artists?.index || []).flatMap((index) => index.artist);
|
||||
console.log(artists.length);
|
||||
if (query.role) {
|
||||
artists = artists.filter(
|
||||
(artist) => !artist.roles || artist.roles.includes(query.role!),
|
||||
);
|
||||
}
|
||||
|
||||
let results = artists.map((artist) =>
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||
);
|
||||
|
||||
if (query.searchTerm) {
|
||||
const searchResults = filter(results, (artist) => {
|
||||
return artist.name.toLowerCase().includes(query.searchTerm!.toLowerCase());
|
||||
});
|
||||
|
||||
results = searchResults;
|
||||
}
|
||||
|
||||
if (query.sortBy) {
|
||||
results = sortAlbumArtistList(results, query.sortBy, query.sortOrder);
|
||||
}
|
||||
|
||||
return {
|
||||
items: results,
|
||||
startIndex: query.startIndex,
|
||||
totalRecordCount: results?.length || 0,
|
||||
};
|
||||
},
|
||||
getArtistListCount: async (args) =>
|
||||
SubsonicController.getArtistList(args).then((res) => res!.totalRecordCount!),
|
||||
getDownloadUrl: (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
|
|
@ -711,6 +756,31 @@ export const SubsonicController: ControllerEndpoint = {
|
|||
totalRecordCount: res.body.randomSongs?.song?.length || 0,
|
||||
};
|
||||
},
|
||||
getRoles: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
const res = await ssApiClient(apiClientProps).getArtists({});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get artist list');
|
||||
}
|
||||
|
||||
const roles = new Set<string>();
|
||||
|
||||
for (const index of res.body.artists?.index || []) {
|
||||
for (const artist of index.artist) {
|
||||
for (const role of artist.roles || []) {
|
||||
roles.add(role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const final: Array<string | { label: string; value: string }> = Array.from(roles).sort();
|
||||
if (final.length > 0) {
|
||||
final.splice(0, 0, { label: 'all artists', value: '' });
|
||||
}
|
||||
return final;
|
||||
},
|
||||
getServerInfo: async (args) => {
|
||||
const { apiClientProps } = args;
|
||||
|
||||
|
|
@ -1230,6 +1300,7 @@ export const SubsonicController: ControllerEndpoint = {
|
|||
|
||||
return null;
|
||||
},
|
||||
|
||||
search: async (args) => {
|
||||
const { query, apiClientProps } = args;
|
||||
|
||||
|
|
@ -1296,109 +1367,3 @@ export const SubsonicController: ControllerEndpoint = {
|
|||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
// export const getAlbumArtistDetail = async (
|
||||
// args: AlbumArtistDetailArgs,
|
||||
// ): Promise<SSAlbumArtistDetail> => {
|
||||
// const { server, signal, query } = args;
|
||||
// const defaultParams = getDefaultParams(server);
|
||||
|
||||
// const searchParams: SSAlbumArtistDetailParams = {
|
||||
// id: query.id,
|
||||
// ...defaultParams,
|
||||
// };
|
||||
|
||||
// const data = await api
|
||||
// .get('/getArtist.view', {
|
||||
// prefixUrl: server?.url,
|
||||
// searchParams,
|
||||
// signal,
|
||||
// })
|
||||
// .json<SSAlbumArtistDetailResponse>();
|
||||
|
||||
// return data.artist;
|
||||
// };
|
||||
|
||||
// const getAlbumArtistList = async (args: AlbumArtistListArgs): Promise<SSAlbumArtistList> => {
|
||||
// const { signal, server, query } = args;
|
||||
// const defaultParams = getDefaultParams(server);
|
||||
|
||||
// const searchParams: SSAlbumArtistListParams = {
|
||||
// musicFolderId: query.musicFolderId,
|
||||
// ...defaultParams,
|
||||
// };
|
||||
|
||||
// const data = await api
|
||||
// .get('rest/getArtists.view', {
|
||||
// prefixUrl: server?.url,
|
||||
// searchParams,
|
||||
// signal,
|
||||
// })
|
||||
// .json<SSAlbumArtistListResponse>();
|
||||
|
||||
// const artists = (data.artists?.index || []).flatMap((index: SSArtistIndex) => index.artist);
|
||||
|
||||
// return {
|
||||
// items: artists,
|
||||
// startIndex: query.startIndex,
|
||||
// totalRecordCount: null,
|
||||
// };
|
||||
// };
|
||||
|
||||
// const getGenreList = async (args: GenreListArgs): Promise<SSGenreList> => {
|
||||
// const { server, signal } = args;
|
||||
// const defaultParams = getDefaultParams(server);
|
||||
|
||||
// const data = await api
|
||||
// .get('rest/getGenres.view', {
|
||||
// prefixUrl: server?.url,
|
||||
// searchParams: defaultParams,
|
||||
// signal,
|
||||
// })
|
||||
// .json<SSGenreListResponse>();
|
||||
|
||||
// return data.genres.genre;
|
||||
// };
|
||||
|
||||
// const getAlbumDetail = async (args: AlbumDetailArgs): Promise<SSAlbumDetail> => {
|
||||
// const { server, query, signal } = args;
|
||||
// const defaultParams = getDefaultParams(server);
|
||||
|
||||
// const searchParams = {
|
||||
// id: query.id,
|
||||
// ...defaultParams,
|
||||
// };
|
||||
|
||||
// const data = await api
|
||||
// .get('rest/getAlbum.view', {
|
||||
// prefixUrl: server?.url,
|
||||
// searchParams: parseSearchParams(searchParams),
|
||||
// signal,
|
||||
// })
|
||||
// .json<SSAlbumDetailResponse>();
|
||||
|
||||
// const { song: songs, ...dataWithoutSong } = data.album;
|
||||
// return { ...dataWithoutSong, songs };
|
||||
// };
|
||||
|
||||
// const getAlbumList = async (args: AlbumListArgs): Promise<SSAlbumList> => {
|
||||
// const { server, query, signal } = args;
|
||||
// const defaultParams = getDefaultParams(server);
|
||||
|
||||
// const searchParams = {
|
||||
// ...defaultParams,
|
||||
// };
|
||||
// const data = await api
|
||||
// .get('rest/getAlbumList2.view', {
|
||||
// prefixUrl: server?.url,
|
||||
// searchParams: parseSearchParams(searchParams),
|
||||
// signal,
|
||||
// })
|
||||
// .json<SSAlbumListResponse>();
|
||||
|
||||
// return {
|
||||
// items: data.albumList2.album,
|
||||
// startIndex: query.startIndex,
|
||||
// totalRecordCount: null,
|
||||
// };
|
||||
// };
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ const albumArtist = z.object({
|
|||
coverArt: z.string().optional(),
|
||||
id,
|
||||
name: z.string(),
|
||||
roles: z.array(z.string()).optional(),
|
||||
starred: z.string().optional(),
|
||||
});
|
||||
|
||||
|
|
@ -175,6 +176,7 @@ const artistListEntry = albumArtist.pick({
|
|||
coverArt: true,
|
||||
id: true,
|
||||
name: true,
|
||||
roles: true,
|
||||
starred: true,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -672,7 +672,7 @@ export type AlbumArtistDetailQuery = { id: string };
|
|||
export type AlbumArtistDetailArgs = { query: AlbumArtistDetailQuery } & BaseEndpointArgs;
|
||||
|
||||
// Artist List
|
||||
export type ArtistListResponse = BasePaginatedResponse<Artist[]> | null | undefined;
|
||||
export type ArtistListResponse = BasePaginatedResponse<AlbumArtist[]> | null | undefined;
|
||||
|
||||
export enum ArtistListSort {
|
||||
ALBUM = 'album',
|
||||
|
|
@ -695,6 +695,8 @@ export interface ArtistListQuery extends BaseQuery<ArtistListSort> {
|
|||
};
|
||||
limit?: number;
|
||||
musicFolderId?: string;
|
||||
role?: string;
|
||||
searchTerm?: string;
|
||||
startIndex: number;
|
||||
}
|
||||
|
||||
|
|
@ -1245,7 +1247,8 @@ export type ControllerEndpoint = {
|
|||
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
|
||||
getAlbumListCount: (args: AlbumListArgs) => Promise<number>;
|
||||
// getArtistInfo?: (args: any) => void;
|
||||
// getArtistList?: (args: ArtistListArgs) => Promise<ArtistListResponse>;
|
||||
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
|
||||
getArtistListCount: (args: ArtistListArgs) => Promise<number>;
|
||||
getDownloadUrl: (args: DownloadArgs) => string;
|
||||
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
|
||||
getLyrics?: (args: LyricsArgs) => Promise<LyricsResponse>;
|
||||
|
|
@ -1255,6 +1258,7 @@ export type ControllerEndpoint = {
|
|||
getPlaylistListCount: (args: PlaylistListArgs) => Promise<number>;
|
||||
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
|
||||
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
|
||||
getRoles: (args: BaseEndpointArgs) => Promise<Array<string | { label: string; value: string }>>;
|
||||
getServerInfo: (args: ServerInfoArgs) => Promise<ServerInfo>;
|
||||
getSimilarSongs: (args: SimilarSongsArgs) => Promise<Song[]>;
|
||||
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
|
||||
|
|
@ -1417,7 +1421,7 @@ export const sortSongList = (songs: QueueSong[], sortBy: SongListSort, sortOrder
|
|||
|
||||
export const sortAlbumArtistList = (
|
||||
artists: AlbumArtist[],
|
||||
sortBy: AlbumArtistListSort,
|
||||
sortBy: AlbumArtistListSort | ArtistListSort,
|
||||
sortOrder: SortOrder,
|
||||
) => {
|
||||
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
|
||||
|
|
|
|||
|
|
@ -105,44 +105,42 @@ export const useVirtualTable = <TFilter extends BaseQuery<any>>({
|
|||
const queryKeyFn:
|
||||
| ((serverId: string, query: Record<any, any>, pagination: QueryPagination) => QueryKey)
|
||||
| null = useMemo(() => {
|
||||
if (itemType === LibraryItem.ALBUM) {
|
||||
return queryKeys.albums.list;
|
||||
switch (itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
return queryKeys.albums.list;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
return queryKeys.albumArtists.list;
|
||||
case LibraryItem.ARTIST:
|
||||
return queryKeys.artists.list;
|
||||
case LibraryItem.GENRE:
|
||||
return queryKeys.genres.list;
|
||||
case LibraryItem.PLAYLIST:
|
||||
return queryKeys.playlists.list;
|
||||
case LibraryItem.SONG:
|
||||
return queryKeys.songs.list;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
if (itemType === LibraryItem.ALBUM_ARTIST) {
|
||||
return queryKeys.albumArtists.list;
|
||||
}
|
||||
if (itemType === LibraryItem.PLAYLIST) {
|
||||
return queryKeys.playlists.list;
|
||||
}
|
||||
if (itemType === LibraryItem.SONG) {
|
||||
return queryKeys.songs.list;
|
||||
}
|
||||
if (itemType === LibraryItem.GENRE) {
|
||||
return queryKeys.genres.list;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [itemType]);
|
||||
|
||||
const queryFn: ((args: any) => Promise<BasePaginatedResponse<any> | null | undefined>) | null =
|
||||
useMemo(() => {
|
||||
if (itemType === LibraryItem.ALBUM) {
|
||||
return api.controller.getAlbumList;
|
||||
switch (itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
return api.controller.getAlbumList;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
return api.controller.getAlbumArtistList;
|
||||
case LibraryItem.ARTIST:
|
||||
return api.controller.getArtistList;
|
||||
case LibraryItem.GENRE:
|
||||
return api.controller.getGenreList;
|
||||
case LibraryItem.PLAYLIST:
|
||||
return api.controller.getPlaylistList;
|
||||
case LibraryItem.SONG:
|
||||
return api.controller.getSongList;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
if (itemType === LibraryItem.ALBUM_ARTIST) {
|
||||
return api.controller.getAlbumArtistList;
|
||||
}
|
||||
if (itemType === LibraryItem.PLAYLIST) {
|
||||
return api.controller.getPlaylistList;
|
||||
}
|
||||
if (itemType === LibraryItem.SONG) {
|
||||
return api.controller.getSongList;
|
||||
}
|
||||
if (itemType === LibraryItem.GENRE) {
|
||||
return api.controller.getGenreList;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [itemType]);
|
||||
|
||||
const onGridReady = useCallback(
|
||||
|
|
@ -390,7 +388,9 @@ export const useVirtualTable = <TFilter extends BaseQuery<any>>({
|
|||
break;
|
||||
case LibraryItem.ARTIST:
|
||||
navigate(
|
||||
generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, { artistId: e.data.id }),
|
||||
generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: e.data.id,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case LibraryItem.PLAYLIST:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { lazy, MutableRefObject, Suspense } from 'react';
|
||||
import { Spinner } from '/@/renderer/components';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { ListDisplayType } from '/@/renderer/types';
|
||||
import { useListStoreByKey } from '../../../store/list.store';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
|
||||
const ArtistListGridView = lazy(() =>
|
||||
import('/@/renderer/features/artists/components/artist-list-grid-view').then((module) => ({
|
||||
default: module.ArtistListGridView,
|
||||
})),
|
||||
);
|
||||
|
||||
const ArtistListTableView = lazy(() =>
|
||||
import('/@/renderer/features/artists/components/artist-list-table-view').then((module) => ({
|
||||
default: module.ArtistListTableView,
|
||||
})),
|
||||
);
|
||||
|
||||
interface ArtistListContentProps {
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
itemCount?: number;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const ArtistListContent = ({ itemCount, gridRef, tableRef }: ArtistListContentProps) => {
|
||||
const { pageKey } = useListContext();
|
||||
const { display } = useListStoreByKey({ key: pageKey });
|
||||
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER;
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
{isGrid ? (
|
||||
<ArtistListGridView
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
/>
|
||||
) : (
|
||||
<ArtistListTableView
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
||||
import { MutableRefObject, useCallback, useMemo } from 'react';
|
||||
import AutoSizer, { Size } from 'react-virtualized-auto-sizer';
|
||||
import { ListOnScrollProps } from 'react-window';
|
||||
import { VirtualGridAutoSizerContainer } from '../../../components/virtual-grid/virtual-grid-wrapper';
|
||||
import { useListStoreByKey } from '../../../store/list.store';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import {
|
||||
AlbumArtist,
|
||||
ArtistListQuery,
|
||||
ArtistListResponse,
|
||||
ArtistListSort,
|
||||
LibraryItem,
|
||||
} from '/@/renderer/api/types';
|
||||
import { ALBUMARTIST_CARD_ROWS } from '/@/renderer/components';
|
||||
import { VirtualInfiniteGrid, VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useCurrentServer, useListStoreActions } from '/@/renderer/store';
|
||||
import { CardRow, ListDisplayType } from '/@/renderer/types';
|
||||
import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite';
|
||||
|
||||
interface ArtistListGridViewProps {
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
itemCount?: number;
|
||||
}
|
||||
|
||||
export const ArtistListGridView = ({ itemCount, gridRef }: ArtistListGridViewProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const server = useCurrentServer();
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
|
||||
const { pageKey } = useListContext();
|
||||
const { grid, display, filter } = useListStoreByKey<ArtistListQuery>({ key: pageKey });
|
||||
const { setGrid } = useListStoreActions();
|
||||
const handleFavorite = useHandleFavorite({ gridRef, server });
|
||||
|
||||
const fetchInitialData = useCallback(() => {
|
||||
const query: Omit<ArtistListQuery, 'startIndex' | 'limit'> = {
|
||||
...filter,
|
||||
};
|
||||
|
||||
const queriesFromCache: [QueryKey, ArtistListResponse][] = queryClient.getQueriesData({
|
||||
exact: false,
|
||||
fetchStatus: 'idle',
|
||||
queryKey: queryKeys.artists.list(server?.id || '', query),
|
||||
stale: false,
|
||||
});
|
||||
|
||||
const itemData = [];
|
||||
|
||||
for (const [, data] of queriesFromCache) {
|
||||
const { items, startIndex } = data || {};
|
||||
|
||||
if (items && items.length !== 1 && startIndex !== undefined) {
|
||||
let itemIndex = 0;
|
||||
for (
|
||||
let rowIndex = startIndex;
|
||||
rowIndex < startIndex + items.length;
|
||||
rowIndex += 1
|
||||
) {
|
||||
itemData[rowIndex] = items[itemIndex];
|
||||
itemIndex += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return itemData;
|
||||
}, [filter, queryClient, server?.id]);
|
||||
|
||||
const fetch = useCallback(
|
||||
async ({ skip: startIndex, take: limit }: { skip: number; take: number }) => {
|
||||
const query: ArtistListQuery = {
|
||||
...filter,
|
||||
limit,
|
||||
startIndex,
|
||||
};
|
||||
|
||||
const queryKey = queryKeys.artists.list(server?.id || '', query);
|
||||
|
||||
const artistsRes = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
return artistsRes;
|
||||
},
|
||||
[filter, queryClient, server],
|
||||
);
|
||||
|
||||
const handleGridScroll = useCallback(
|
||||
(e: ListOnScrollProps) => {
|
||||
setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey });
|
||||
},
|
||||
[pageKey, setGrid],
|
||||
);
|
||||
|
||||
const cardRows = useMemo(() => {
|
||||
const rows: CardRow<AlbumArtist>[] = [ALBUMARTIST_CARD_ROWS.name];
|
||||
|
||||
switch (filter.sortBy) {
|
||||
case ArtistListSort.DURATION:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.duration);
|
||||
break;
|
||||
case ArtistListSort.FAVORITED:
|
||||
break;
|
||||
case ArtistListSort.NAME:
|
||||
break;
|
||||
case ArtistListSort.ALBUM_COUNT:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.albumCount);
|
||||
break;
|
||||
case ArtistListSort.PLAY_COUNT:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.playCount);
|
||||
break;
|
||||
case ArtistListSort.RANDOM:
|
||||
break;
|
||||
case ArtistListSort.RATING:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.rating);
|
||||
break;
|
||||
case ArtistListSort.RECENTLY_ADDED:
|
||||
break;
|
||||
case ArtistListSort.SONG_COUNT:
|
||||
rows.push(ALBUMARTIST_CARD_ROWS.songCount);
|
||||
break;
|
||||
case ArtistListSort.RELEASE_DATE:
|
||||
break;
|
||||
}
|
||||
|
||||
return rows;
|
||||
}, [filter.sortBy]);
|
||||
|
||||
return (
|
||||
<VirtualGridAutoSizerContainer>
|
||||
<AutoSizer>
|
||||
{({ height, width }: Size) => (
|
||||
<VirtualInfiniteGrid
|
||||
ref={gridRef}
|
||||
cardRows={cardRows}
|
||||
display={display || ListDisplayType.CARD}
|
||||
fetchFn={fetch}
|
||||
fetchInitialData={fetchInitialData}
|
||||
handleFavorite={handleFavorite}
|
||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||
height={height}
|
||||
initialScrollOffset={grid?.scrollOffset || 0}
|
||||
itemCount={itemCount || 0}
|
||||
itemGap={grid?.itemGap ?? 10}
|
||||
itemSize={grid?.itemSize || 200}
|
||||
itemType={LibraryItem.ALBUM_ARTIST}
|
||||
loading={itemCount === undefined || itemCount === null}
|
||||
minimumBatchSize={40}
|
||||
route={{
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
}}
|
||||
width={width}
|
||||
onScroll={handleGridScroll}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</VirtualGridAutoSizerContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,619 @@
|
|||
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react';
|
||||
import { IDatasource } from '@ag-grid-community/core';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { Divider, Flex, Group, Stack } from '@mantine/core';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiFolder2Line, RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react-icons/ri';
|
||||
import { useListContext } from '../../../context/list-context';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import {
|
||||
ArtistListQuery,
|
||||
ArtistListSort,
|
||||
LibraryItem,
|
||||
ServerType,
|
||||
SortOrder,
|
||||
} from '/@/renderer/api/types';
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
MultiSelect,
|
||||
Select,
|
||||
Slider,
|
||||
Switch,
|
||||
Text,
|
||||
} from '/@/renderer/components';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import {
|
||||
ArtistListFilter,
|
||||
useCurrentServer,
|
||||
useListStoreActions,
|
||||
useListStoreByKey,
|
||||
} from '/@/renderer/store';
|
||||
import { ListDisplayType, TableColumn } from '/@/renderer/types';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { useRoles } from '/@/renderer/features/artists/queries/roles-query';
|
||||
|
||||
const FILTERS = {
|
||||
jellyfin: [
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.album', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.ALBUM,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.DURATION,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.NAME,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.RANDOM,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.RECENTLY_ADDED,
|
||||
},
|
||||
],
|
||||
navidrome: [
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.ALBUM_COUNT,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.FAVORITED,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.PLAY_COUNT,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.NAME,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.RATING,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.SONG_COUNT,
|
||||
},
|
||||
],
|
||||
subsonic: [
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.ALBUM_COUNT,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.FAVORITED,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.ASC,
|
||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.NAME,
|
||||
},
|
||||
{
|
||||
defaultOrder: SortOrder.DESC,
|
||||
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
|
||||
value: ArtistListSort.RATING,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
interface ArtistListHeaderFiltersProps {
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderFiltersProps) => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const server = useCurrentServer();
|
||||
const { pageKey } = useListContext();
|
||||
const { display, table, grid, filter } = useListStoreByKey<ArtistListQuery>({
|
||||
key: pageKey,
|
||||
});
|
||||
const { setFilter, setTable, setTablePagination, setDisplayType, setGrid } =
|
||||
useListStoreActions();
|
||||
const cq = useContainerQuery();
|
||||
const roles = useRoles({
|
||||
options: {
|
||||
cacheTime: 1000 * 60 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 60 * 2,
|
||||
},
|
||||
query: {},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER;
|
||||
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id });
|
||||
|
||||
const sortByLabel =
|
||||
(server?.type &&
|
||||
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filter.sortBy)
|
||||
?.name) ||
|
||||
t('common.unknown', { postProcess: 'titleCase' });
|
||||
|
||||
const handleItemSize = (e: number) => {
|
||||
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
||||
setTable({ data: { rowHeight: e }, key: pageKey });
|
||||
} else {
|
||||
setGrid({ data: { itemSize: e }, key: pageKey });
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemGap = (e: number) => {
|
||||
setGrid({ data: { itemGap: e }, key: pageKey });
|
||||
};
|
||||
|
||||
const debouncedHandleItemSize = debounce(handleItemSize, 20);
|
||||
|
||||
const fetch = useCallback(
|
||||
async (startIndex: number, limit: number, filters: ArtistListFilter) => {
|
||||
const queryKey = queryKeys.artists.list(server?.id || '', {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
});
|
||||
|
||||
const albums = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
return albums;
|
||||
},
|
||||
[queryClient, server],
|
||||
);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
async (filters: ArtistListFilter) => {
|
||||
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
||||
const dataSource: IDatasource = {
|
||||
getRows: async (params) => {
|
||||
const limit = params.endRow - params.startRow;
|
||||
const startIndex = params.startRow;
|
||||
|
||||
const queryKey = queryKeys.artists.list(server?.id || '', {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
});
|
||||
|
||||
const artistsRes = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
params.successCallback(
|
||||
artistsRes?.items || [],
|
||||
artistsRes?.totalRecordCount || 0,
|
||||
);
|
||||
},
|
||||
rowCount: undefined,
|
||||
};
|
||||
tableRef.current?.api.setDatasource(dataSource);
|
||||
tableRef.current?.api.purgeInfiniteCache();
|
||||
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||
|
||||
if (display === ListDisplayType.TABLE_PAGINATED) {
|
||||
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
|
||||
}
|
||||
} else {
|
||||
gridRef.current?.scrollTo(0);
|
||||
gridRef.current?.resetLoadMoreItemsCache();
|
||||
|
||||
// Refetching within the virtualized grid may be inconsistent due to it refetching
|
||||
// using an outdated set of filters. To avoid this, we fetch using the updated filters
|
||||
// and then set the grid's data here.
|
||||
const data = await fetch(0, 200, filters);
|
||||
|
||||
if (!data?.items) return;
|
||||
gridRef.current?.setItemData(data.items);
|
||||
}
|
||||
},
|
||||
[display, tableRef, server, queryClient, setTablePagination, pageKey, gridRef, fetch],
|
||||
);
|
||||
|
||||
const handleSetSortBy = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value || !server?.type) return;
|
||||
|
||||
const sortOrder = FILTERS[server.type as keyof typeof FILTERS].find(
|
||||
(f) => f.value === e.currentTarget.value,
|
||||
)?.defaultOrder;
|
||||
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
sortBy: e.currentTarget.value as ArtistListSort,
|
||||
sortOrder: sortOrder || SortOrder.ASC,
|
||||
},
|
||||
itemType: LibraryItem.ARTIST,
|
||||
key: pageKey,
|
||||
}) as ArtistListFilter;
|
||||
|
||||
handleFilterChange(updatedFilters);
|
||||
},
|
||||
[handleFilterChange, pageKey, server?.type, setFilter],
|
||||
);
|
||||
|
||||
const handleSetMusicFolder = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value) return;
|
||||
|
||||
let updatedFilters = null;
|
||||
if (e.currentTarget.value === String(filter.musicFolderId)) {
|
||||
updatedFilters = setFilter({
|
||||
data: { musicFolderId: undefined },
|
||||
itemType: LibraryItem.ARTIST,
|
||||
key: pageKey,
|
||||
}) as ArtistListFilter;
|
||||
} else {
|
||||
updatedFilters = setFilter({
|
||||
data: { musicFolderId: e.currentTarget.value },
|
||||
itemType: LibraryItem.ARTIST,
|
||||
key: pageKey,
|
||||
}) as ArtistListFilter;
|
||||
}
|
||||
|
||||
handleFilterChange(updatedFilters);
|
||||
},
|
||||
[filter.musicFolderId, handleFilterChange, setFilter, pageKey],
|
||||
);
|
||||
|
||||
const handleToggleSortOrder = useCallback(() => {
|
||||
const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
||||
const updatedFilters = setFilter({
|
||||
data: { sortOrder: newSortOrder },
|
||||
itemType: LibraryItem.ARTIST,
|
||||
key: pageKey,
|
||||
}) as ArtistListFilter;
|
||||
handleFilterChange(updatedFilters);
|
||||
}, [filter.sortOrder, handleFilterChange, pageKey, setFilter]);
|
||||
|
||||
const handleSetViewType = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value) return;
|
||||
|
||||
setDisplayType({ data: e.currentTarget.value as ListDisplayType, key: pageKey });
|
||||
},
|
||||
[pageKey, setDisplayType],
|
||||
);
|
||||
|
||||
const handleTableColumns = (values: TableColumn[]) => {
|
||||
const existingColumns = table.columns;
|
||||
|
||||
if (values.length === 0) {
|
||||
return setTable({
|
||||
data: {
|
||||
columns: [],
|
||||
},
|
||||
key: pageKey,
|
||||
});
|
||||
}
|
||||
|
||||
// If adding a column
|
||||
if (values.length > existingColumns.length) {
|
||||
const newColumn = { column: values[values.length - 1], width: 100 };
|
||||
|
||||
setTable({ data: { columns: [...existingColumns, newColumn] }, key: pageKey });
|
||||
} else {
|
||||
// If removing a column
|
||||
const removed = existingColumns.filter((column) => !values.includes(column.column));
|
||||
const newColumns = existingColumns.filter((column) => !removed.includes(column));
|
||||
|
||||
setTable({ data: { columns: newColumns }, key: pageKey });
|
||||
}
|
||||
|
||||
return tableRef.current?.api.sizeColumnsToFit();
|
||||
};
|
||||
|
||||
const handleAutoFitColumns = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setTable({ data: { autoFit: e.currentTarget.checked }, key: pageKey });
|
||||
|
||||
if (e.currentTarget.checked) {
|
||||
tableRef.current?.api.sizeColumnsToFit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
queryClient.invalidateQueries(queryKeys.artists.list(server?.id || ''));
|
||||
handleFilterChange(filter);
|
||||
}, [filter, handleFilterChange, queryClient, server?.id]);
|
||||
|
||||
const handleSetRole = useCallback(
|
||||
(e: string | null) => {
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
role: e || '',
|
||||
},
|
||||
itemType: LibraryItem.ARTIST,
|
||||
key: pageKey,
|
||||
}) as ArtistListFilter;
|
||||
handleFilterChange(updatedFilters);
|
||||
},
|
||||
[handleFilterChange, pageKey, setFilter],
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
<Group
|
||||
ref={cq.ref}
|
||||
spacing="sm"
|
||||
w="100%"
|
||||
>
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
{sortByLabel}
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{FILTERS[server?.type as keyof typeof FILTERS].map((f) => (
|
||||
<DropdownMenu.Item
|
||||
key={`filter-${f.name}`}
|
||||
$isActive={f.value === filter.sortBy}
|
||||
value={f.value}
|
||||
onClick={handleSetSortBy}
|
||||
>
|
||||
{f.name}
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
<Divider orientation="vertical" />
|
||||
<OrderToggleButton
|
||||
sortOrder={filter.sortOrder}
|
||||
onToggle={handleToggleSortOrder}
|
||||
/>
|
||||
{server?.type === ServerType.JELLYFIN && (
|
||||
<>
|
||||
<Divider orientation="vertical" />
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
{cq.isMd ? 'Folder' : <RiFolder2Line size={15} />}
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{musicFoldersQuery.data?.items.map((folder) => (
|
||||
<DropdownMenu.Item
|
||||
key={`musicFolder-${folder.id}`}
|
||||
$isActive={filter.musicFolderId === folder.id}
|
||||
value={folder.id}
|
||||
onClick={handleSetMusicFolder}
|
||||
>
|
||||
{folder.name}
|
||||
</DropdownMenu.Item>
|
||||
))}
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
)}
|
||||
{roles.data?.length && (
|
||||
<>
|
||||
<Divider orientation="vertical" />
|
||||
<Select
|
||||
data={roles.data}
|
||||
value={filter.role}
|
||||
onChange={handleSetRole}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Divider orientation="vertical" />
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
tooltip={{ label: t('common.refresh', { postProcess: 'titleCase' }) }}
|
||||
variant="subtle"
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
<RiRefreshLine size="1.3rem" />
|
||||
</Button>
|
||||
<Divider orientation="vertical" />
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
<RiMoreFill size={15} />
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiRefreshLine />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
{t('common.refresh', {
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
<Group>
|
||||
<DropdownMenu
|
||||
position="bottom-end"
|
||||
width={425}
|
||||
>
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
<RiSettings3Fill size="1.3rem" />
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.general.displayType', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.CARD}
|
||||
value={ListDisplayType.CARD}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
{t('table.config.view.card', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.POSTER}
|
||||
value={ListDisplayType.POSTER}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
{t('table.config.view.poster', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.TABLE}
|
||||
value={ListDisplayType.TABLE}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
{t('table.config.view.table', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.general.itemSize', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
{display === ListDisplayType.CARD ||
|
||||
display === ListDisplayType.POSTER ? (
|
||||
<Slider
|
||||
defaultValue={grid?.itemSize}
|
||||
max={300}
|
||||
min={150}
|
||||
onChange={debouncedHandleItemSize}
|
||||
/>
|
||||
) : (
|
||||
<Slider
|
||||
defaultValue={table.rowHeight}
|
||||
max={100}
|
||||
min={30}
|
||||
onChange={debouncedHandleItemSize}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenu.Item>
|
||||
{isGrid && (
|
||||
<>
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.general.itemGap', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
<Slider
|
||||
defaultValue={grid?.itemGap || 0}
|
||||
max={30}
|
||||
min={0}
|
||||
onChangeEnd={handleItemGap}
|
||||
/>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
{!isGrid && (
|
||||
<>
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.general.tableColumns', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
closeMenuOnClick={false}
|
||||
component="div"
|
||||
sx={{ cursor: 'default' }}
|
||||
>
|
||||
<Stack>
|
||||
<MultiSelect
|
||||
clearable
|
||||
data={ALBUMARTIST_TABLE_COLUMNS}
|
||||
defaultValue={table?.columns.map(
|
||||
(column) => column.column,
|
||||
)}
|
||||
width={300}
|
||||
onChange={handleTableColumns}
|
||||
/>
|
||||
<Group position="apart">
|
||||
<Text>
|
||||
{t('table.config.general.autoFitColumns', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Text>
|
||||
<Switch
|
||||
defaultChecked={table.autoFit}
|
||||
onChange={handleAutoFitColumns}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import type { ChangeEvent, MutableRefObject } from 'react';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { Flex, Group, Stack } from '@mantine/core';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FilterBar } from '../../shared/components/filter-bar';
|
||||
import { ArtistListQuery, LibraryItem } from '/@/renderer/api/types';
|
||||
import { PageHeader, SearchInput } from '/@/renderer/components';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { ArtistListFilter, useCurrentServer } from '/@/renderer/store';
|
||||
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
|
||||
import { ArtistListHeaderFilters } from '/@/renderer/features/artists/components/artist-list-header-filters';
|
||||
|
||||
interface ArtistListHeaderProps {
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
itemCount?: number;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const ArtistListHeader = ({ itemCount, gridRef, tableRef }: ArtistListHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const server = useCurrentServer();
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const { filter, refresh, search } = useDisplayRefresh<ArtistListQuery>({
|
||||
gridRef,
|
||||
itemCount,
|
||||
itemType: LibraryItem.ARTIST,
|
||||
server,
|
||||
tableRef,
|
||||
});
|
||||
|
||||
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedFilters = search(e) as ArtistListFilter;
|
||||
refresh(updatedFilters);
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
ref={cq.ref}
|
||||
spacing={0}
|
||||
>
|
||||
<PageHeader backgroundColor="var(--titlebar-bg)">
|
||||
<Flex
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
>
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.Title>
|
||||
{t('entity.artist_other', { postProcess: 'titleCase' })}
|
||||
</LibraryHeaderBar.Title>
|
||||
<LibraryHeaderBar.Badge
|
||||
isLoading={itemCount === null || itemCount === undefined}
|
||||
>
|
||||
{itemCount}
|
||||
</LibraryHeaderBar.Badge>
|
||||
</LibraryHeaderBar>
|
||||
<Group>
|
||||
<SearchInput
|
||||
defaultValue={filter.searchTerm}
|
||||
openedWidth={cq.isMd ? 250 : cq.isSm ? 200 : 150}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
</Group>
|
||||
</Flex>
|
||||
</PageHeader>
|
||||
<FilterBar>
|
||||
<ArtistListHeaderFilters
|
||||
gridRef={gridRef}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</FilterBar>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { MutableRefObject } from 'react';
|
||||
import { useListContext } from '../../../context/list-context';
|
||||
import { ARTIST_CONTEXT_MENU_ITEMS } from '../../context-menu/context-menu-items';
|
||||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
||||
import { VirtualTable } from '/@/renderer/components/virtual-table';
|
||||
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
|
||||
interface ArtistListTableViewProps {
|
||||
itemCount?: number;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const ArtistListTableView = ({ itemCount, tableRef }: ArtistListTableViewProps) => {
|
||||
const server = useCurrentServer();
|
||||
const { pageKey } = useListContext();
|
||||
|
||||
const tableProps = useVirtualTable({
|
||||
contextMenu: ARTIST_CONTEXT_MENU_ITEMS,
|
||||
itemCount,
|
||||
itemType: LibraryItem.ARTIST,
|
||||
pageKey,
|
||||
server,
|
||||
tableRef,
|
||||
});
|
||||
|
||||
return (
|
||||
<VirtualGridAutoSizerContainer>
|
||||
<VirtualTable
|
||||
// https://github.com/ag-grid/ag-grid/issues/5284
|
||||
// Key is used to force remount of table when display, rowHeight, or server changes
|
||||
key={`table-${tableProps.rowHeight}-${server?.id}`}
|
||||
ref={tableRef}
|
||||
{...tableProps}
|
||||
/>
|
||||
</VirtualGridAutoSizerContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { ArtistListQuery } from '/@/renderer/api/types';
|
||||
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { getServerById } from '/@/renderer/store';
|
||||
|
||||
export const useArtistListCount = (args: QueryHookArgs<ArtistListQuery>) => {
|
||||
const { options, query, serverId } = args;
|
||||
const server = getServerById(serverId);
|
||||
|
||||
return useQuery({
|
||||
enabled: !!serverId,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!server) throw new Error('Server not found');
|
||||
return api.controller.getArtistListCount({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query,
|
||||
});
|
||||
},
|
||||
queryKey: queryKeys.albumArtists.count(
|
||||
serverId || '',
|
||||
Object.keys(query).length === 0 ? undefined : query,
|
||||
),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
25
src/renderer/features/artists/queries/roles-query.ts
Normal file
25
src/renderer/features/artists/queries/roles-query.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { getServerById } from '/@/renderer/store';
|
||||
|
||||
export const useRoles = (args: QueryHookArgs<{}>) => {
|
||||
const { options, serverId } = args;
|
||||
const server = getServerById(serverId);
|
||||
|
||||
return useQuery({
|
||||
enabled: !!serverId,
|
||||
queryFn: ({ signal }) => {
|
||||
if (!server) throw new Error('Server not found');
|
||||
return api.controller.getRoles({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
});
|
||||
},
|
||||
queryKey: queryKeys.roles.list(serverId || ''),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
57
src/renderer/features/artists/routes/artist-list-route.tsx
Normal file
57
src/renderer/features/artists/routes/artist-list-route.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useCurrentServer } from '../../../store/auth.store';
|
||||
import { useListFilterByKey } from '../../../store/list.store';
|
||||
import { ArtistListQuery, LibraryItem } from '/@/renderer/api/types';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { ListContext } from '/@/renderer/context/list-context';
|
||||
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||
import { useArtistListCount } from '/@/renderer/features/artists/queries/artist-list-count-query';
|
||||
import { ArtistListHeader } from '/@/renderer/features/artists/components/artist-list-header';
|
||||
import { ArtistListContent } from '/@/renderer/features/artists/components/artist-list-content';
|
||||
|
||||
const ArtistListRoute = () => {
|
||||
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
const pageKey = LibraryItem.ARTIST;
|
||||
const server = useCurrentServer();
|
||||
|
||||
const artistListFilter = useListFilterByKey<ArtistListQuery>({ key: pageKey });
|
||||
|
||||
const itemCountCheck = useArtistListCount({
|
||||
options: {
|
||||
cacheTime: 1000 * 60,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
query: artistListFilter,
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
|
||||
|
||||
const providerValue = useMemo(() => {
|
||||
return {
|
||||
id: undefined,
|
||||
pageKey,
|
||||
};
|
||||
}, [pageKey]);
|
||||
|
||||
return (
|
||||
<AnimatedPage>
|
||||
<ListContext.Provider value={providerValue}>
|
||||
<ArtistListHeader
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
<ArtistListContent
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</ListContext.Provider>
|
||||
</AnimatedPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArtistListRoute;
|
||||
|
|
@ -3,8 +3,8 @@ import { useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store';
|
|||
|
||||
const SIDEBAR_ITEMS: Array<[string, string]> = [
|
||||
['Albums', 'page.sidebar.albums'],
|
||||
['Artists', 'page.sidebar.artists'],
|
||||
['Folders', 'page.sidebar.folders'],
|
||||
['Artists', 'page.sidebar.albumArtists'],
|
||||
['Artists-all', 'page.sidebar.artists'],
|
||||
['Genres', 'page.sidebar.genres'],
|
||||
['Home', 'page.sidebar.home'],
|
||||
['Now Playing', 'page.sidebar.nowPlaying'],
|
||||
|
|
|
|||
|
|
@ -81,8 +81,8 @@ export const Sidebar = () => {
|
|||
const translatedSidebarItemMap = useMemo(
|
||||
() => ({
|
||||
Albums: t('page.sidebar.albums', { postProcess: 'titleCase' }),
|
||||
Artists: t('page.sidebar.artists', { postProcess: 'titleCase' }),
|
||||
Folders: t('page.sidebar.folders', { postProcess: 'titleCase' }),
|
||||
Artists: t('page.sidebar.albumArtists', { postProcess: 'titleCase' }),
|
||||
'Artists-all': t('page.sidebar.artists', { postProcess: 'titleCase' }),
|
||||
Genres: t('page.sidebar.genres', { postProcess: 'titleCase' }),
|
||||
Home: t('page.sidebar.home', { postProcess: 'titleCase' }),
|
||||
'Now Playing': t('page.sidebar.nowPlaying', { postProcess: 'titleCase' }),
|
||||
|
|
|
|||
|
|
@ -27,44 +27,42 @@ export const useListFilterRefresh = ({
|
|||
|
||||
const queryKeyFn: ((serverId: string, query: Record<any, any>) => QueryKey) | null =
|
||||
useMemo(() => {
|
||||
if (itemType === LibraryItem.ALBUM) {
|
||||
return queryKeys.albums.list;
|
||||
switch (itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
return queryKeys.albums.list;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
return queryKeys.albumArtists.list;
|
||||
case LibraryItem.ARTIST:
|
||||
return queryKeys.artists.list;
|
||||
case LibraryItem.GENRE:
|
||||
return queryKeys.genres.list;
|
||||
case LibraryItem.PLAYLIST:
|
||||
return queryKeys.playlists.list;
|
||||
case LibraryItem.SONG:
|
||||
return queryKeys.songs.list;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
if (itemType === LibraryItem.ALBUM_ARTIST) {
|
||||
return queryKeys.albumArtists.list;
|
||||
}
|
||||
if (itemType === LibraryItem.PLAYLIST) {
|
||||
return queryKeys.playlists.list;
|
||||
}
|
||||
if (itemType === LibraryItem.SONG) {
|
||||
return queryKeys.songs.list;
|
||||
}
|
||||
if (itemType === LibraryItem.GENRE) {
|
||||
return queryKeys.genres.list;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [itemType]);
|
||||
|
||||
const queryFn: ((args: any) => Promise<BasePaginatedResponse<any> | null | undefined>) | null =
|
||||
useMemo(() => {
|
||||
if (itemType === LibraryItem.ALBUM) {
|
||||
return api.controller.getAlbumList;
|
||||
switch (itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
return api.controller.getAlbumList;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
return api.controller.getAlbumArtistList;
|
||||
case LibraryItem.ARTIST:
|
||||
return api.controller.getArtistList;
|
||||
case LibraryItem.GENRE:
|
||||
return api.controller.getGenreList;
|
||||
case LibraryItem.PLAYLIST:
|
||||
return api.controller.getPlaylistList;
|
||||
case LibraryItem.SONG:
|
||||
return api.controller.getSongList;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
if (itemType === LibraryItem.ALBUM_ARTIST) {
|
||||
return api.controller.getAlbumArtistList;
|
||||
}
|
||||
if (itemType === LibraryItem.PLAYLIST) {
|
||||
return api.controller.getPlaylistList;
|
||||
}
|
||||
if (itemType === LibraryItem.SONG) {
|
||||
return api.controller.getSongList;
|
||||
}
|
||||
if (itemType === LibraryItem.GENRE) {
|
||||
return api.controller.getGenreList;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [itemType]);
|
||||
|
||||
const handleRefreshTable = useCallback(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { ModalsProvider } from '@mantine/modals';
|
|||
import { BaseContextModal } from '/@/renderer/components';
|
||||
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists';
|
||||
import { ShareItemContextModal } from '/@/renderer/features/sharing';
|
||||
import ArtistListRoute from '/@/renderer/features/artists/routes/artist-list-route';
|
||||
|
||||
const NowPlayingRoute = lazy(
|
||||
() => import('/@/renderer/features/now-playing/routes/now-playing-route'),
|
||||
|
|
@ -144,6 +145,11 @@ export const AppRouter = () => {
|
|||
errorElement={<RouteErrorBoundary />}
|
||||
path={AppRoute.LIBRARY_ALBUMS_DETAIL}
|
||||
/>
|
||||
<Route
|
||||
element={<ArtistListRoute />}
|
||||
errorElement={<RouteErrorBoundary />}
|
||||
path={AppRoute.LIBRARY_ARTISTS}
|
||||
/>
|
||||
<Route
|
||||
element={<DummyAlbumDetailRoute />}
|
||||
errorElement={<RouteErrorBoundary />}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
AlbumArtistListSort,
|
||||
AlbumListArgs,
|
||||
AlbumListSort,
|
||||
ArtistListArgs,
|
||||
GenreListArgs,
|
||||
GenreListSort,
|
||||
LibraryItem,
|
||||
|
|
@ -27,6 +28,7 @@ export const generatePageKey = (page: string, id?: string) => {
|
|||
export type AlbumListFilter = Omit<AlbumListArgs['query'], 'startIndex' | 'limit'>;
|
||||
export type SongListFilter = Omit<SongListArgs['query'], 'startIndex' | 'limit'>;
|
||||
export type AlbumArtistListFilter = Omit<AlbumArtistListArgs['query'], 'startIndex' | 'limit'>;
|
||||
export type ArtistListFilter = Omit<ArtistListArgs['query'], 'startIndex' | 'limit'>;
|
||||
export type PlaylistListFilter = Omit<PlaylistListArgs['query'], 'startIndex' | 'limit'>;
|
||||
export type GenreListFilter = Omit<GenreListArgs['query'], 'startIndex' | 'limit'>;
|
||||
|
||||
|
|
@ -34,6 +36,7 @@ type FilterType =
|
|||
| AlbumListFilter
|
||||
| SongListFilter
|
||||
| AlbumArtistListFilter
|
||||
| ArtistListFilter
|
||||
| PlaylistListFilter
|
||||
| GenreListFilter;
|
||||
|
||||
|
|
@ -509,6 +512,36 @@ export const useListStore = create<ListSlice>()(
|
|||
scrollOffset: 0,
|
||||
},
|
||||
},
|
||||
artist: {
|
||||
display: ListDisplayType.POSTER,
|
||||
filter: {
|
||||
role: '',
|
||||
sortBy: AlbumArtistListSort.NAME,
|
||||
sortOrder: SortOrder.DESC,
|
||||
},
|
||||
grid: { itemGap: 10, itemSize: 200, scrollOffset: 0 },
|
||||
table: {
|
||||
autoFit: true,
|
||||
columns: [
|
||||
{
|
||||
column: TableColumn.ROW_INDEX,
|
||||
width: 50,
|
||||
},
|
||||
{
|
||||
column: TableColumn.TITLE_COMBINED,
|
||||
width: 500,
|
||||
},
|
||||
],
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 100,
|
||||
totalItems: 1,
|
||||
totalPages: 1,
|
||||
},
|
||||
rowHeight: 60,
|
||||
scrollOffset: 0,
|
||||
},
|
||||
},
|
||||
genre: {
|
||||
display: ListDisplayType.TABLE,
|
||||
filter: {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export type SidebarItemType = {
|
|||
route: AppRoute | string;
|
||||
};
|
||||
|
||||
export const sidebarItems = [
|
||||
export const sidebarItems: SidebarItemType[] = [
|
||||
{
|
||||
disabled: true,
|
||||
id: 'Now Playing',
|
||||
|
|
@ -64,21 +64,21 @@ export const sidebarItems = [
|
|||
{
|
||||
disabled: false,
|
||||
id: 'Artists',
|
||||
label: i18n.t('page.sidebar.artists'),
|
||||
label: i18n.t('page.sidebar.albumArtists'),
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS,
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
id: 'Artists-all',
|
||||
label: i18n.t('page.sidebar.artists'),
|
||||
route: AppRoute.LIBRARY_ARTISTS,
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
id: 'Genres',
|
||||
label: i18n.t('page.sidebar.genres'),
|
||||
route: AppRoute.LIBRARY_GENRES,
|
||||
},
|
||||
{
|
||||
disabled: true,
|
||||
id: 'Folders',
|
||||
label: i18n.t('page.sidebar.folders'),
|
||||
route: AppRoute.LIBRARY_FOLDERS,
|
||||
},
|
||||
{
|
||||
disabled: true,
|
||||
id: 'Playlists',
|
||||
|
|
@ -735,7 +735,7 @@ export const useSettingsStore = create<SettingsSlice>()(
|
|||
{
|
||||
merge: mergeOverridingColumns,
|
||||
name: 'store_settings',
|
||||
version: 8,
|
||||
version: 9,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue