Subsonic 2, general rework (#758)
This commit is contained in:
parent
31492fa9ef
commit
8cddbef701
69 changed files with 4625 additions and 3566 deletions
|
@ -1,119 +1,11 @@
|
||||||
import { useAuthStore } from '/@/renderer/store';
|
import { useAuthStore } from '/@/renderer/store';
|
||||||
import { toast } from '/@/renderer/components/toast/index';
|
import { toast } from '/@/renderer/components/toast/index';
|
||||||
import type {
|
import type { ServerType, ControllerEndpoint, AuthenticationResponse } from '/@/renderer/api/types';
|
||||||
AlbumDetailArgs,
|
import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller';
|
||||||
AlbumListArgs,
|
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||||
SongListArgs,
|
import { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller';
|
||||||
SongDetailArgs,
|
|
||||||
AlbumArtistDetailArgs,
|
|
||||||
AlbumArtistListArgs,
|
|
||||||
SetRatingArgs,
|
|
||||||
ShareItemArgs,
|
|
||||||
GenreListArgs,
|
|
||||||
CreatePlaylistArgs,
|
|
||||||
DeletePlaylistArgs,
|
|
||||||
PlaylistDetailArgs,
|
|
||||||
PlaylistListArgs,
|
|
||||||
MusicFolderListArgs,
|
|
||||||
PlaylistSongListArgs,
|
|
||||||
ArtistListArgs,
|
|
||||||
UpdatePlaylistArgs,
|
|
||||||
UserListArgs,
|
|
||||||
FavoriteArgs,
|
|
||||||
TopSongListArgs,
|
|
||||||
AddToPlaylistArgs,
|
|
||||||
AddToPlaylistResponse,
|
|
||||||
RemoveFromPlaylistArgs,
|
|
||||||
RemoveFromPlaylistResponse,
|
|
||||||
ScrobbleArgs,
|
|
||||||
ScrobbleResponse,
|
|
||||||
AlbumArtistDetailResponse,
|
|
||||||
FavoriteResponse,
|
|
||||||
CreatePlaylistResponse,
|
|
||||||
AlbumArtistListResponse,
|
|
||||||
AlbumDetailResponse,
|
|
||||||
AlbumListResponse,
|
|
||||||
ArtistListResponse,
|
|
||||||
GenreListResponse,
|
|
||||||
MusicFolderListResponse,
|
|
||||||
PlaylistDetailResponse,
|
|
||||||
PlaylistListResponse,
|
|
||||||
RatingResponse,
|
|
||||||
SongDetailResponse,
|
|
||||||
SongListResponse,
|
|
||||||
TopSongListResponse,
|
|
||||||
UpdatePlaylistResponse,
|
|
||||||
UserListResponse,
|
|
||||||
AuthenticationResponse,
|
|
||||||
SearchArgs,
|
|
||||||
SearchResponse,
|
|
||||||
LyricsArgs,
|
|
||||||
LyricsResponse,
|
|
||||||
ServerInfo,
|
|
||||||
ServerInfoArgs,
|
|
||||||
StructuredLyricsArgs,
|
|
||||||
StructuredLyric,
|
|
||||||
SimilarSongsArgs,
|
|
||||||
Song,
|
|
||||||
ServerType,
|
|
||||||
ShareItemResponse,
|
|
||||||
MoveItemArgs,
|
|
||||||
DownloadArgs,
|
|
||||||
TranscodingArgs,
|
|
||||||
} from '/@/renderer/api/types';
|
|
||||||
import { DeletePlaylistResponse, RandomSongListArgs } from './types';
|
|
||||||
import { ndController } from '/@/renderer/api/navidrome/navidrome-controller';
|
|
||||||
import { ssController } from '/@/renderer/api/subsonic/subsonic-controller';
|
|
||||||
import { jfController } from '/@/renderer/api/jellyfin/jellyfin-controller';
|
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
|
|
||||||
export type ControllerEndpoint = Partial<{
|
|
||||||
addToPlaylist: (args: AddToPlaylistArgs) => Promise<AddToPlaylistResponse>;
|
|
||||||
authenticate: (
|
|
||||||
url: string,
|
|
||||||
body: { password: string; username: string },
|
|
||||||
) => Promise<AuthenticationResponse>;
|
|
||||||
clearPlaylist: () => void;
|
|
||||||
createFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
|
|
||||||
createPlaylist: (args: CreatePlaylistArgs) => Promise<CreatePlaylistResponse>;
|
|
||||||
deleteFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
|
|
||||||
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
|
|
||||||
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
|
|
||||||
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
|
|
||||||
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
|
|
||||||
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
|
|
||||||
getArtistDetail: () => void;
|
|
||||||
getArtistInfo: (args: any) => void;
|
|
||||||
getArtistList: (args: ArtistListArgs) => Promise<ArtistListResponse>;
|
|
||||||
getDownloadUrl: (args: DownloadArgs) => string;
|
|
||||||
getFavoritesList: () => void;
|
|
||||||
getFolderItemList: () => void;
|
|
||||||
getFolderList: () => void;
|
|
||||||
getFolderSongs: () => void;
|
|
||||||
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
|
|
||||||
getLyrics: (args: LyricsArgs) => Promise<LyricsResponse>;
|
|
||||||
getMusicFolderList: (args: MusicFolderListArgs) => Promise<MusicFolderListResponse>;
|
|
||||||
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
|
|
||||||
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
|
|
||||||
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
|
|
||||||
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
|
|
||||||
getServerInfo: (args: ServerInfoArgs) => Promise<ServerInfo>;
|
|
||||||
getSimilarSongs: (args: SimilarSongsArgs) => Promise<Song[]>;
|
|
||||||
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
|
|
||||||
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
|
|
||||||
getStructuredLyrics: (args: StructuredLyricsArgs) => Promise<StructuredLyric[]>;
|
|
||||||
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
|
|
||||||
getTranscodingUrl: (args: TranscodingArgs) => string;
|
|
||||||
getUserList: (args: UserListArgs) => Promise<UserListResponse>;
|
|
||||||
movePlaylistItem: (args: MoveItemArgs) => Promise<void>;
|
|
||||||
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
|
|
||||||
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
|
|
||||||
search: (args: SearchArgs) => Promise<SearchResponse>;
|
|
||||||
setRating: (args: SetRatingArgs) => Promise<RatingResponse>;
|
|
||||||
shareItem: (args: ShareItemArgs) => Promise<ShareItemResponse>;
|
|
||||||
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
type ApiController = {
|
type ApiController = {
|
||||||
jellyfin: ControllerEndpoint;
|
jellyfin: ControllerEndpoint;
|
||||||
navidrome: ControllerEndpoint;
|
navidrome: ControllerEndpoint;
|
||||||
|
@ -121,133 +13,15 @@ type ApiController = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoints: ApiController = {
|
const endpoints: ApiController = {
|
||||||
jellyfin: {
|
jellyfin: JellyfinController,
|
||||||
addToPlaylist: jfController.addToPlaylist,
|
navidrome: NavidromeController,
|
||||||
authenticate: jfController.authenticate,
|
subsonic: SubsonicController,
|
||||||
clearPlaylist: undefined,
|
|
||||||
createFavorite: jfController.createFavorite,
|
|
||||||
createPlaylist: jfController.createPlaylist,
|
|
||||||
deleteFavorite: jfController.deleteFavorite,
|
|
||||||
deletePlaylist: jfController.deletePlaylist,
|
|
||||||
getAlbumArtistDetail: jfController.getAlbumArtistDetail,
|
|
||||||
getAlbumArtistList: jfController.getAlbumArtistList,
|
|
||||||
getAlbumDetail: jfController.getAlbumDetail,
|
|
||||||
getAlbumList: jfController.getAlbumList,
|
|
||||||
getArtistDetail: undefined,
|
|
||||||
getArtistInfo: undefined,
|
|
||||||
getArtistList: undefined,
|
|
||||||
getDownloadUrl: jfController.getDownloadUrl,
|
|
||||||
getFavoritesList: undefined,
|
|
||||||
getFolderItemList: undefined,
|
|
||||||
getFolderList: undefined,
|
|
||||||
getFolderSongs: undefined,
|
|
||||||
getGenreList: jfController.getGenreList,
|
|
||||||
getLyrics: jfController.getLyrics,
|
|
||||||
getMusicFolderList: jfController.getMusicFolderList,
|
|
||||||
getPlaylistDetail: jfController.getPlaylistDetail,
|
|
||||||
getPlaylistList: jfController.getPlaylistList,
|
|
||||||
getPlaylistSongList: jfController.getPlaylistSongList,
|
|
||||||
getRandomSongList: jfController.getRandomSongList,
|
|
||||||
getServerInfo: jfController.getServerInfo,
|
|
||||||
getSimilarSongs: jfController.getSimilarSongs,
|
|
||||||
getSongDetail: jfController.getSongDetail,
|
|
||||||
getSongList: jfController.getSongList,
|
|
||||||
getStructuredLyrics: undefined,
|
|
||||||
getTopSongs: jfController.getTopSongList,
|
|
||||||
getTranscodingUrl: jfController.getTranscodingUrl,
|
|
||||||
getUserList: undefined,
|
|
||||||
movePlaylistItem: jfController.movePlaylistItem,
|
|
||||||
removeFromPlaylist: jfController.removeFromPlaylist,
|
|
||||||
scrobble: jfController.scrobble,
|
|
||||||
search: jfController.search,
|
|
||||||
setRating: undefined,
|
|
||||||
shareItem: undefined,
|
|
||||||
updatePlaylist: jfController.updatePlaylist,
|
|
||||||
},
|
|
||||||
navidrome: {
|
|
||||||
addToPlaylist: ndController.addToPlaylist,
|
|
||||||
authenticate: ndController.authenticate,
|
|
||||||
clearPlaylist: undefined,
|
|
||||||
createFavorite: ssController.createFavorite,
|
|
||||||
createPlaylist: ndController.createPlaylist,
|
|
||||||
deleteFavorite: ssController.removeFavorite,
|
|
||||||
deletePlaylist: ndController.deletePlaylist,
|
|
||||||
getAlbumArtistDetail: ndController.getAlbumArtistDetail,
|
|
||||||
getAlbumArtistList: ndController.getAlbumArtistList,
|
|
||||||
getAlbumDetail: ndController.getAlbumDetail,
|
|
||||||
getAlbumList: ndController.getAlbumList,
|
|
||||||
getArtistDetail: undefined,
|
|
||||||
getArtistInfo: undefined,
|
|
||||||
getArtistList: undefined,
|
|
||||||
getDownloadUrl: ssController.getDownloadUrl,
|
|
||||||
getFavoritesList: undefined,
|
|
||||||
getFolderItemList: undefined,
|
|
||||||
getFolderList: undefined,
|
|
||||||
getFolderSongs: undefined,
|
|
||||||
getGenreList: ndController.getGenreList,
|
|
||||||
getLyrics: undefined,
|
|
||||||
getMusicFolderList: ssController.getMusicFolderList,
|
|
||||||
getPlaylistDetail: ndController.getPlaylistDetail,
|
|
||||||
getPlaylistList: ndController.getPlaylistList,
|
|
||||||
getPlaylistSongList: ndController.getPlaylistSongList,
|
|
||||||
getRandomSongList: ssController.getRandomSongList,
|
|
||||||
getServerInfo: ndController.getServerInfo,
|
|
||||||
getSimilarSongs: ndController.getSimilarSongs,
|
|
||||||
getSongDetail: ndController.getSongDetail,
|
|
||||||
getSongList: ndController.getSongList,
|
|
||||||
getStructuredLyrics: ssController.getStructuredLyrics,
|
|
||||||
getTopSongs: ssController.getTopSongList,
|
|
||||||
getTranscodingUrl: ssController.getTranscodingUrl,
|
|
||||||
getUserList: ndController.getUserList,
|
|
||||||
movePlaylistItem: ndController.movePlaylistItem,
|
|
||||||
removeFromPlaylist: ndController.removeFromPlaylist,
|
|
||||||
scrobble: ssController.scrobble,
|
|
||||||
search: ssController.search3,
|
|
||||||
setRating: ssController.setRating,
|
|
||||||
shareItem: ndController.shareItem,
|
|
||||||
updatePlaylist: ndController.updatePlaylist,
|
|
||||||
},
|
|
||||||
subsonic: {
|
|
||||||
authenticate: ssController.authenticate,
|
|
||||||
clearPlaylist: undefined,
|
|
||||||
createFavorite: ssController.createFavorite,
|
|
||||||
createPlaylist: undefined,
|
|
||||||
deleteFavorite: ssController.removeFavorite,
|
|
||||||
deletePlaylist: undefined,
|
|
||||||
getAlbumArtistDetail: undefined,
|
|
||||||
getAlbumArtistList: undefined,
|
|
||||||
getAlbumDetail: undefined,
|
|
||||||
getAlbumList: undefined,
|
|
||||||
getArtistDetail: undefined,
|
|
||||||
getArtistInfo: undefined,
|
|
||||||
getArtistList: undefined,
|
|
||||||
getDownloadUrl: ssController.getDownloadUrl,
|
|
||||||
getFavoritesList: undefined,
|
|
||||||
getFolderItemList: undefined,
|
|
||||||
getFolderList: undefined,
|
|
||||||
getFolderSongs: undefined,
|
|
||||||
getGenreList: undefined,
|
|
||||||
getLyrics: undefined,
|
|
||||||
getMusicFolderList: ssController.getMusicFolderList,
|
|
||||||
getPlaylistDetail: undefined,
|
|
||||||
getPlaylistList: undefined,
|
|
||||||
getServerInfo: ssController.getServerInfo,
|
|
||||||
getSimilarSongs: ssController.getSimilarSongs,
|
|
||||||
getSongDetail: undefined,
|
|
||||||
getSongList: undefined,
|
|
||||||
getStructuredLyrics: ssController.getStructuredLyrics,
|
|
||||||
getTopSongs: ssController.getTopSongList,
|
|
||||||
getTranscodingUrl: ssController.getTranscodingUrl,
|
|
||||||
getUserList: undefined,
|
|
||||||
scrobble: ssController.scrobble,
|
|
||||||
search: ssController.search3,
|
|
||||||
setRating: undefined,
|
|
||||||
shareItem: undefined,
|
|
||||||
updatePlaylist: undefined,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiController = (endpoint: keyof ControllerEndpoint, type?: ServerType) => {
|
const apiController = <K extends keyof ControllerEndpoint>(
|
||||||
|
endpoint: K,
|
||||||
|
type?: ServerType,
|
||||||
|
): NonNullable<ControllerEndpoint[K]> => {
|
||||||
const serverType = type || useAuthStore.getState().currentServer?.type;
|
const serverType = type || useAuthStore.getState().currentServer?.type;
|
||||||
|
|
||||||
if (!serverType) {
|
if (!serverType) {
|
||||||
|
@ -277,344 +51,127 @@ const apiController = (endpoint: keyof ControllerEndpoint, type?: ServerType) =>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return endpoints[serverType][endpoint];
|
return controllerFn;
|
||||||
};
|
};
|
||||||
|
|
||||||
const authenticate = async (
|
export interface GeneralController extends Omit<Required<ControllerEndpoint>, 'authenticate'> {
|
||||||
url: string,
|
authenticate: (
|
||||||
body: { legacy?: boolean; password: string; username: string },
|
url: string,
|
||||||
type: ServerType,
|
body: { legacy?: boolean; password: string; username: string },
|
||||||
) => {
|
type: ServerType,
|
||||||
return (apiController('authenticate', type) as ControllerEndpoint['authenticate'])?.(url, body);
|
) => Promise<AuthenticationResponse>;
|
||||||
};
|
}
|
||||||
|
|
||||||
const getAlbumList = async (args: AlbumListArgs) => {
|
export const controller: GeneralController = {
|
||||||
return (
|
addToPlaylist(args) {
|
||||||
apiController(
|
return apiController('addToPlaylist', args.apiClientProps.server?.type)?.(args);
|
||||||
'getAlbumList',
|
},
|
||||||
args.apiClientProps.server?.type,
|
authenticate(url, body, type) {
|
||||||
) as ControllerEndpoint['getAlbumList']
|
return apiController('authenticate', type)(url, body);
|
||||||
)?.(args);
|
},
|
||||||
};
|
createFavorite(args) {
|
||||||
|
return apiController('createFavorite', args.apiClientProps.server?.type)?.(args);
|
||||||
const getAlbumDetail = async (args: AlbumDetailArgs) => {
|
},
|
||||||
return (
|
createPlaylist(args) {
|
||||||
apiController(
|
return apiController('createPlaylist', args.apiClientProps.server?.type)?.(args);
|
||||||
'getAlbumDetail',
|
},
|
||||||
args.apiClientProps.server?.type,
|
deleteFavorite(args) {
|
||||||
) as ControllerEndpoint['getAlbumDetail']
|
return apiController('deleteFavorite', args.apiClientProps.server?.type)?.(args);
|
||||||
)?.(args);
|
},
|
||||||
};
|
deletePlaylist(args) {
|
||||||
|
return apiController('deletePlaylist', args.apiClientProps.server?.type)?.(args);
|
||||||
const getSongList = async (args: SongListArgs) => {
|
},
|
||||||
return (
|
getAlbumArtistDetail(args) {
|
||||||
apiController(
|
return apiController('getAlbumArtistDetail', args.apiClientProps.server?.type)?.(args);
|
||||||
'getSongList',
|
},
|
||||||
args.apiClientProps.server?.type,
|
getAlbumArtistList(args) {
|
||||||
) as ControllerEndpoint['getSongList']
|
return apiController('getAlbumArtistList', args.apiClientProps.server?.type)?.(args);
|
||||||
)?.(args);
|
},
|
||||||
};
|
getAlbumArtistListCount(args) {
|
||||||
|
return apiController('getAlbumArtistListCount', args.apiClientProps.server?.type)?.(args);
|
||||||
const getSongDetail = async (args: SongDetailArgs) => {
|
},
|
||||||
return (
|
getAlbumDetail(args) {
|
||||||
apiController(
|
return apiController('getAlbumDetail', args.apiClientProps.server?.type)?.(args);
|
||||||
'getSongDetail',
|
},
|
||||||
args.apiClientProps.server?.type,
|
getAlbumList(args) {
|
||||||
) as ControllerEndpoint['getSongDetail']
|
return apiController('getAlbumList', args.apiClientProps.server?.type)?.(args);
|
||||||
)?.(args);
|
},
|
||||||
};
|
getAlbumListCount(args) {
|
||||||
|
return apiController('getAlbumListCount', args.apiClientProps.server?.type)?.(args);
|
||||||
const getMusicFolderList = async (args: MusicFolderListArgs) => {
|
},
|
||||||
return (
|
getDownloadUrl(args) {
|
||||||
apiController(
|
return apiController('getDownloadUrl', args.apiClientProps.server?.type)?.(args);
|
||||||
'getMusicFolderList',
|
},
|
||||||
args.apiClientProps.server?.type,
|
getGenreList(args) {
|
||||||
) as ControllerEndpoint['getMusicFolderList']
|
return apiController('getGenreList', args.apiClientProps.server?.type)?.(args);
|
||||||
)?.(args);
|
},
|
||||||
};
|
getLyrics(args) {
|
||||||
|
return apiController('getLyrics', args.apiClientProps.server?.type)?.(args);
|
||||||
const getGenreList = async (args: GenreListArgs) => {
|
},
|
||||||
return (
|
getMusicFolderList(args) {
|
||||||
apiController(
|
return apiController('getMusicFolderList', args.apiClientProps.server?.type)?.(args);
|
||||||
'getGenreList',
|
},
|
||||||
args.apiClientProps.server?.type,
|
getPlaylistDetail(args) {
|
||||||
) as ControllerEndpoint['getGenreList']
|
return apiController('getPlaylistDetail', args.apiClientProps.server?.type)?.(args);
|
||||||
)?.(args);
|
},
|
||||||
};
|
getPlaylistList(args) {
|
||||||
|
return apiController('getPlaylistList', args.apiClientProps.server?.type)?.(args);
|
||||||
const getAlbumArtistDetail = async (args: AlbumArtistDetailArgs) => {
|
},
|
||||||
return (
|
getPlaylistListCount(args) {
|
||||||
apiController(
|
return apiController('getPlaylistListCount', args.apiClientProps.server?.type)?.(args);
|
||||||
'getAlbumArtistDetail',
|
},
|
||||||
args.apiClientProps.server?.type,
|
getPlaylistSongList(args) {
|
||||||
) as ControllerEndpoint['getAlbumArtistDetail']
|
return apiController('getPlaylistSongList', args.apiClientProps.server?.type)?.(args);
|
||||||
)?.(args);
|
},
|
||||||
};
|
getRandomSongList(args) {
|
||||||
|
return apiController('getRandomSongList', args.apiClientProps.server?.type)?.(args);
|
||||||
const getAlbumArtistList = async (args: AlbumArtistListArgs) => {
|
},
|
||||||
return (
|
getServerInfo(args) {
|
||||||
apiController(
|
return apiController('getServerInfo', args.apiClientProps.server?.type)?.(args);
|
||||||
'getAlbumArtistList',
|
},
|
||||||
args.apiClientProps.server?.type,
|
getSimilarSongs(args) {
|
||||||
) as ControllerEndpoint['getAlbumArtistList']
|
return apiController('getSimilarSongs', args.apiClientProps.server?.type)?.(args);
|
||||||
)?.(args);
|
},
|
||||||
};
|
getSongDetail(args) {
|
||||||
|
return apiController('getSongDetail', args.apiClientProps.server?.type)?.(args);
|
||||||
const getArtistList = async (args: ArtistListArgs) => {
|
},
|
||||||
return (
|
getSongList(args) {
|
||||||
apiController(
|
return apiController('getSongList', args.apiClientProps.server?.type)?.(args);
|
||||||
'getArtistList',
|
},
|
||||||
args.apiClientProps.server?.type,
|
getSongListCount(args) {
|
||||||
) as ControllerEndpoint['getArtistList']
|
return apiController('getSongListCount', args.apiClientProps.server?.type)?.(args);
|
||||||
)?.(args);
|
},
|
||||||
};
|
getStructuredLyrics(args) {
|
||||||
|
return apiController('getStructuredLyrics', args.apiClientProps.server?.type)?.(args);
|
||||||
const getPlaylistList = async (args: PlaylistListArgs) => {
|
},
|
||||||
return (
|
getTopSongs(args) {
|
||||||
apiController(
|
return apiController('getTopSongs', args.apiClientProps.server?.type)?.(args);
|
||||||
'getPlaylistList',
|
},
|
||||||
args.apiClientProps.server?.type,
|
getTranscodingUrl(args) {
|
||||||
) as ControllerEndpoint['getPlaylistList']
|
return apiController('getTranscodingUrl', args.apiClientProps.server?.type)?.(args);
|
||||||
)?.(args);
|
},
|
||||||
};
|
getUserList(args) {
|
||||||
|
return apiController('getUserList', args.apiClientProps.server?.type)?.(args);
|
||||||
const createPlaylist = async (args: CreatePlaylistArgs) => {
|
},
|
||||||
return (
|
movePlaylistItem(args) {
|
||||||
apiController(
|
return apiController('movePlaylistItem', args.apiClientProps.server?.type)?.(args);
|
||||||
'createPlaylist',
|
},
|
||||||
args.apiClientProps.server?.type,
|
removeFromPlaylist(args) {
|
||||||
) as ControllerEndpoint['createPlaylist']
|
return apiController('removeFromPlaylist', args.apiClientProps.server?.type)?.(args);
|
||||||
)?.(args);
|
},
|
||||||
};
|
scrobble(args) {
|
||||||
|
return apiController('scrobble', args.apiClientProps.server?.type)?.(args);
|
||||||
const updatePlaylist = async (args: UpdatePlaylistArgs) => {
|
},
|
||||||
return (
|
search(args) {
|
||||||
apiController(
|
return apiController('search', args.apiClientProps.server?.type)?.(args);
|
||||||
'updatePlaylist',
|
},
|
||||||
args.apiClientProps.server?.type,
|
setRating(args) {
|
||||||
) as ControllerEndpoint['updatePlaylist']
|
return apiController('setRating', args.apiClientProps.server?.type)?.(args);
|
||||||
)?.(args);
|
},
|
||||||
};
|
shareItem(args) {
|
||||||
|
return apiController('shareItem', args.apiClientProps.server?.type)?.(args);
|
||||||
const deletePlaylist = async (args: DeletePlaylistArgs) => {
|
},
|
||||||
return (
|
updatePlaylist(args) {
|
||||||
apiController(
|
return apiController('updatePlaylist', args.apiClientProps.server?.type)?.(args);
|
||||||
'deletePlaylist',
|
},
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['deletePlaylist']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const addToPlaylist = async (args: AddToPlaylistArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'addToPlaylist',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['addToPlaylist']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeFromPlaylist = async (args: RemoveFromPlaylistArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'removeFromPlaylist',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['removeFromPlaylist']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlaylistDetail = async (args: PlaylistDetailArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'getPlaylistDetail',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['getPlaylistDetail']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlaylistSongList = async (args: PlaylistSongListArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'getPlaylistSongList',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['getPlaylistSongList']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserList = async (args: UserListArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'getUserList',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['getUserList']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createFavorite = async (args: FavoriteArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'createFavorite',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['createFavorite']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteFavorite = async (args: FavoriteArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'deleteFavorite',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['deleteFavorite']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateRating = async (args: SetRatingArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'setRating',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['setRating']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const shareItem = async (args: ShareItemArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'shareItem',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['shareItem']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTopSongList = async (args: TopSongListArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'getTopSongs',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['getTopSongs']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrobble = async (args: ScrobbleArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'scrobble',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['scrobble']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const search = async (args: SearchArgs) => {
|
|
||||||
return (
|
|
||||||
apiController('search', args.apiClientProps.server?.type) as ControllerEndpoint['search']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRandomSongList = async (args: RandomSongListArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'getRandomSongList',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['getRandomSongList']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLyrics = async (args: LyricsArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'getLyrics',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['getLyrics']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getServerInfo = async (args: ServerInfoArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'getServerInfo',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['getServerInfo']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStructuredLyrics = async (args: StructuredLyricsArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'getStructuredLyrics',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['getStructuredLyrics']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSimilarSongs = async (args: SimilarSongsArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'getSimilarSongs',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['getSimilarSongs']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const movePlaylistItem = async (args: MoveItemArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'movePlaylistItem',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['movePlaylistItem']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDownloadUrl = (args: DownloadArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'getDownloadUrl',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['getDownloadUrl']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTranscodingUrl = (args: TranscodingArgs) => {
|
|
||||||
return (
|
|
||||||
apiController(
|
|
||||||
'getTranscodingUrl',
|
|
||||||
args.apiClientProps.server?.type,
|
|
||||||
) as ControllerEndpoint['getTranscodingUrl']
|
|
||||||
)?.(args);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const controller = {
|
|
||||||
addToPlaylist,
|
|
||||||
authenticate,
|
|
||||||
createFavorite,
|
|
||||||
createPlaylist,
|
|
||||||
deleteFavorite,
|
|
||||||
deletePlaylist,
|
|
||||||
getAlbumArtistDetail,
|
|
||||||
getAlbumArtistList,
|
|
||||||
getAlbumDetail,
|
|
||||||
getAlbumList,
|
|
||||||
getArtistList,
|
|
||||||
getDownloadUrl,
|
|
||||||
getGenreList,
|
|
||||||
getLyrics,
|
|
||||||
getMusicFolderList,
|
|
||||||
getPlaylistDetail,
|
|
||||||
getPlaylistList,
|
|
||||||
getPlaylistSongList,
|
|
||||||
getRandomSongList,
|
|
||||||
getServerInfo,
|
|
||||||
getSimilarSongs,
|
|
||||||
getSongDetail,
|
|
||||||
getSongList,
|
|
||||||
getStructuredLyrics,
|
|
||||||
getTopSongList,
|
|
||||||
getTranscodingUrl,
|
|
||||||
getUserList,
|
|
||||||
movePlaylistItem,
|
|
||||||
removeFromPlaylist,
|
|
||||||
scrobble,
|
|
||||||
search,
|
|
||||||
shareItem,
|
|
||||||
updatePlaylist,
|
|
||||||
updateRating,
|
|
||||||
};
|
};
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -237,7 +237,7 @@ export enum NDSongListSort {
|
||||||
CHANNELS = 'channels',
|
CHANNELS = 'channels',
|
||||||
COMMENT = 'comment',
|
COMMENT = 'comment',
|
||||||
DURATION = 'duration',
|
DURATION = 'duration',
|
||||||
FAVORITED = 'starred',
|
FAVORITED = 'starred_at',
|
||||||
GENRE = 'genre',
|
GENRE = 'genre',
|
||||||
ID = 'id',
|
ID = 'id',
|
||||||
PLAY_COUNT = 'playCount',
|
PLAY_COUNT = 'playCount',
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -224,7 +224,7 @@ const songListParameters = paginationParameters.extend({
|
||||||
album_artist_id: z.array(z.string()).optional(),
|
album_artist_id: z.array(z.string()).optional(),
|
||||||
album_id: z.array(z.string()).optional(),
|
album_id: z.array(z.string()).optional(),
|
||||||
artist_id: z.array(z.string()).optional(),
|
artist_id: z.array(z.string()).optional(),
|
||||||
genre_id: z.string().optional(),
|
genre_id: z.array(z.string()).optional(),
|
||||||
path: z.string().optional(),
|
path: z.string().optional(),
|
||||||
starred: z.boolean().optional(),
|
starred: z.boolean().optional(),
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
|
|
|
@ -50,6 +50,19 @@ export const queryKeys: Record<
|
||||||
Record<string, (...props: any) => QueryFunctionContext['queryKey']>
|
Record<string, (...props: any) => QueryFunctionContext['queryKey']>
|
||||||
> = {
|
> = {
|
||||||
albumArtists: {
|
albumArtists: {
|
||||||
|
count: (serverId: string, query?: AlbumArtistListQuery) => {
|
||||||
|
const { pagination, filter } = splitPaginatedQuery(query);
|
||||||
|
|
||||||
|
if (query && pagination) {
|
||||||
|
return [serverId, 'albumArtists', 'count', filter, pagination] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
return [serverId, 'albumArtists', 'count', filter] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [serverId, 'albumArtists', 'count'] as const;
|
||||||
|
},
|
||||||
detail: (serverId: string, query?: AlbumArtistDetailQuery) => {
|
detail: (serverId: string, query?: AlbumArtistDetailQuery) => {
|
||||||
if (query) return [serverId, 'albumArtists', 'detail', query] as const;
|
if (query) return [serverId, 'albumArtists', 'detail', query] as const;
|
||||||
return [serverId, 'albumArtists', 'detail'] as const;
|
return [serverId, 'albumArtists', 'detail'] as const;
|
||||||
|
@ -73,6 +86,27 @@ export const queryKeys: Record<
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
albums: {
|
albums: {
|
||||||
|
count: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
|
||||||
|
const { pagination, filter } = splitPaginatedQuery(query);
|
||||||
|
|
||||||
|
if (query && pagination && artistId) {
|
||||||
|
return [serverId, 'albums', 'count', artistId, filter, pagination] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query && pagination) {
|
||||||
|
return [serverId, 'albums', 'count', filter, pagination] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query && artistId) {
|
||||||
|
return [serverId, 'albums', 'count', artistId, filter] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
return [serverId, 'albums', 'count', filter] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [serverId, 'albums', 'count'] as const;
|
||||||
|
},
|
||||||
detail: (serverId: string, query?: AlbumDetailQuery) =>
|
detail: (serverId: string, query?: AlbumDetailQuery) =>
|
||||||
[serverId, 'albums', 'detail', query] as const,
|
[serverId, 'albums', 'detail', query] as const,
|
||||||
list: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
|
list: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
|
||||||
|
@ -208,6 +242,18 @@ export const queryKeys: Record<
|
||||||
root: (serverId: string) => [serverId] as const,
|
root: (serverId: string) => [serverId] as const,
|
||||||
},
|
},
|
||||||
songs: {
|
songs: {
|
||||||
|
count: (serverId: string, query?: SongListQuery) => {
|
||||||
|
const { pagination, filter } = splitPaginatedQuery(query);
|
||||||
|
if (query && pagination) {
|
||||||
|
return [serverId, 'songs', 'count', filter, pagination] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
return [serverId, 'songs', 'count', filter] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [serverId, 'songs', 'count'] as const;
|
||||||
|
},
|
||||||
detail: (serverId: string, query?: SongDetailQuery) => {
|
detail: (serverId: string, query?: SongDetailQuery) => {
|
||||||
if (query) return [serverId, 'songs', 'detail', query] as const;
|
if (query) return [serverId, 'songs', 'detail', query] as const;
|
||||||
return [serverId, 'songs', 'detail'] as const;
|
return [serverId, 'songs', 'detail'] as const;
|
||||||
|
|
|
@ -27,6 +27,46 @@ export const contract = c.router({
|
||||||
200: ssType._response.createFavorite,
|
200: ssType._response.createFavorite,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
createPlaylist: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'createPlaylist.view',
|
||||||
|
query: ssType._parameters.createPlaylist,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.createPlaylist,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deletePlaylist: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'deletePlaylist.view',
|
||||||
|
query: ssType._parameters.deletePlaylist,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.baseResponse,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getAlbum: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'getAlbum.view',
|
||||||
|
query: ssType._parameters.getAlbum,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.getAlbum,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getAlbumList2: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'getAlbumList2.view',
|
||||||
|
query: ssType._parameters.getAlbumList2,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.getAlbumList2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getArtist: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'getArtist.view',
|
||||||
|
query: ssType._parameters.getArtist,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.getArtist,
|
||||||
|
},
|
||||||
|
},
|
||||||
getArtistInfo: {
|
getArtistInfo: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'getArtistInfo.view',
|
path: 'getArtistInfo.view',
|
||||||
|
@ -35,6 +75,22 @@ export const contract = c.router({
|
||||||
200: ssType._response.artistInfo,
|
200: ssType._response.artistInfo,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
getArtists: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'getArtists.view',
|
||||||
|
query: ssType._parameters.getArtists,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.getArtists,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getGenres: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'getGenres.view',
|
||||||
|
query: ssType._parameters.getGenres,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.getGenres,
|
||||||
|
},
|
||||||
|
},
|
||||||
getMusicFolderList: {
|
getMusicFolderList: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'getMusicFolders.view',
|
path: 'getMusicFolders.view',
|
||||||
|
@ -42,6 +98,22 @@ export const contract = c.router({
|
||||||
200: ssType._response.musicFolderList,
|
200: ssType._response.musicFolderList,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
getPlaylist: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'getPlaylist.view',
|
||||||
|
query: ssType._parameters.getPlaylist,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.getPlaylist,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getPlaylists: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'getPlaylists.view',
|
||||||
|
query: ssType._parameters.getPlaylists,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.getPlaylists,
|
||||||
|
},
|
||||||
|
},
|
||||||
getRandomSongList: {
|
getRandomSongList: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'getRandomSongs.view',
|
path: 'getRandomSongs.view',
|
||||||
|
@ -65,6 +137,30 @@ export const contract = c.router({
|
||||||
200: ssType._response.similarSongs,
|
200: ssType._response.similarSongs,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
getSong: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'getSong.view',
|
||||||
|
query: ssType._parameters.getSong,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.getSong,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getSongsByGenre: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'getSongsByGenre.view',
|
||||||
|
query: ssType._parameters.getSongsByGenre,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.getSongsByGenre,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getStarred: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'getStarred.view',
|
||||||
|
query: ssType._parameters.getStarred,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.getStarred,
|
||||||
|
},
|
||||||
|
},
|
||||||
getStructuredLyrics: {
|
getStructuredLyrics: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: 'getLyricsBySongId.view',
|
path: 'getLyricsBySongId.view',
|
||||||
|
@ -120,6 +216,14 @@ export const contract = c.router({
|
||||||
200: ssType._response.setRating,
|
200: ssType._response.setRating,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
updatePlaylist: {
|
||||||
|
method: 'GET',
|
||||||
|
path: 'updatePlaylist.view',
|
||||||
|
query: ssType._parameters.updatePlaylist,
|
||||||
|
responses: {
|
||||||
|
200: ssType._response.baseResponse,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const axiosClient = axios.create({});
|
const axiosClient = axios.create({});
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -8,6 +8,8 @@ import {
|
||||||
Album,
|
Album,
|
||||||
ServerListItem,
|
ServerListItem,
|
||||||
ServerType,
|
ServerType,
|
||||||
|
Playlist,
|
||||||
|
Genre,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
|
|
||||||
const getCoverArtUrl = (args: {
|
const getCoverArtUrl = (args: {
|
||||||
|
@ -36,13 +38,14 @@ const normalizeSong = (
|
||||||
item: z.infer<typeof ssType._response.song>,
|
item: z.infer<typeof ssType._response.song>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
|
size?: number,
|
||||||
): QueueSong => {
|
): QueueSong => {
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
getCoverArtUrl({
|
getCoverArtUrl({
|
||||||
baseUrl: server?.url,
|
baseUrl: server?.url,
|
||||||
coverArtId: item.coverArt,
|
coverArtId: item.coverArt,
|
||||||
credential: server?.credential,
|
credential: server?.credential,
|
||||||
size: 100,
|
size: size || 300,
|
||||||
}) || null;
|
}) || null;
|
||||||
|
|
||||||
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=feishin_${deviceId}&${server?.credential}`;
|
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=feishin_${deviceId}&${server?.credential}`;
|
||||||
|
@ -66,7 +69,7 @@ const normalizeSong = (
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bitRate: item.bitRate || 0,
|
bitRate: item.bitRate || 0,
|
||||||
bpm: null,
|
bpm: item.bpm || null,
|
||||||
channels: null,
|
channels: null,
|
||||||
comment: null,
|
comment: null,
|
||||||
compilation: null,
|
compilation: null,
|
||||||
|
@ -123,15 +126,18 @@ const normalizeSong = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeAlbumArtist = (
|
const normalizeAlbumArtist = (
|
||||||
item: z.infer<typeof ssType._response.albumArtist>,
|
item:
|
||||||
|
| z.infer<typeof ssType._response.albumArtist>
|
||||||
|
| z.infer<typeof ssType._response.artistListEntry>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
|
imageSize?: number,
|
||||||
): AlbumArtist => {
|
): AlbumArtist => {
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
getCoverArtUrl({
|
getCoverArtUrl({
|
||||||
baseUrl: server?.url,
|
baseUrl: server?.url,
|
||||||
coverArtId: item.coverArt,
|
coverArtId: item.coverArt,
|
||||||
credential: server?.credential,
|
credential: server?.credential,
|
||||||
size: 100,
|
size: imageSize || 100,
|
||||||
}) || null;
|
}) || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -157,15 +163,16 @@ const normalizeAlbumArtist = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeAlbum = (
|
const normalizeAlbum = (
|
||||||
item: z.infer<typeof ssType._response.album>,
|
item: z.infer<typeof ssType._response.album> | z.infer<typeof ssType._response.albumListEntry>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
|
imageSize?: number,
|
||||||
): Album => {
|
): Album => {
|
||||||
const imageUrl =
|
const imageUrl =
|
||||||
getCoverArtUrl({
|
getCoverArtUrl({
|
||||||
baseUrl: server?.url,
|
baseUrl: server?.url,
|
||||||
coverArtId: item.coverArt,
|
coverArtId: item.coverArt,
|
||||||
credential: server?.credential,
|
credential: server?.credential,
|
||||||
size: 300,
|
size: imageSize || 300,
|
||||||
}) || null;
|
}) || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -177,7 +184,7 @@ const normalizeAlbum = (
|
||||||
backdropImageUrl: null,
|
backdropImageUrl: null,
|
||||||
comment: null,
|
comment: null,
|
||||||
createdAt: item.created,
|
createdAt: item.created,
|
||||||
duration: item.duration,
|
duration: item.duration * 1000,
|
||||||
genres: item.genre
|
genres: item.genre
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
|
@ -204,7 +211,10 @@ const normalizeAlbum = (
|
||||||
serverType: ServerType.SUBSONIC,
|
serverType: ServerType.SUBSONIC,
|
||||||
size: null,
|
size: null,
|
||||||
songCount: item.songCount,
|
songCount: item.songCount,
|
||||||
songs: [],
|
songs:
|
||||||
|
(item as z.infer<typeof ssType._response.album>).song?.map((song) =>
|
||||||
|
normalizeSong(song, server, ''),
|
||||||
|
) || [],
|
||||||
uniqueId: nanoid(),
|
uniqueId: nanoid(),
|
||||||
updatedAt: item.created,
|
updatedAt: item.created,
|
||||||
userFavorite: item.starred || false,
|
userFavorite: item.starred || false,
|
||||||
|
@ -212,8 +222,51 @@ const normalizeAlbum = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizePlaylist = (
|
||||||
|
item:
|
||||||
|
| z.infer<typeof ssType._response.playlist>
|
||||||
|
| z.infer<typeof ssType._response.playlistListEntry>,
|
||||||
|
server: ServerListItem | null,
|
||||||
|
): Playlist => {
|
||||||
|
return {
|
||||||
|
description: item.comment || null,
|
||||||
|
duration: item.duration,
|
||||||
|
genres: [],
|
||||||
|
id: item.id,
|
||||||
|
imagePlaceholderUrl: null,
|
||||||
|
imageUrl: getCoverArtUrl({
|
||||||
|
baseUrl: server?.url,
|
||||||
|
coverArtId: item.coverArt,
|
||||||
|
credential: server?.credential,
|
||||||
|
size: 300,
|
||||||
|
}),
|
||||||
|
itemType: LibraryItem.PLAYLIST,
|
||||||
|
name: item.name,
|
||||||
|
owner: item.owner,
|
||||||
|
ownerId: item.owner,
|
||||||
|
public: item.public,
|
||||||
|
serverId: server?.id || 'unknown',
|
||||||
|
serverType: ServerType.SUBSONIC,
|
||||||
|
size: null,
|
||||||
|
songCount: item.songCount,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeGenre = (item: z.infer<typeof ssType._response.genre>): Genre => {
|
||||||
|
return {
|
||||||
|
albumCount: item.albumCount,
|
||||||
|
id: item.value,
|
||||||
|
imageUrl: null,
|
||||||
|
itemType: LibraryItem.GENRE,
|
||||||
|
name: item.value,
|
||||||
|
songCount: item.songCount,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const ssNormalize = {
|
export const ssNormalize = {
|
||||||
album: normalizeAlbum,
|
album: normalizeAlbum,
|
||||||
albumArtist: normalizeAlbumArtist,
|
albumArtist: normalizeAlbumArtist,
|
||||||
|
genre: normalizeGenre,
|
||||||
|
playlist: normalizePlaylist,
|
||||||
song: normalizeSong,
|
song: normalizeSong,
|
||||||
};
|
};
|
||||||
|
|
|
@ -60,6 +60,10 @@ const songGain = z.object({
|
||||||
trackPeak: z.number().optional(),
|
trackPeak: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const genreItem = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
const song = z.object({
|
const song = z.object({
|
||||||
album: z.string().optional(),
|
album: z.string().optional(),
|
||||||
albumId: z.string().optional(),
|
albumId: z.string().optional(),
|
||||||
|
@ -67,15 +71,18 @@ const song = z.object({
|
||||||
artistId: z.string().optional(),
|
artistId: z.string().optional(),
|
||||||
averageRating: z.number().optional(),
|
averageRating: z.number().optional(),
|
||||||
bitRate: z.number().optional(),
|
bitRate: z.number().optional(),
|
||||||
|
bpm: z.number().optional(),
|
||||||
contentType: z.string(),
|
contentType: z.string(),
|
||||||
coverArt: z.string().optional(),
|
coverArt: z.string().optional(),
|
||||||
created: z.string(),
|
created: z.string(),
|
||||||
discNumber: z.number(),
|
discNumber: z.number(),
|
||||||
duration: z.number().optional(),
|
duration: z.number().optional(),
|
||||||
genre: z.string().optional(),
|
genre: z.string().optional(),
|
||||||
|
genres: z.array(genreItem).optional(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
isDir: z.boolean(),
|
isDir: z.boolean(),
|
||||||
isVideo: z.boolean(),
|
isVideo: z.boolean(),
|
||||||
|
musicBrainzId: z.string().optional(),
|
||||||
parent: z.string(),
|
parent: z.string(),
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
playCount: z.number().optional(),
|
playCount: z.number().optional(),
|
||||||
|
@ -99,6 +106,7 @@ const album = z.object({
|
||||||
duration: z.number(),
|
duration: z.number(),
|
||||||
genre: z.string().optional(),
|
genre: z.string().optional(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
isCompilation: z.boolean().optional(),
|
||||||
isDir: z.boolean(),
|
isDir: z.boolean(),
|
||||||
isVideo: z.boolean(),
|
isVideo: z.boolean(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
@ -111,6 +119,10 @@ const album = z.object({
|
||||||
year: z.number().optional(),
|
year: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const albumListEntry = album.omit({
|
||||||
|
song: true,
|
||||||
|
});
|
||||||
|
|
||||||
const albumListParameters = z.object({
|
const albumListParameters = z.object({
|
||||||
fromYear: z.number().optional(),
|
fromYear: z.number().optional(),
|
||||||
genre: z.string().optional(),
|
genre: z.string().optional(),
|
||||||
|
@ -124,11 +136,13 @@ const albumListParameters = z.object({
|
||||||
const albumList = z.array(album.omit({ song: true }));
|
const albumList = z.array(album.omit({ song: true }));
|
||||||
|
|
||||||
const albumArtist = z.object({
|
const albumArtist = z.object({
|
||||||
|
album: z.array(album),
|
||||||
albumCount: z.string(),
|
albumCount: z.string(),
|
||||||
artistImageUrl: z.string().optional(),
|
artistImageUrl: z.string().optional(),
|
||||||
coverArt: z.string().optional(),
|
coverArt: z.string().optional(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
starred: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const albumArtistList = z.object({
|
const albumArtistList = z.object({
|
||||||
|
@ -136,6 +150,14 @@ const albumArtistList = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const artistListEntry = albumArtist.pick({
|
||||||
|
albumCount: true,
|
||||||
|
coverArt: true,
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
starred: true,
|
||||||
|
});
|
||||||
|
|
||||||
const artistInfoParameters = z.object({
|
const artistInfoParameters = z.object({
|
||||||
count: z.number().optional(),
|
count: z.number().optional(),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
@ -274,12 +296,215 @@ export enum SubsonicExtensions {
|
||||||
TRANSCODE_OFFSET = 'transcodeOffset',
|
TRANSCODE_OFFSET = 'transcodeOffset',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatePlaylistParameters = z.object({
|
||||||
|
comment: z.string().optional(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
playlistId: z.string(),
|
||||||
|
public: z.boolean().optional(),
|
||||||
|
songIdToAdd: z.array(z.string()).optional(),
|
||||||
|
songIndexToRemove: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getStarredParameters = z.object({
|
||||||
|
musicFolderId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getStarred = z.object({
|
||||||
|
starred: z.object({
|
||||||
|
album: z.array(albumListEntry),
|
||||||
|
artist: z.array(artistListEntry),
|
||||||
|
song: z.array(song),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSongsByGenreParameters = z.object({
|
||||||
|
count: z.number().optional(),
|
||||||
|
genre: z.string(),
|
||||||
|
musicFolderId: z.string().optional(),
|
||||||
|
offset: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSongsByGenre = z.object({
|
||||||
|
songsByGenre: z.object({
|
||||||
|
song: z.array(song),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAlbumParameters = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
musicFolderId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAlbum = z.object({
|
||||||
|
album,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getArtistParameters = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getArtist = z.object({
|
||||||
|
artist: albumArtist,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSongParameters = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSong = z.object({
|
||||||
|
song,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getArtistsParameters = z.object({
|
||||||
|
musicFolderId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getArtists = z.object({
|
||||||
|
artists: z.object({
|
||||||
|
ignoredArticles: z.string(),
|
||||||
|
index: z.array(
|
||||||
|
z.object({
|
||||||
|
artist: z.array(artistListEntry),
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const deletePlaylistParameters = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createPlaylistParameters = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
playlistId: z.string().optional(),
|
||||||
|
songId: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const playlist = z.object({
|
||||||
|
changed: z.string().optional(),
|
||||||
|
comment: z.string().optional(),
|
||||||
|
coverArt: z.string().optional(),
|
||||||
|
created: z.string(),
|
||||||
|
duration: z.number(),
|
||||||
|
entry: z.array(song).optional(),
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
owner: z.string(),
|
||||||
|
public: z.boolean(),
|
||||||
|
songCount: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createPlaylist = z.object({
|
||||||
|
playlist,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPlaylistsParameters = z.object({
|
||||||
|
username: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const playlistListEntry = playlist.omit({
|
||||||
|
entry: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPlaylists = z.object({
|
||||||
|
playlists: z.object({
|
||||||
|
playlist: z.array(playlistListEntry),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPlaylistParameters = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getPlaylist = z.object({
|
||||||
|
playlist,
|
||||||
|
});
|
||||||
|
|
||||||
|
const genre = z.object({
|
||||||
|
albumCount: z.number(),
|
||||||
|
songCount: z.number(),
|
||||||
|
value: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const getGenresParameters = z.object({});
|
||||||
|
|
||||||
|
const getGenres = z.object({
|
||||||
|
genres: z.object({
|
||||||
|
genre: z.array(genre),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export enum AlbumListSortType {
|
||||||
|
ALPHABETICAL_BY_ARTIST = 'alphabeticalByArtist',
|
||||||
|
ALPHABETICAL_BY_NAME = 'alphabeticalByName',
|
||||||
|
BY_GENRE = 'byGenre',
|
||||||
|
BY_YEAR = 'byYear',
|
||||||
|
FREQUENT = 'frequent',
|
||||||
|
NEWEST = 'newest',
|
||||||
|
RANDOM = 'random',
|
||||||
|
RECENT = 'recent',
|
||||||
|
STARRED = 'starred',
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAlbumList2Parameters = z
|
||||||
|
.object({
|
||||||
|
fromYear: z.number().optional(),
|
||||||
|
genre: z.string().optional(),
|
||||||
|
musicFolderId: z.string().optional(),
|
||||||
|
offset: z.number().optional(),
|
||||||
|
size: z.number().optional(),
|
||||||
|
toYear: z.number().optional(),
|
||||||
|
type: z.nativeEnum(AlbumListSortType),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(val) => {
|
||||||
|
if (val.type === AlbumListSortType.BY_YEAR) {
|
||||||
|
return val.fromYear !== undefined && val.toYear !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Parameters "fromYear" and "toYear" are required when using sort "byYear"',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(val) => {
|
||||||
|
if (val.type === AlbumListSortType.BY_GENRE) {
|
||||||
|
return val.genre !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{ message: 'Parameter "genre" is required when using sort "byGenre"' },
|
||||||
|
);
|
||||||
|
|
||||||
|
const getAlbumList2 = z.object({
|
||||||
|
albumList2: z.object({
|
||||||
|
album: z.array(albumListEntry),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export const ssType = {
|
export const ssType = {
|
||||||
_parameters: {
|
_parameters: {
|
||||||
albumList: albumListParameters,
|
albumList: albumListParameters,
|
||||||
artistInfo: artistInfoParameters,
|
artistInfo: artistInfoParameters,
|
||||||
authenticate: authenticateParameters,
|
authenticate: authenticateParameters,
|
||||||
createFavorite: createFavoriteParameters,
|
createFavorite: createFavoriteParameters,
|
||||||
|
createPlaylist: createPlaylistParameters,
|
||||||
|
deletePlaylist: deletePlaylistParameters,
|
||||||
|
getAlbum: getAlbumParameters,
|
||||||
|
getAlbumList2: getAlbumList2Parameters,
|
||||||
|
getArtist: getArtistParameters,
|
||||||
|
getArtists: getArtistsParameters,
|
||||||
|
getGenre: getGenresParameters,
|
||||||
|
getGenres: getGenresParameters,
|
||||||
|
getPlaylist: getPlaylistParameters,
|
||||||
|
getPlaylists: getPlaylistsParameters,
|
||||||
|
getSong: getSongParameters,
|
||||||
|
getSongsByGenre: getSongsByGenreParameters,
|
||||||
|
getStarred: getStarredParameters,
|
||||||
randomSongList: randomSongListParameters,
|
randomSongList: randomSongListParameters,
|
||||||
removeFavorite: removeFavoriteParameters,
|
removeFavorite: removeFavoriteParameters,
|
||||||
scrobble: scrobbleParameters,
|
scrobble: scrobbleParameters,
|
||||||
|
@ -288,18 +513,35 @@ export const ssType = {
|
||||||
similarSongs: similarSongsParameters,
|
similarSongs: similarSongsParameters,
|
||||||
structuredLyrics: structuredLyricsParameters,
|
structuredLyrics: structuredLyricsParameters,
|
||||||
topSongsList: topSongsListParameters,
|
topSongsList: topSongsListParameters,
|
||||||
|
updatePlaylist: updatePlaylistParameters,
|
||||||
},
|
},
|
||||||
_response: {
|
_response: {
|
||||||
album,
|
album,
|
||||||
albumArtist,
|
albumArtist,
|
||||||
albumArtistList,
|
albumArtistList,
|
||||||
albumList,
|
albumList,
|
||||||
|
albumListEntry,
|
||||||
artistInfo,
|
artistInfo,
|
||||||
|
artistListEntry,
|
||||||
authenticate,
|
authenticate,
|
||||||
baseResponse,
|
baseResponse,
|
||||||
createFavorite,
|
createFavorite,
|
||||||
|
createPlaylist,
|
||||||
|
genre,
|
||||||
|
getAlbum,
|
||||||
|
getAlbumList2,
|
||||||
|
getArtist,
|
||||||
|
getArtists,
|
||||||
|
getGenres,
|
||||||
|
getPlaylist,
|
||||||
|
getPlaylists,
|
||||||
|
getSong,
|
||||||
|
getSongsByGenre,
|
||||||
|
getStarred,
|
||||||
musicFolderList,
|
musicFolderList,
|
||||||
ping,
|
ping,
|
||||||
|
playlist,
|
||||||
|
playlistListEntry,
|
||||||
randomSongList,
|
randomSongList,
|
||||||
removeFavorite,
|
removeFavorite,
|
||||||
scrobble,
|
scrobble,
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import reverse from 'lodash/reverse';
|
||||||
|
import shuffle from 'lodash/shuffle';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ServerFeatures } from './features-types';
|
import { ServerFeatures } from './features-types';
|
||||||
import { jfType } from './jellyfin/jellyfin-types';
|
import { jfType } from './jellyfin/jellyfin-types';
|
||||||
|
@ -128,7 +131,7 @@ export interface BasePaginatedResponse<T> {
|
||||||
error?: string | any;
|
error?: string | any;
|
||||||
items: T;
|
items: T;
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
totalRecordCount: number;
|
totalRecordCount: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthenticationResponse = {
|
export type AuthenticationResponse = {
|
||||||
|
@ -309,6 +312,11 @@ type BaseEndpointArgs = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface BaseQuery<T> {
|
||||||
|
sortBy: T;
|
||||||
|
sortOrder: SortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
// Genre List
|
// Genre List
|
||||||
export type GenreListResponse = BasePaginatedResponse<Genre[]> | null | undefined;
|
export type GenreListResponse = BasePaginatedResponse<Genre[]> | null | undefined;
|
||||||
|
|
||||||
|
@ -318,7 +326,7 @@ export enum GenreListSort {
|
||||||
NAME = 'name',
|
NAME = 'name',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GenreListQuery = {
|
export interface GenreListQuery extends BaseQuery<GenreListSort> {
|
||||||
_custom?: {
|
_custom?: {
|
||||||
jellyfin?: null;
|
jellyfin?: null;
|
||||||
navidrome?: null;
|
navidrome?: null;
|
||||||
|
@ -326,10 +334,8 @@ export type GenreListQuery = {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
musicFolderId?: string;
|
musicFolderId?: string;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
sortBy: GenreListSort;
|
|
||||||
sortOrder: SortOrder;
|
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
type GenreListSortMap = {
|
type GenreListSortMap = {
|
||||||
jellyfin: Record<GenreListSort, JFGenreListSort | undefined>;
|
jellyfin: Record<GenreListSort, JFGenreListSort | undefined>;
|
||||||
|
@ -370,22 +376,22 @@ export enum AlbumListSort {
|
||||||
YEAR = 'year',
|
YEAR = 'year',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AlbumListQuery = {
|
export interface AlbumListQuery extends BaseQuery<AlbumListSort> {
|
||||||
_custom?: {
|
_custom?: {
|
||||||
jellyfin?: Partial<z.infer<typeof jfType._parameters.albumList>> & {
|
jellyfin?: Partial<z.infer<typeof jfType._parameters.albumList>>;
|
||||||
maxYear?: number;
|
|
||||||
minYear?: number;
|
|
||||||
};
|
|
||||||
navidrome?: Partial<z.infer<typeof ndType._parameters.albumList>>;
|
navidrome?: Partial<z.infer<typeof ndType._parameters.albumList>>;
|
||||||
};
|
};
|
||||||
artistIds?: string[];
|
artistIds?: string[];
|
||||||
|
compilation?: boolean;
|
||||||
|
favorite?: boolean;
|
||||||
|
genres?: string[];
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
maxYear?: number;
|
||||||
|
minYear?: number;
|
||||||
musicFolderId?: string;
|
musicFolderId?: string;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
sortBy: AlbumListSort;
|
|
||||||
sortOrder: SortOrder;
|
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type AlbumListArgs = { query: AlbumListQuery } & BaseEndpointArgs;
|
export type AlbumListArgs = { query: AlbumListQuery } & BaseEndpointArgs;
|
||||||
|
|
||||||
|
@ -481,24 +487,23 @@ export enum SongListSort {
|
||||||
YEAR = 'year',
|
YEAR = 'year',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SongListQuery = {
|
export interface SongListQuery extends BaseQuery<SongListSort> {
|
||||||
_custom?: {
|
_custom?: {
|
||||||
jellyfin?: Partial<z.infer<typeof jfType._parameters.songList>> & {
|
jellyfin?: Partial<z.infer<typeof jfType._parameters.songList>>;
|
||||||
maxYear?: number;
|
|
||||||
minYear?: number;
|
|
||||||
};
|
|
||||||
navidrome?: Partial<z.infer<typeof ndType._parameters.songList>>;
|
navidrome?: Partial<z.infer<typeof ndType._parameters.songList>>;
|
||||||
};
|
};
|
||||||
albumIds?: string[];
|
albumIds?: string[];
|
||||||
artistIds?: string[];
|
artistIds?: string[];
|
||||||
|
favorite?: boolean;
|
||||||
|
genreIds?: string[];
|
||||||
imageSize?: number;
|
imageSize?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
maxYear?: number;
|
||||||
|
minYear?: number;
|
||||||
musicFolderId?: string;
|
musicFolderId?: string;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
sortBy: SongListSort;
|
|
||||||
sortOrder: SortOrder;
|
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type SongListArgs = { query: SongListQuery } & BaseEndpointArgs;
|
export type SongListArgs = { query: SongListQuery } & BaseEndpointArgs;
|
||||||
|
|
||||||
|
@ -595,7 +600,7 @@ export enum AlbumArtistListSort {
|
||||||
SONG_COUNT = 'songCount',
|
SONG_COUNT = 'songCount',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AlbumArtistListQuery = {
|
export interface AlbumArtistListQuery extends BaseQuery<AlbumArtistListSort> {
|
||||||
_custom?: {
|
_custom?: {
|
||||||
jellyfin?: Partial<z.infer<typeof jfType._parameters.albumArtistList>>;
|
jellyfin?: Partial<z.infer<typeof jfType._parameters.albumArtistList>>;
|
||||||
navidrome?: Partial<z.infer<typeof ndType._parameters.albumArtistList>>;
|
navidrome?: Partial<z.infer<typeof ndType._parameters.albumArtistList>>;
|
||||||
|
@ -603,10 +608,8 @@ export type AlbumArtistListQuery = {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
musicFolderId?: string;
|
musicFolderId?: string;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
sortBy: AlbumArtistListSort;
|
|
||||||
sortOrder: SortOrder;
|
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type AlbumArtistListArgs = { query: AlbumArtistListQuery } & BaseEndpointArgs;
|
export type AlbumArtistListArgs = { query: AlbumArtistListQuery } & BaseEndpointArgs;
|
||||||
|
|
||||||
|
@ -683,17 +686,15 @@ export enum ArtistListSort {
|
||||||
SONG_COUNT = 'songCount',
|
SONG_COUNT = 'songCount',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ArtistListQuery = {
|
export interface ArtistListQuery extends BaseQuery<ArtistListSort> {
|
||||||
_custom?: {
|
_custom?: {
|
||||||
jellyfin?: Partial<z.infer<typeof jfType._parameters.albumArtistList>>;
|
jellyfin?: Partial<z.infer<typeof jfType._parameters.albumArtistList>>;
|
||||||
navidrome?: Partial<z.infer<typeof ndType._parameters.albumArtistList>>;
|
navidrome?: Partial<z.infer<typeof ndType._parameters.albumArtistList>>;
|
||||||
};
|
};
|
||||||
limit?: number;
|
limit?: number;
|
||||||
musicFolderId?: string;
|
musicFolderId?: string;
|
||||||
sortBy: ArtistListSort;
|
|
||||||
sortOrder: SortOrder;
|
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type ArtistListArgs = { query: ArtistListQuery } & BaseEndpointArgs;
|
export type ArtistListArgs = { query: ArtistListQuery } & BaseEndpointArgs;
|
||||||
|
|
||||||
|
@ -879,17 +880,15 @@ export enum PlaylistListSort {
|
||||||
UPDATED_AT = 'updatedAt',
|
UPDATED_AT = 'updatedAt',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlaylistListQuery = {
|
export interface PlaylistListQuery extends BaseQuery<PlaylistListSort> {
|
||||||
_custom?: {
|
_custom?: {
|
||||||
jellyfin?: Partial<z.infer<typeof jfType._parameters.playlistList>>;
|
jellyfin?: Partial<z.infer<typeof jfType._parameters.playlistList>>;
|
||||||
navidrome?: Partial<z.infer<typeof ndType._parameters.playlistList>>;
|
navidrome?: Partial<z.infer<typeof ndType._parameters.playlistList>>;
|
||||||
};
|
};
|
||||||
limit?: number;
|
limit?: number;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
sortBy: PlaylistListSort;
|
|
||||||
sortOrder: SortOrder;
|
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type PlaylistListArgs = { query: PlaylistListQuery } & BaseEndpointArgs;
|
export type PlaylistListArgs = { query: PlaylistListQuery } & BaseEndpointArgs;
|
||||||
|
|
||||||
|
@ -963,7 +962,7 @@ export enum UserListSort {
|
||||||
NAME = 'name',
|
NAME = 'name',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserListQuery = {
|
export interface UserListQuery extends BaseQuery<UserListSort> {
|
||||||
_custom?: {
|
_custom?: {
|
||||||
navidrome?: {
|
navidrome?: {
|
||||||
owner_id?: string;
|
owner_id?: string;
|
||||||
|
@ -971,10 +970,8 @@ export type UserListQuery = {
|
||||||
};
|
};
|
||||||
limit?: number;
|
limit?: number;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
sortBy: UserListSort;
|
|
||||||
sortOrder: SortOrder;
|
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type UserListArgs = { query: UserListQuery } & BaseEndpointArgs;
|
export type UserListArgs = { query: UserListQuery } & BaseEndpointArgs;
|
||||||
|
|
||||||
|
@ -1228,3 +1225,223 @@ export type TranscodingQuery = {
|
||||||
export type TranscodingArgs = {
|
export type TranscodingArgs = {
|
||||||
query: TranscodingQuery;
|
query: TranscodingQuery;
|
||||||
} & BaseEndpointArgs;
|
} & BaseEndpointArgs;
|
||||||
|
|
||||||
|
export type ControllerEndpoint = {
|
||||||
|
addToPlaylist: (args: AddToPlaylistArgs) => Promise<AddToPlaylistResponse>;
|
||||||
|
authenticate: (
|
||||||
|
url: string,
|
||||||
|
body: { legacy?: boolean; password: string; username: string },
|
||||||
|
) => Promise<AuthenticationResponse>;
|
||||||
|
createFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
|
||||||
|
createPlaylist: (args: CreatePlaylistArgs) => Promise<CreatePlaylistResponse>;
|
||||||
|
deleteFavorite: (args: FavoriteArgs) => Promise<FavoriteResponse>;
|
||||||
|
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
|
||||||
|
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
|
||||||
|
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
|
||||||
|
getAlbumArtistListCount: (args: AlbumArtistListArgs) => Promise<number>;
|
||||||
|
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
|
||||||
|
getAlbumList: (args: AlbumListArgs) => Promise<AlbumListResponse>;
|
||||||
|
getAlbumListCount: (args: AlbumListArgs) => Promise<number>;
|
||||||
|
// getArtistInfo?: (args: any) => void;
|
||||||
|
// getArtistList?: (args: ArtistListArgs) => Promise<ArtistListResponse>;
|
||||||
|
getDownloadUrl: (args: DownloadArgs) => string;
|
||||||
|
getGenreList: (args: GenreListArgs) => Promise<GenreListResponse>;
|
||||||
|
getLyrics?: (args: LyricsArgs) => Promise<LyricsResponse>;
|
||||||
|
getMusicFolderList: (args: MusicFolderListArgs) => Promise<MusicFolderListResponse>;
|
||||||
|
getPlaylistDetail: (args: PlaylistDetailArgs) => Promise<PlaylistDetailResponse>;
|
||||||
|
getPlaylistList: (args: PlaylistListArgs) => Promise<PlaylistListResponse>;
|
||||||
|
getPlaylistListCount: (args: PlaylistListArgs) => Promise<number>;
|
||||||
|
getPlaylistSongList: (args: PlaylistSongListArgs) => Promise<SongListResponse>;
|
||||||
|
getRandomSongList: (args: RandomSongListArgs) => Promise<SongListResponse>;
|
||||||
|
getServerInfo: (args: ServerInfoArgs) => Promise<ServerInfo>;
|
||||||
|
getSimilarSongs: (args: SimilarSongsArgs) => Promise<Song[]>;
|
||||||
|
getSongDetail: (args: SongDetailArgs) => Promise<SongDetailResponse>;
|
||||||
|
getSongList: (args: SongListArgs) => Promise<SongListResponse>;
|
||||||
|
getSongListCount: (args: SongListArgs) => Promise<number>;
|
||||||
|
getStructuredLyrics?: (args: StructuredLyricsArgs) => Promise<StructuredLyric[]>;
|
||||||
|
getTopSongs: (args: TopSongListArgs) => Promise<TopSongListResponse>;
|
||||||
|
getTranscodingUrl: (args: TranscodingArgs) => string;
|
||||||
|
getUserList?: (args: UserListArgs) => Promise<UserListResponse>;
|
||||||
|
movePlaylistItem?: (args: MoveItemArgs) => Promise<void>;
|
||||||
|
removeFromPlaylist: (args: RemoveFromPlaylistArgs) => Promise<RemoveFromPlaylistResponse>;
|
||||||
|
scrobble: (args: ScrobbleArgs) => Promise<ScrobbleResponse>;
|
||||||
|
search: (args: SearchArgs) => Promise<SearchResponse>;
|
||||||
|
setRating?: (args: SetRatingArgs) => Promise<RatingResponse>;
|
||||||
|
shareItem?: (args: ShareItemArgs) => Promise<ShareItemResponse>;
|
||||||
|
updatePlaylist: (args: UpdatePlaylistArgs) => Promise<UpdatePlaylistResponse>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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.RECENTLY_ADDED:
|
||||||
|
results = orderBy(results, ['createdAt'], [order]);
|
||||||
|
break;
|
||||||
|
case AlbumListSort.RECENTLY_PLAYED:
|
||||||
|
results = orderBy(results, ['lastPlayedAt'], [order]);
|
||||||
|
break;
|
||||||
|
case AlbumListSort.RATING:
|
||||||
|
results = orderBy(results, ['userRating'], [order]);
|
||||||
|
break;
|
||||||
|
case AlbumListSort.YEAR:
|
||||||
|
results = orderBy(results, ['releaseYear'], [order]);
|
||||||
|
break;
|
||||||
|
case AlbumListSort.SONG_COUNT:
|
||||||
|
results = orderBy(results, ['songCount'], [order]);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sortSongList = (songs: QueueSong[], 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,
|
||||||
|
['albumArtist', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
|
[order, order, 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SongListSort.ARTIST:
|
||||||
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['artist', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
|
||||||
|
[order, order, 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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, ['created'], [order]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SongListSort.YEAR:
|
||||||
|
results = orderBy(
|
||||||
|
results,
|
||||||
|
['year', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
|
||||||
|
[order, 'asc', 'asc', 'asc'],
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sortAlbumArtistList = (
|
||||||
|
artists: AlbumArtist[],
|
||||||
|
sortBy: AlbumArtistListSort,
|
||||||
|
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.NAME:
|
||||||
|
results = orderBy(artists, [(v) => v.name.toLowerCase()], [order]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AlbumArtistListSort.FAVORITED:
|
||||||
|
results = orderBy(artists, ['starred'], [order]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AlbumArtistListSort.RATING:
|
||||||
|
results = orderBy(artists, ['userRating'], [order]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
|
@ -294,7 +294,7 @@ export const PLAYLIST_CARD_ROWS: { [key: string]: CardRow<Playlist> } = {
|
||||||
name: {
|
name: {
|
||||||
property: 'name',
|
property: 'name',
|
||||||
route: {
|
route: {
|
||||||
route: AppRoute.PLAYLISTS_DETAIL,
|
route: AppRoute.PLAYLISTS_DETAIL_SONGS,
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'playlistId' }],
|
slugs: [{ idProperty: 'id', slugProperty: 'playlistId' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
IDatasource,
|
IDatasource,
|
||||||
PaginationChangedEvent,
|
PaginationChangedEvent,
|
||||||
RowDoubleClickedEvent,
|
RowDoubleClickedEvent,
|
||||||
RowModelType,
|
|
||||||
} from '@ag-grid-community/core';
|
} from '@ag-grid-community/core';
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
||||||
|
@ -16,7 +15,12 @@ import orderBy from 'lodash/orderBy';
|
||||||
import { generatePath, useNavigate } from 'react-router';
|
import { generatePath, useNavigate } from 'react-router';
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { QueryPagination, queryKeys } from '/@/renderer/api/query-keys';
|
import { QueryPagination, queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { BasePaginatedResponse, LibraryItem, ServerListItem } from '/@/renderer/api/types';
|
import {
|
||||||
|
BasePaginatedResponse,
|
||||||
|
BaseQuery,
|
||||||
|
LibraryItem,
|
||||||
|
ServerListItem,
|
||||||
|
} from '/@/renderer/api/types';
|
||||||
import { getColumnDefs, VirtualTableProps } from '/@/renderer/components/virtual-table';
|
import { getColumnDefs, VirtualTableProps } from '/@/renderer/components/virtual-table';
|
||||||
import { SetContextMenuItems, useHandleTableContextMenu } from '/@/renderer/features/context-menu';
|
import { SetContextMenuItems, useHandleTableContextMenu } from '/@/renderer/features/context-menu';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
@ -34,6 +38,7 @@ interface UseAgGridProps<TFilter> {
|
||||||
columnType?: 'albumDetail' | 'generic';
|
columnType?: 'albumDetail' | 'generic';
|
||||||
contextMenu: SetContextMenuItems;
|
contextMenu: SetContextMenuItems;
|
||||||
customFilters?: Partial<TFilter>;
|
customFilters?: Partial<TFilter>;
|
||||||
|
isClientSide?: boolean;
|
||||||
isClientSideSort?: boolean;
|
isClientSideSort?: boolean;
|
||||||
isSearchParams?: boolean;
|
isSearchParams?: boolean;
|
||||||
itemCount?: number;
|
itemCount?: number;
|
||||||
|
@ -43,7 +48,9 @@ interface UseAgGridProps<TFilter> {
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useVirtualTable = <TFilter>({
|
const BLOCK_SIZE = 500;
|
||||||
|
|
||||||
|
export const useVirtualTable = <TFilter extends BaseQuery<any>>({
|
||||||
server,
|
server,
|
||||||
tableRef,
|
tableRef,
|
||||||
pageKey,
|
pageKey,
|
||||||
|
@ -52,13 +59,14 @@ export const useVirtualTable = <TFilter>({
|
||||||
itemCount,
|
itemCount,
|
||||||
customFilters,
|
customFilters,
|
||||||
isSearchParams,
|
isSearchParams,
|
||||||
|
isClientSide,
|
||||||
isClientSideSort,
|
isClientSideSort,
|
||||||
columnType,
|
columnType,
|
||||||
}: UseAgGridProps<TFilter>) => {
|
}: UseAgGridProps<TFilter>) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { setTable, setTablePagination } = useListStoreActions();
|
const { setTable, setTablePagination } = useListStoreActions();
|
||||||
const properties = useListStoreByKey({ filter: customFilters, key: pageKey });
|
const properties = useListStoreByKey<TFilter>({ filter: customFilters, key: pageKey });
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const scrollOffset = searchParams.get('scrollOffset');
|
const scrollOffset = searchParams.get('scrollOffset');
|
||||||
|
@ -182,6 +190,19 @@ export const useVirtualTable = <TFilter>({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (results.totalRecordCount === null) {
|
||||||
|
const hasMoreRows = results?.items?.length === BLOCK_SIZE;
|
||||||
|
const lastRowIndex = hasMoreRows
|
||||||
|
? undefined
|
||||||
|
: params.startRow + results.items.length;
|
||||||
|
|
||||||
|
params.successCallback(
|
||||||
|
results?.items || [],
|
||||||
|
hasMoreRows ? undefined : lastRowIndex,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
params.successCallback(results?.items || [], results?.totalRecordCount || 0);
|
params.successCallback(results?.items || [], results?.totalRecordCount || 0);
|
||||||
},
|
},
|
||||||
rowCount: undefined,
|
rowCount: undefined,
|
||||||
|
@ -321,6 +342,7 @@ export const useVirtualTable = <TFilter>({
|
||||||
alwaysShowHorizontalScroll: true,
|
alwaysShowHorizontalScroll: true,
|
||||||
autoFitColumns: properties.table.autoFit,
|
autoFitColumns: properties.table.autoFit,
|
||||||
blockLoadDebounceMillis: 200,
|
blockLoadDebounceMillis: 200,
|
||||||
|
cacheBlockSize: BLOCK_SIZE,
|
||||||
getRowId: (data: GetRowIdParams<any>) => data.data.id,
|
getRowId: (data: GetRowIdParams<any>) => data.data.id,
|
||||||
infiniteInitialRowCount: itemCount || 100,
|
infiniteInitialRowCount: itemCount || 100,
|
||||||
pagination: isPaginationEnabled,
|
pagination: isPaginationEnabled,
|
||||||
|
@ -335,10 +357,11 @@ export const useVirtualTable = <TFilter>({
|
||||||
: undefined,
|
: undefined,
|
||||||
rowBuffer: 20,
|
rowBuffer: 20,
|
||||||
rowHeight: properties.table.rowHeight || 40,
|
rowHeight: properties.table.rowHeight || 40,
|
||||||
rowModelType: 'infinite' as RowModelType,
|
rowModelType: isClientSide ? 'clientSide' : 'infinite',
|
||||||
suppressRowDrag: true,
|
suppressRowDrag: true,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
|
isClientSide,
|
||||||
isPaginationEnabled,
|
isPaginationEnabled,
|
||||||
isSearchParams,
|
isSearchParams,
|
||||||
itemCount,
|
itemCount,
|
||||||
|
@ -370,7 +393,9 @@ export const useVirtualTable = <TFilter>({
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case LibraryItem.PLAYLIST:
|
case LibraryItem.PLAYLIST:
|
||||||
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL, { playlistId: e.data.id }));
|
navigate(
|
||||||
|
generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: e.data.id }),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -11,7 +11,13 @@ import { generatePath, useParams } from 'react-router';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { AlbumListSort, LibraryItem, QueueSong, SortOrder } from '/@/renderer/api/types';
|
import {
|
||||||
|
AlbumListQuery,
|
||||||
|
AlbumListSort,
|
||||||
|
LibraryItem,
|
||||||
|
QueueSong,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/renderer/api/types';
|
||||||
import { Button, Popover, Spoiler } from '/@/renderer/components';
|
import { Button, Popover, Spoiler } from '/@/renderer/components';
|
||||||
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel';
|
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel';
|
||||||
import {
|
import {
|
||||||
|
@ -164,13 +170,12 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
|
||||||
query: {
|
query: {
|
||||||
_custom: {
|
_custom: {
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
AlbumArtistIds: detailQuery?.data?.albumArtists[0]?.id,
|
|
||||||
ExcludeItemIds: detailQuery?.data?.id,
|
ExcludeItemIds: detailQuery?.data?.id,
|
||||||
},
|
},
|
||||||
navidrome: {
|
|
||||||
artist_id: detailQuery?.data?.albumArtists[0]?.id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
artistIds: detailQuery?.data?.albumArtists.length
|
||||||
|
? [detailQuery?.data?.albumArtists[0].id]
|
||||||
|
: undefined,
|
||||||
limit: 15,
|
limit: 15,
|
||||||
sortBy: AlbumListSort.YEAR,
|
sortBy: AlbumListSort.YEAR,
|
||||||
sortOrder: SortOrder.DESC,
|
sortOrder: SortOrder.DESC,
|
||||||
|
@ -179,15 +184,8 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const relatedAlbumGenresRequest = {
|
const relatedAlbumGenresRequest: AlbumListQuery = {
|
||||||
_custom: {
|
genres: detailQuery.data?.genres.length ? [detailQuery.data.genres[0].id] : undefined,
|
||||||
jellyfin: {
|
|
||||||
GenreIds: detailQuery?.data?.genres?.[0]?.id,
|
|
||||||
},
|
|
||||||
navidrome: {
|
|
||||||
genre_id: detailQuery?.data?.genres?.[0]?.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
limit: 15,
|
limit: 15,
|
||||||
sortBy: AlbumListSort.RANDOM,
|
sortBy: AlbumListSort.RANDOM,
|
||||||
sortOrder: SortOrder.ASC,
|
sortOrder: SortOrder.ASC,
|
||||||
|
|
|
@ -29,7 +29,7 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
const { pageKey, customFilters, id } = useListContext();
|
const { pageKey, customFilters, id } = useListContext();
|
||||||
const { grid, display, filter } = useListStoreByKey({ key: pageKey });
|
const { grid, display, filter } = useListStoreByKey<AlbumListQuery>({ key: pageKey });
|
||||||
const { setGrid } = useListStoreActions();
|
const { setGrid } = useListStoreActions();
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
@ -162,9 +162,9 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
|
||||||
|
|
||||||
const query: AlbumListQuery = {
|
const query: AlbumListQuery = {
|
||||||
limit: take,
|
limit: take,
|
||||||
startIndex: skip,
|
|
||||||
...filter,
|
...filter,
|
||||||
...customFilters,
|
...customFilters,
|
||||||
|
startIndex: skip,
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryKey = queryKeys.albums.list(server?.id || '', query, id);
|
const queryKey = queryKeys.albums.list(server?.id || '', query, id);
|
||||||
|
|
|
@ -15,13 +15,20 @@ import {
|
||||||
RiSettings3Fill,
|
RiSettings3Fill,
|
||||||
} from 'react-icons/ri';
|
} from 'react-icons/ri';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { AlbumListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types';
|
import {
|
||||||
|
AlbumListQuery,
|
||||||
|
AlbumListSort,
|
||||||
|
LibraryItem,
|
||||||
|
ServerType,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/renderer/api/types';
|
||||||
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
|
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters';
|
import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters';
|
||||||
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
|
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
|
||||||
|
import { SubsonicAlbumFilters } from '/@/renderer/features/albums/components/subsonic-album-filters';
|
||||||
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared';
|
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh';
|
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh';
|
||||||
|
@ -139,26 +146,74 @@ const FILTERS = {
|
||||||
value: AlbumListSort.YEAR,
|
value: AlbumListSort.YEAR,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
subsonic: [
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.ALBUM_ARTIST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.PLAY_COUNT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.NAME,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.RANDOM,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.RECENTLY_ADDED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.RECENTLY_PLAYED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.favorited', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.FAVORITED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumListSort.YEAR,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AlbumListHeaderFiltersProps {
|
interface AlbumListHeaderFiltersProps {
|
||||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||||
|
itemCount: number | undefined;
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFiltersProps) => {
|
export const AlbumListHeaderFilters = ({
|
||||||
|
gridRef,
|
||||||
|
itemCount,
|
||||||
|
tableRef,
|
||||||
|
}: AlbumListHeaderFiltersProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { pageKey, customFilters, handlePlay } = useListContext();
|
const { pageKey, customFilters, handlePlay } = useListContext();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { setFilter, setTable, setGrid, setDisplayType } = useListStoreActions();
|
const { setFilter, setTable, setGrid, setDisplayType } = useListStoreActions();
|
||||||
const { display, filter, table, grid } = useListStoreByKey({
|
const { display, filter, table, grid } = useListStoreByKey<AlbumListQuery>({
|
||||||
filter: customFilters,
|
filter: customFilters,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
});
|
});
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
|
|
||||||
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
|
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
|
||||||
|
itemCount,
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
server,
|
server,
|
||||||
});
|
});
|
||||||
|
@ -191,27 +246,35 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenFiltersModal = () => {
|
const handleOpenFiltersModal = () => {
|
||||||
|
let FilterComponent;
|
||||||
|
|
||||||
|
switch (server?.type) {
|
||||||
|
case ServerType.NAVIDROME:
|
||||||
|
FilterComponent = NavidromeAlbumFilters;
|
||||||
|
break;
|
||||||
|
case ServerType.JELLYFIN:
|
||||||
|
FilterComponent = JellyfinAlbumFilters;
|
||||||
|
break;
|
||||||
|
case ServerType.SUBSONIC:
|
||||||
|
FilterComponent = SubsonicAlbumFilters;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FilterComponent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
children: (
|
children: (
|
||||||
<>
|
<FilterComponent
|
||||||
{server?.type === ServerType.NAVIDROME ? (
|
customFilters={customFilters}
|
||||||
<NavidromeAlbumFilters
|
disableArtistFilter={!!customFilters}
|
||||||
customFilters={customFilters}
|
pageKey={pageKey}
|
||||||
disableArtistFilter={!!customFilters}
|
serverId={server?.id}
|
||||||
pageKey={pageKey}
|
onFilterChange={onFilterChange}
|
||||||
serverId={server?.id}
|
/>
|
||||||
onFilterChange={onFilterChange}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<JellyfinAlbumFilters
|
|
||||||
customFilters={customFilters}
|
|
||||||
disableArtistFilter={!!customFilters}
|
|
||||||
pageKey={pageKey}
|
|
||||||
serverId={server?.id}
|
|
||||||
onFilterChange={onFilterChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
),
|
),
|
||||||
title: 'Album Filters',
|
title: 'Album Filters',
|
||||||
});
|
});
|
||||||
|
@ -347,8 +410,20 @@ export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFil
|
||||||
filter?._custom?.jellyfin &&
|
filter?._custom?.jellyfin &&
|
||||||
Object.values(filter?._custom?.jellyfin).some((value) => value !== undefined);
|
Object.values(filter?._custom?.jellyfin).some((value) => value !== undefined);
|
||||||
|
|
||||||
return isNavidromeFilterApplied || isJellyfinFilterApplied;
|
const isSubsonicFilterApplied =
|
||||||
}, [filter?._custom?.jellyfin, filter?._custom?.navidrome, server?.type]);
|
server?.type === ServerType.SUBSONIC &&
|
||||||
|
(filter.maxYear || filter.minYear || filter.genres?.length || filter.favorite);
|
||||||
|
|
||||||
|
return isNavidromeFilterApplied || isJellyfinFilterApplied || isSubsonicFilterApplied;
|
||||||
|
}, [
|
||||||
|
filter?._custom?.jellyfin,
|
||||||
|
filter?._custom?.navidrome,
|
||||||
|
filter.favorite,
|
||||||
|
filter.genres?.length,
|
||||||
|
filter.maxYear,
|
||||||
|
filter.minYear,
|
||||||
|
server?.type,
|
||||||
|
]);
|
||||||
|
|
||||||
const isFolderFilterApplied = useMemo(() => {
|
const isFolderFilterApplied = useMemo(() => {
|
||||||
return filter.musicFolderId !== undefined;
|
return filter.musicFolderId !== undefined;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
|
||||||
import { Flex, Group, Stack } from '@mantine/core';
|
import { Flex, Group, Stack } from '@mantine/core';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
import { AlbumListQuery, LibraryItem } from '/@/renderer/api/types';
|
||||||
import { PageHeader, SearchInput } from '/@/renderer/components';
|
import { PageHeader, SearchInput } from '/@/renderer/components';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
|
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
|
||||||
|
@ -33,8 +33,9 @@ export const AlbumListHeader = ({
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
const genreRef = useRef<string>();
|
const genreRef = useRef<string>();
|
||||||
const { filter, handlePlay, refresh, search } = useDisplayRefresh({
|
const { filter, handlePlay, refresh, search } = useDisplayRefresh<AlbumListQuery>({
|
||||||
gridRef,
|
gridRef,
|
||||||
|
itemCount,
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
server,
|
server,
|
||||||
tableRef,
|
tableRef,
|
||||||
|
@ -90,6 +91,7 @@ export const AlbumListHeader = ({
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<AlbumListHeaderFilters
|
<AlbumListHeaderFilters
|
||||||
gridRef={gridRef}
|
gridRef={gridRef}
|
||||||
|
itemCount={itemCount}
|
||||||
tableRef={tableRef}
|
tableRef={tableRef}
|
||||||
/>
|
/>
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
|
|
|
@ -3,7 +3,13 @@ import { Divider, Group, Stack } from '@mantine/core';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useListFilterByKey } from '../../../store/list.store';
|
import { useListFilterByKey } from '../../../store/list.store';
|
||||||
import { AlbumArtistListSort, GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
import {
|
||||||
|
AlbumArtistListSort,
|
||||||
|
AlbumListQuery,
|
||||||
|
GenreListSort,
|
||||||
|
LibraryItem,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/renderer/api/types';
|
||||||
import { MultiSelect, NumberInput, SpinnerIcon, Switch, Text } from '/@/renderer/components';
|
import { MultiSelect, NumberInput, SpinnerIcon, Switch, Text } from '/@/renderer/components';
|
||||||
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
|
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
|
||||||
import { useGenreList } from '/@/renderer/features/genres';
|
import { useGenreList } from '/@/renderer/features/genres';
|
||||||
|
@ -25,7 +31,7 @@ export const JellyfinAlbumFilters = ({
|
||||||
serverId,
|
serverId,
|
||||||
}: JellyfinAlbumFiltersProps) => {
|
}: JellyfinAlbumFiltersProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const filter = useListFilterByKey({ key: pageKey });
|
const filter = useListFilterByKey<AlbumListQuery>({ key: pageKey });
|
||||||
const { setFilter } = useListStoreActions();
|
const { setFilter } = useListStoreActions();
|
||||||
|
|
||||||
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
|
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
|
||||||
|
@ -47,10 +53,6 @@ export const JellyfinAlbumFilters = ({
|
||||||
}));
|
}));
|
||||||
}, [genreListQuery.data]);
|
}, [genreListQuery.data]);
|
||||||
|
|
||||||
const selectedGenres = useMemo(() => {
|
|
||||||
return filter?._custom?.jellyfin?.GenreIds?.split(',');
|
|
||||||
}, [filter?._custom?.jellyfin?.GenreIds]);
|
|
||||||
|
|
||||||
const toggleFilters = [
|
const toggleFilters = [
|
||||||
{
|
{
|
||||||
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||||
|
@ -58,20 +60,15 @@ export const JellyfinAlbumFilters = ({
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: filter?._custom,
|
||||||
...filter?._custom,
|
favorite: e.currentTarget.checked ? true : undefined,
|
||||||
jellyfin: {
|
|
||||||
...filter?._custom?.jellyfin,
|
|
||||||
IsFavorite: e.currentTarget.checked ? true : undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
onFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
},
|
},
|
||||||
value: filter?._custom?.jellyfin?.IsFavorite,
|
value: filter?.favorite,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -80,13 +77,8 @@ export const JellyfinAlbumFilters = ({
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: filter?._custom,
|
||||||
...filter?._custom,
|
minYear: e === '' ? undefined : (e as number),
|
||||||
jellyfin: {
|
|
||||||
...filter?._custom?.jellyfin,
|
|
||||||
minYear: e === '' ? undefined : (e as number),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
|
@ -99,13 +91,8 @@ export const JellyfinAlbumFilters = ({
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: filter?._custom,
|
||||||
...filter?._custom,
|
maxYear: e === '' ? undefined : (e as number),
|
||||||
jellyfin: {
|
|
||||||
...filter?._custom?.jellyfin,
|
|
||||||
maxYear: e === '' ? undefined : (e as number),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
|
@ -114,17 +101,11 @@ export const JellyfinAlbumFilters = ({
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
const handleGenresFilter = debounce((e: string[] | undefined) => {
|
const handleGenresFilter = debounce((e: string[] | undefined) => {
|
||||||
const genreFilterString = e?.length ? e.join(',') : undefined;
|
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: filter?._custom,
|
||||||
...filter?._custom,
|
genres: e,
|
||||||
jellyfin: {
|
|
||||||
...filter?._custom?.jellyfin,
|
|
||||||
GenreIds: genreFilterString,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
|
@ -157,17 +138,11 @@ export const JellyfinAlbumFilters = ({
|
||||||
}, [albumArtistListQuery?.data?.items]);
|
}, [albumArtistListQuery?.data?.items]);
|
||||||
|
|
||||||
const handleAlbumArtistFilter = (e: string[] | null) => {
|
const handleAlbumArtistFilter = (e: string[] | null) => {
|
||||||
const albumArtistFilterString = e?.length ? e.join(',') : undefined;
|
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: filter?._custom,
|
||||||
...filter?._custom,
|
artistIds: e || undefined,
|
||||||
jellyfin: {
|
|
||||||
...filter?._custom?.jellyfin,
|
|
||||||
AlbumArtistIds: albumArtistFilterString,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
|
@ -193,21 +168,21 @@ export const JellyfinAlbumFilters = ({
|
||||||
<Divider my="0.5rem" />
|
<Divider my="0.5rem" />
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
defaultValue={filter?._custom?.jellyfin?.minYear}
|
defaultValue={filter?.minYear}
|
||||||
hideControls={false}
|
hideControls={false}
|
||||||
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
||||||
max={2300}
|
max={2300}
|
||||||
min={1700}
|
min={1700}
|
||||||
required={!!filter?._custom?.jellyfin?.maxYear}
|
required={!!filter?.maxYear}
|
||||||
onChange={(e) => handleMinYearFilter(e)}
|
onChange={(e) => handleMinYearFilter(e)}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
defaultValue={filter?._custom?.jellyfin?.maxYear}
|
defaultValue={filter?.maxYear}
|
||||||
hideControls={false}
|
hideControls={false}
|
||||||
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
||||||
max={2300}
|
max={2300}
|
||||||
min={1700}
|
min={1700}
|
||||||
required={!!filter?._custom?.jellyfin?.minYear}
|
required={!!filter?.minYear}
|
||||||
onChange={(e) => handleMaxYearFilter(e)}
|
onChange={(e) => handleMaxYearFilter(e)}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
@ -216,7 +191,7 @@ export const JellyfinAlbumFilters = ({
|
||||||
clearable
|
clearable
|
||||||
searchable
|
searchable
|
||||||
data={genreList}
|
data={genreList}
|
||||||
defaultValue={selectedGenres}
|
defaultValue={filter.genres}
|
||||||
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
|
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
|
||||||
onChange={handleGenresFilter}
|
onChange={handleGenresFilter}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -5,7 +5,13 @@ import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/rend
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { useGenreList } from '/@/renderer/features/genres';
|
import { useGenreList } from '/@/renderer/features/genres';
|
||||||
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
|
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
|
||||||
import { AlbumArtistListSort, GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
import {
|
||||||
|
AlbumArtistListSort,
|
||||||
|
AlbumListQuery,
|
||||||
|
GenreListSort,
|
||||||
|
LibraryItem,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/renderer/api/types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface NavidromeAlbumFiltersProps {
|
interface NavidromeAlbumFiltersProps {
|
||||||
|
@ -24,7 +30,7 @@ export const NavidromeAlbumFilters = ({
|
||||||
serverId,
|
serverId,
|
||||||
}: NavidromeAlbumFiltersProps) => {
|
}: NavidromeAlbumFiltersProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { filter } = useListStoreByKey({ key: pageKey });
|
const { filter } = useListStoreByKey<AlbumListQuery>({ key: pageKey });
|
||||||
const { setFilter } = useListStoreActions();
|
const { setFilter } = useListStoreActions();
|
||||||
|
|
||||||
const genreListQuery = useGenreList({
|
const genreListQuery = useGenreList({
|
||||||
|
@ -48,13 +54,8 @@ export const NavidromeAlbumFilters = ({
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: filter._custom,
|
||||||
...filter._custom,
|
genres: e ? [e] : undefined,
|
||||||
navidrome: {
|
|
||||||
...filter._custom?.navidrome,
|
|
||||||
genre_id: e || undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
|
@ -90,20 +91,15 @@ export const NavidromeAlbumFilters = ({
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: filter._custom,
|
||||||
...filter._custom,
|
favorite: e.currentTarget.checked ? true : undefined,
|
||||||
navidrome: {
|
|
||||||
...filter._custom?.navidrome,
|
|
||||||
starred: e.currentTarget.checked ? true : undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
onFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
},
|
},
|
||||||
value: filter._custom?.navidrome?.starred,
|
value: filter.favorite,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
|
label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
|
||||||
|
@ -111,20 +107,15 @@ export const NavidromeAlbumFilters = ({
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: filter._custom,
|
||||||
...filter._custom,
|
compilation: e.currentTarget.checked ? true : undefined,
|
||||||
navidrome: {
|
|
||||||
...filter._custom?.navidrome,
|
|
||||||
compilation: e.currentTarget.checked ? true : undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
onFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
},
|
},
|
||||||
value: filter._custom?.navidrome?.compilation,
|
value: filter.compilation,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('filter.isRecentlyPlayed', { postProcess: 'sentenceCase' }),
|
label: t('filter.isRecentlyPlayed', { postProcess: 'sentenceCase' }),
|
||||||
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { Divider, Group, Stack } from '@mantine/core';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { ChangeEvent, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { AlbumListQuery, GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
||||||
|
import { NumberInput, Select, Switch, Text } from '/@/renderer/components';
|
||||||
|
import { useGenreList } from '/@/renderer/features/genres';
|
||||||
|
import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
|
||||||
|
|
||||||
|
interface SubsonicAlbumFiltersProps {
|
||||||
|
onFilterChange: (filters: AlbumListFilter) => void;
|
||||||
|
pageKey: string;
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubsonicAlbumFilters = ({
|
||||||
|
onFilterChange,
|
||||||
|
pageKey,
|
||||||
|
serverId,
|
||||||
|
}: SubsonicAlbumFiltersProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { filter } = useListStoreByKey<AlbumListQuery>({ key: pageKey });
|
||||||
|
const { setFilter } = useListStoreActions();
|
||||||
|
|
||||||
|
const genreListQuery = useGenreList({
|
||||||
|
query: {
|
||||||
|
sortBy: GenreListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const genreList = useMemo(() => {
|
||||||
|
if (!genreListQuery?.data) return [];
|
||||||
|
return genreListQuery.data.items.map((genre) => ({
|
||||||
|
label: genre.name,
|
||||||
|
value: genre.id,
|
||||||
|
}));
|
||||||
|
}, [genreListQuery.data]);
|
||||||
|
|
||||||
|
const handleGenresFilter = debounce((e: string | null) => {
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
data: {
|
||||||
|
genres: e ? [e] : undefined,
|
||||||
|
},
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
key: pageKey,
|
||||||
|
}) as AlbumListFilter;
|
||||||
|
|
||||||
|
onFilterChange(updatedFilters);
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
const toggleFilters = [
|
||||||
|
{
|
||||||
|
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||||
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
data: {
|
||||||
|
favorite: e.target.checked ? true : undefined,
|
||||||
|
},
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
key: pageKey,
|
||||||
|
}) as AlbumListFilter;
|
||||||
|
onFilterChange(updatedFilters);
|
||||||
|
},
|
||||||
|
value: filter.favorite,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleYearFilter = debounce((e: number | string, type: 'min' | 'max') => {
|
||||||
|
let data: Partial<AlbumListQuery> = {};
|
||||||
|
|
||||||
|
if (type === 'min') {
|
||||||
|
data = {
|
||||||
|
minYear: e ? Number(e) : undefined,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
data = {
|
||||||
|
maxYear: e ? Number(e) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
data,
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
key: pageKey,
|
||||||
|
}) as AlbumListFilter;
|
||||||
|
|
||||||
|
onFilterChange(updatedFilters);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack p="0.8rem">
|
||||||
|
{toggleFilters.map((filter) => (
|
||||||
|
<Group
|
||||||
|
key={`nd-filter-${filter.label}`}
|
||||||
|
position="apart"
|
||||||
|
>
|
||||||
|
<Text>{filter.label}</Text>
|
||||||
|
<Switch
|
||||||
|
checked={filter?.value || false}
|
||||||
|
onChange={filter.onChange}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
<Divider my="0.5rem" />
|
||||||
|
<Group grow>
|
||||||
|
<NumberInput
|
||||||
|
defaultValue={filter.minYear}
|
||||||
|
disabled={filter.genres?.length !== undefined}
|
||||||
|
hideControls={false}
|
||||||
|
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
||||||
|
max={5000}
|
||||||
|
min={0}
|
||||||
|
onChange={(e) => handleYearFilter(e, 'min')}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
defaultValue={filter.maxYear}
|
||||||
|
disabled={filter.genres?.length !== undefined}
|
||||||
|
hideControls={false}
|
||||||
|
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
||||||
|
max={5000}
|
||||||
|
min={0}
|
||||||
|
onChange={(e) => handleYearFilter(e, 'max')}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group grow>
|
||||||
|
<Select
|
||||||
|
clearable
|
||||||
|
searchable
|
||||||
|
data={genreList}
|
||||||
|
defaultValue={filter.genres?.length ? filter.genres[0] : undefined}
|
||||||
|
disabled={Boolean(filter.minYear || filter.maxYear)}
|
||||||
|
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
||||||
|
onChange={handleGenresFilter}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import type { AlbumListQuery } from '/@/renderer/api/types';
|
||||||
|
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||||
|
import { getServerById } from '/@/renderer/store';
|
||||||
|
|
||||||
|
export const useAlbumListCount = (args: QueryHookArgs<AlbumListQuery>) => {
|
||||||
|
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.getAlbumListCount({
|
||||||
|
apiClientProps: {
|
||||||
|
server,
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
queryKey: queryKeys.albums.count(
|
||||||
|
serverId || '',
|
||||||
|
Object.keys(query).length === 0 ? undefined : query,
|
||||||
|
),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
|
@ -5,12 +5,11 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
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 { GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
import { AlbumListQuery, GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
import { ListContext } from '/@/renderer/context/list-context';
|
import { ListContext } from '/@/renderer/context/list-context';
|
||||||
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
|
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
|
||||||
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
|
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
|
||||||
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
|
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared';
|
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||||
import { queryClient } from '/@/renderer/lib/react-query';
|
import { queryClient } from '/@/renderer/lib/react-query';
|
||||||
|
@ -18,6 +17,7 @@ import { useCurrentServer, useListFilterByKey } from '/@/renderer/store';
|
||||||
import { Play } from '/@/renderer/types';
|
import { Play } from '/@/renderer/types';
|
||||||
import { useGenreList } from '/@/renderer/features/genres';
|
import { useGenreList } from '/@/renderer/features/genres';
|
||||||
import { titleCase } from '/@/renderer/utils';
|
import { titleCase } from '/@/renderer/utils';
|
||||||
|
import { useAlbumListCount } from '/@/renderer/features/albums/queries/album-list-count-query';
|
||||||
|
|
||||||
const AlbumListRoute = () => {
|
const AlbumListRoute = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -33,14 +33,7 @@ const AlbumListRoute = () => {
|
||||||
const value = {
|
const value = {
|
||||||
...(albumArtistId && { artistIds: [albumArtistId] }),
|
...(albumArtistId && { artistIds: [albumArtistId] }),
|
||||||
...(genreId && {
|
...(genreId && {
|
||||||
_custom: {
|
genres: [genreId],
|
||||||
jellyfin: {
|
|
||||||
GenreIds: genreId,
|
|
||||||
},
|
|
||||||
navidrome: {
|
|
||||||
genre_id: genreId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -51,7 +44,7 @@ const AlbumListRoute = () => {
|
||||||
return value;
|
return value;
|
||||||
}, [albumArtistId, genreId]);
|
}, [albumArtistId, genreId]);
|
||||||
|
|
||||||
const albumListFilter = useListFilterByKey({
|
const albumListFilter = useListFilterByKey<AlbumListQuery>({
|
||||||
filter: customFilters,
|
filter: customFilters,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
});
|
});
|
||||||
|
@ -78,32 +71,27 @@ const AlbumListRoute = () => {
|
||||||
return genre?.name;
|
return genre?.name;
|
||||||
}, [genreId, genreList.data]);
|
}, [genreId, genreList.data]);
|
||||||
|
|
||||||
const itemCountCheck = useAlbumList({
|
const itemCountCheck = useAlbumListCount({
|
||||||
options: {
|
options: {
|
||||||
cacheTime: 1000 * 60,
|
cacheTime: 1000 * 60,
|
||||||
staleTime: 1000 * 60,
|
staleTime: 1000 * 60,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
limit: 1,
|
|
||||||
startIndex: 0,
|
|
||||||
...albumListFilter,
|
...albumListFilter,
|
||||||
},
|
},
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemCount =
|
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
|
||||||
itemCountCheck.data?.totalRecordCount === null
|
|
||||||
? undefined
|
|
||||||
: itemCountCheck.data?.totalRecordCount;
|
|
||||||
|
|
||||||
const handlePlay = useCallback(
|
const handlePlay = useCallback(
|
||||||
async (args: { initialSongId?: string; playType: Play }) => {
|
async (args: { initialSongId?: string; playType: Play }) => {
|
||||||
if (!itemCount || itemCount === 0) return;
|
if (!itemCount || itemCount === 0) return;
|
||||||
const { playType } = args;
|
const { playType } = args;
|
||||||
const query = {
|
const query = {
|
||||||
startIndex: 0,
|
|
||||||
...albumListFilter,
|
...albumListFilter,
|
||||||
...customFilters,
|
...customFilters,
|
||||||
|
startIndex: 0,
|
||||||
};
|
};
|
||||||
const queryKey = queryKeys.albums.list(server?.id || '', query);
|
const queryKey = queryKeys.albums.list(server?.id || '', query);
|
||||||
|
|
||||||
|
|
|
@ -111,18 +111,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
enabled: enabledItem.recentAlbums,
|
enabled: enabledItem.recentAlbums,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
_custom: {
|
artistIds: [albumArtistId],
|
||||||
jellyfin: {
|
|
||||||
...(server?.type === ServerType.JELLYFIN
|
|
||||||
? { AlbumArtistIds: albumArtistId }
|
|
||||||
: undefined),
|
|
||||||
},
|
|
||||||
navidrome: {
|
|
||||||
...(server?.type === ServerType.NAVIDROME
|
|
||||||
? { artist_id: albumArtistId, compilation: false }
|
|
||||||
: undefined),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
limit: 15,
|
limit: 15,
|
||||||
sortBy: AlbumListSort.RELEASE_DATE,
|
sortBy: AlbumListSort.RELEASE_DATE,
|
||||||
sortOrder: SortOrder.DESC,
|
sortOrder: SortOrder.DESC,
|
||||||
|
@ -133,21 +122,11 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
|
|
||||||
const compilationAlbumsQuery = useAlbumList({
|
const compilationAlbumsQuery = useAlbumList({
|
||||||
options: {
|
options: {
|
||||||
enabled: enabledItem.compilations,
|
enabled: enabledItem.compilations && server?.type !== ServerType.SUBSONIC,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
_custom: {
|
artistIds: [albumArtistId],
|
||||||
jellyfin: {
|
compilation: true,
|
||||||
...(server?.type === ServerType.JELLYFIN
|
|
||||||
? { ContributingArtistIds: albumArtistId }
|
|
||||||
: undefined),
|
|
||||||
},
|
|
||||||
navidrome: {
|
|
||||||
...(server?.type === ServerType.NAVIDROME
|
|
||||||
? { artist_id: albumArtistId, compilation: true }
|
|
||||||
: undefined),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
limit: 15,
|
limit: 15,
|
||||||
sortBy: AlbumListSort.RELEASE_DATE,
|
sortBy: AlbumListSort.RELEASE_DATE,
|
||||||
sortOrder: SortOrder.DESC,
|
sortOrder: SortOrder.DESC,
|
||||||
|
@ -254,7 +233,10 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
data: compilationAlbumsQuery?.data?.items,
|
data: compilationAlbumsQuery?.data?.items,
|
||||||
isHidden: !compilationAlbumsQuery?.data?.items?.length || !enabledItem.compilations,
|
isHidden:
|
||||||
|
!compilationAlbumsQuery?.data?.items?.length ||
|
||||||
|
!enabledItem.compilations ||
|
||||||
|
server?.type === ServerType.SUBSONIC,
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
loading: compilationAlbumsQuery?.isLoading || compilationAlbumsQuery.isFetching,
|
loading: compilationAlbumsQuery?.isLoading || compilationAlbumsQuery.isFetching,
|
||||||
order: itemOrder.compilations,
|
order: itemOrder.compilations,
|
||||||
|
@ -301,6 +283,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
recentAlbumsQuery?.data?.items,
|
recentAlbumsQuery?.data?.items,
|
||||||
recentAlbumsQuery.isFetching,
|
recentAlbumsQuery.isFetching,
|
||||||
recentAlbumsQuery?.isLoading,
|
recentAlbumsQuery?.isLoading,
|
||||||
|
server?.type,
|
||||||
t,
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -26,16 +26,19 @@ export const AlbumArtistDetailHeader = forwardRef(
|
||||||
|
|
||||||
const metadataItems = [
|
const metadataItems = [
|
||||||
{
|
{
|
||||||
|
enabled: detailQuery?.data?.albumCount,
|
||||||
id: 'albumCount',
|
id: 'albumCount',
|
||||||
secondary: false,
|
secondary: false,
|
||||||
value: t('entity.albumWithCount', { count: detailQuery?.data?.albumCount || 0 }),
|
value: t('entity.albumWithCount', { count: detailQuery?.data?.albumCount || 0 }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
enabled: detailQuery?.data?.songCount,
|
||||||
id: 'songCount',
|
id: 'songCount',
|
||||||
secondary: false,
|
secondary: false,
|
||||||
value: t('entity.trackWithCount', { count: detailQuery?.data?.songCount || 0 }),
|
value: t('entity.trackWithCount', { count: detailQuery?.data?.songCount || 0 }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
enabled: detailQuery.data?.duration,
|
||||||
id: 'duration',
|
id: 'duration',
|
||||||
secondary: true,
|
secondary: true,
|
||||||
value:
|
value:
|
||||||
|
@ -70,7 +73,7 @@ export const AlbumArtistDetailHeader = forwardRef(
|
||||||
<Stack>
|
<Stack>
|
||||||
<Group>
|
<Group>
|
||||||
{metadataItems
|
{metadataItems
|
||||||
.filter((i) => i.value)
|
.filter((i) => i.enabled)
|
||||||
.map((item, index) => (
|
.map((item, index) => (
|
||||||
<Fragment key={`item-${item.id}-${index}`}>
|
<Fragment key={`item-${item.id}-${index}`}>
|
||||||
{index > 0 && <Text $noSelect>•</Text>}
|
{index > 0 && <Text $noSelect>•</Text>}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {
|
||||||
AlbumArtistListQuery,
|
AlbumArtistListQuery,
|
||||||
AlbumArtistListResponse,
|
AlbumArtistListResponse,
|
||||||
AlbumArtistListSort,
|
AlbumArtistListSort,
|
||||||
ArtistListQuery,
|
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import { ALBUMARTIST_CARD_ROWS } from '/@/renderer/components';
|
import { ALBUMARTIST_CARD_ROWS } from '/@/renderer/components';
|
||||||
|
@ -34,7 +33,7 @@ export const AlbumArtistListGridView = ({ itemCount, gridRef }: AlbumArtistListG
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
|
|
||||||
const { pageKey } = useListContext();
|
const { pageKey } = useListContext();
|
||||||
const { grid, display, filter } = useListStoreByKey({ key: pageKey });
|
const { grid, display, filter } = useListStoreByKey<AlbumArtistListQuery>({ key: pageKey });
|
||||||
const { setGrid } = useListStoreActions();
|
const { setGrid } = useListStoreActions();
|
||||||
const handleFavorite = useHandleFavorite({ gridRef, server });
|
const handleFavorite = useHandleFavorite({ gridRef, server });
|
||||||
|
|
||||||
|
@ -73,7 +72,7 @@ export const AlbumArtistListGridView = ({ itemCount, gridRef }: AlbumArtistListG
|
||||||
|
|
||||||
const fetch = useCallback(
|
const fetch = useCallback(
|
||||||
async ({ skip: startIndex, take: limit }: { skip: number; take: number }) => {
|
async ({ skip: startIndex, take: limit }: { skip: number; take: number }) => {
|
||||||
const query: ArtistListQuery = {
|
const query: AlbumArtistListQuery = {
|
||||||
...filter,
|
...filter,
|
||||||
limit,
|
limit,
|
||||||
startIndex,
|
startIndex,
|
||||||
|
@ -91,7 +90,6 @@ export const AlbumArtistListGridView = ({ itemCount, gridRef }: AlbumArtistListG
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
limit,
|
limit,
|
||||||
startIndex,
|
|
||||||
...filter,
|
...filter,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -9,7 +9,13 @@ import { RiFolder2Line, RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react
|
||||||
import { useListContext } from '../../../context/list-context';
|
import { useListContext } from '../../../context/list-context';
|
||||||
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 { AlbumArtistListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types';
|
import {
|
||||||
|
AlbumArtistListQuery,
|
||||||
|
AlbumArtistListSort,
|
||||||
|
LibraryItem,
|
||||||
|
ServerType,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/renderer/api/types';
|
||||||
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
|
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||||
|
@ -85,6 +91,28 @@ const FILTERS = {
|
||||||
value: AlbumArtistListSort.SONG_COUNT,
|
value: AlbumArtistListSort.SONG_COUNT,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
subsonic: [
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumArtistListSort.ALBUM_COUNT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumArtistListSort.FAVORITED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumArtistListSort.NAME,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
|
||||||
|
value: AlbumArtistListSort.RATING,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AlbumArtistListHeaderFiltersProps {
|
interface AlbumArtistListHeaderFiltersProps {
|
||||||
|
@ -100,7 +128,9 @@ export const AlbumArtistListHeaderFilters = ({
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { pageKey } = useListContext();
|
const { pageKey } = useListContext();
|
||||||
const { display, table, grid, filter } = useListStoreByKey({ key: pageKey });
|
const { display, table, grid, filter } = useListStoreByKey<AlbumArtistListQuery>({
|
||||||
|
key: pageKey,
|
||||||
|
});
|
||||||
const { setFilter, setTable, setTablePagination, setDisplayType, setGrid } =
|
const { setFilter, setTable, setTablePagination, setDisplayType, setGrid } =
|
||||||
useListStoreActions();
|
useListStoreActions();
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Flex, Group, Stack } from '@mantine/core';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { FilterBar } from '../../shared/components/filter-bar';
|
import { FilterBar } from '../../shared/components/filter-bar';
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
import { AlbumArtistListQuery, LibraryItem } from '/@/renderer/api/types';
|
||||||
import { PageHeader, SearchInput } from '/@/renderer/components';
|
import { PageHeader, SearchInput } from '/@/renderer/components';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters';
|
import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters';
|
||||||
|
@ -28,8 +28,9 @@ export const AlbumArtistListHeader = ({
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
|
|
||||||
const { filter, refresh, search } = useDisplayRefresh({
|
const { filter, refresh, search } = useDisplayRefresh<AlbumArtistListQuery>({
|
||||||
gridRef,
|
gridRef,
|
||||||
|
itemCount,
|
||||||
itemType: LibraryItem.ALBUM_ARTIST,
|
itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
server,
|
server,
|
||||||
tableRef,
|
tableRef,
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { AlbumArtistListQuery } from '/@/renderer/api/types';
|
||||||
|
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||||
|
import { getServerById } from '/@/renderer/store';
|
||||||
|
|
||||||
|
export const useAlbumArtistListCount = (args: QueryHookArgs<AlbumArtistListQuery>) => {
|
||||||
|
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.getAlbumArtistListCount({
|
||||||
|
apiClientProps: {
|
||||||
|
server,
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
queryKey: queryKeys.albumArtists.count(
|
||||||
|
serverId || '',
|
||||||
|
Object.keys(query).length === 0 ? undefined : query,
|
||||||
|
),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
|
@ -13,7 +13,7 @@ export const useTopSongsList = (args: QueryHookArgs<TopSongListQuery>) => {
|
||||||
enabled: !!server?.id,
|
enabled: !!server?.id,
|
||||||
queryFn: ({ signal }) => {
|
queryFn: ({ signal }) => {
|
||||||
if (!server) throw new Error('Server not found');
|
if (!server) throw new Error('Server not found');
|
||||||
return api.controller.getTopSongList({ apiClientProps: { server, signal }, query });
|
return api.controller.getTopSongs({ apiClientProps: { server, signal }, query });
|
||||||
},
|
},
|
||||||
queryKey: queryKeys.albumArtists.topSongs(server?.id || '', query),
|
queryKey: queryKeys.albumArtists.topSongs(server?.id || '', query),
|
||||||
...options,
|
...options,
|
||||||
|
|
|
@ -2,13 +2,13 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { useCurrentServer } from '../../../store/auth.store';
|
import { useCurrentServer } from '../../../store/auth.store';
|
||||||
import { useListFilterByKey } from '../../../store/list.store';
|
import { useListFilterByKey } from '../../../store/list.store';
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
import { AlbumArtistListQuery, LibraryItem } from '/@/renderer/api/types';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
import { ListContext } from '/@/renderer/context/list-context';
|
import { ListContext } from '/@/renderer/context/list-context';
|
||||||
import { AlbumArtistListContent } from '/@/renderer/features/artists/components/album-artist-list-content';
|
import { AlbumArtistListContent } from '/@/renderer/features/artists/components/album-artist-list-content';
|
||||||
import { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header';
|
import { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header';
|
||||||
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
|
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared';
|
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||||
|
import { useAlbumArtistListCount } from '/@/renderer/features/artists/queries/album-artist-list-count-query';
|
||||||
|
|
||||||
const AlbumArtistListRoute = () => {
|
const AlbumArtistListRoute = () => {
|
||||||
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||||
|
@ -16,25 +16,18 @@ const AlbumArtistListRoute = () => {
|
||||||
const pageKey = LibraryItem.ALBUM_ARTIST;
|
const pageKey = LibraryItem.ALBUM_ARTIST;
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
|
||||||
const albumArtistListFilter = useListFilterByKey({ key: pageKey });
|
const albumArtistListFilter = useListFilterByKey<AlbumArtistListQuery>({ key: pageKey });
|
||||||
|
|
||||||
const itemCountCheck = useAlbumArtistList({
|
const itemCountCheck = useAlbumArtistListCount({
|
||||||
options: {
|
options: {
|
||||||
cacheTime: 1000 * 60,
|
cacheTime: 1000 * 60,
|
||||||
staleTime: 1000 * 60,
|
staleTime: 1000 * 60,
|
||||||
},
|
},
|
||||||
query: {
|
query: albumArtistListFilter,
|
||||||
limit: 1,
|
|
||||||
startIndex: 0,
|
|
||||||
...albumArtistListFilter,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemCount =
|
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
|
||||||
itemCountCheck.data?.totalRecordCount === null
|
|
||||||
? undefined
|
|
||||||
: itemCountCheck.data?.totalRecordCount;
|
|
||||||
|
|
||||||
const providerValue = useMemo(() => {
|
const providerValue = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -494,17 +494,24 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
const removeFromPlaylistMutation = useRemoveFromPlaylist();
|
const removeFromPlaylistMutation = useRemoveFromPlaylist();
|
||||||
|
|
||||||
const handleRemoveFromPlaylist = useCallback(() => {
|
const handleRemoveFromPlaylist = useCallback(() => {
|
||||||
const songId =
|
let songId: string[] | undefined;
|
||||||
(serverType === ServerType.NAVIDROME || ServerType.JELLYFIN
|
|
||||||
? ctx.dataNodes?.map((node) => node.data.playlistItemId)
|
switch (serverType) {
|
||||||
: ctx.dataNodes?.map((node) => node.data.id)) || [];
|
case ServerType.NAVIDROME:
|
||||||
|
case ServerType.JELLYFIN:
|
||||||
|
songId = ctx.dataNodes?.map((node) => node.data.playlistItemId);
|
||||||
|
break;
|
||||||
|
case ServerType.SUBSONIC:
|
||||||
|
songId = ctx.dataNodes?.map((node) => node.rowIndex!.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const confirm = () => {
|
const confirm = () => {
|
||||||
removeFromPlaylistMutation.mutate(
|
removeFromPlaylistMutation.mutate(
|
||||||
{
|
{
|
||||||
query: {
|
query: {
|
||||||
id: ctx.context.playlistId,
|
id: ctx.context.playlistId,
|
||||||
songId,
|
songId: songId || [],
|
||||||
},
|
},
|
||||||
serverId: ctx.data?.[0]?.serverId,
|
serverId: ctx.data?.[0]?.serverId,
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const GenreListGridView = ({ gridRef, itemCount }: any) => {
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
const { pageKey, id } = useListContext();
|
const { pageKey, id } = useListContext();
|
||||||
const { grid, display, filter } = useListStoreByKey({ key: pageKey });
|
const { grid, display, filter } = useListStoreByKey<GenreListQuery>({ key: pageKey });
|
||||||
const { setGrid } = useListStoreActions();
|
const { setGrid } = useListStoreActions();
|
||||||
const genrePath = useGenreRoute();
|
const genrePath = useGenreRoute();
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,13 @@ import {
|
||||||
RiSettings3Fill,
|
RiSettings3Fill,
|
||||||
} from 'react-icons/ri';
|
} from 'react-icons/ri';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { GenreListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types';
|
import {
|
||||||
|
GenreListQuery,
|
||||||
|
GenreListSort,
|
||||||
|
LibraryItem,
|
||||||
|
ServerType,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/renderer/api/types';
|
||||||
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
|
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
import { GENRE_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
import { GENRE_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||||
|
@ -47,25 +53,38 @@ const FILTERS = {
|
||||||
value: GenreListSort.NAME,
|
value: GenreListSort.NAME,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
subsonic: [
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||||
|
value: GenreListSort.NAME,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface GenreListHeaderFiltersProps {
|
interface GenreListHeaderFiltersProps {
|
||||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||||
|
itemCount: number | undefined;
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFiltersProps) => {
|
export const GenreListHeaderFilters = ({
|
||||||
|
gridRef,
|
||||||
|
itemCount,
|
||||||
|
tableRef,
|
||||||
|
}: GenreListHeaderFiltersProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { pageKey, customFilters } = useListContext();
|
const { pageKey, customFilters } = useListContext();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { setFilter, setTable, setGrid, setDisplayType } = useListStoreActions();
|
const { setFilter, setTable, setGrid, setDisplayType } = useListStoreActions();
|
||||||
const { display, filter, table, grid } = useListStoreByKey({ key: pageKey });
|
const { display, filter, table, grid } = useListStoreByKey<GenreListQuery>({ key: pageKey });
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
const { genreTarget } = useGeneralSettings();
|
const { genreTarget } = useGeneralSettings();
|
||||||
const { setGenreBehavior } = useSettingsStoreActions();
|
const { setGenreBehavior } = useSettingsStoreActions();
|
||||||
|
|
||||||
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
|
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
|
||||||
|
itemCount,
|
||||||
itemType: LibraryItem.GENRE,
|
itemType: LibraryItem.GENRE,
|
||||||
server,
|
server,
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { ChangeEvent, MutableRefObject } from 'react';
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { Flex, Group, Stack } from '@mantine/core';
|
import { Flex, Group, Stack } from '@mantine/core';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
import { GenreListQuery, LibraryItem } from '/@/renderer/api/types';
|
||||||
import { PageHeader, SearchInput } from '/@/renderer/components';
|
import { PageHeader, SearchInput } from '/@/renderer/components';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
import { GenreListHeaderFilters } from '/@/renderer/features/genres/components/genre-list-header-filters';
|
import { GenreListHeaderFilters } from '/@/renderer/features/genres/components/genre-list-header-filters';
|
||||||
|
@ -22,7 +22,7 @@ export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeade
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { filter, refresh, search } = useDisplayRefresh({
|
const { filter, refresh, search } = useDisplayRefresh<GenreListQuery>({
|
||||||
gridRef,
|
gridRef,
|
||||||
itemType: LibraryItem.GENRE,
|
itemType: LibraryItem.GENRE,
|
||||||
server,
|
server,
|
||||||
|
@ -66,6 +66,7 @@ export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeade
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<GenreListHeaderFilters
|
<GenreListHeaderFilters
|
||||||
gridRef={gridRef}
|
gridRef={gridRef}
|
||||||
|
itemCount={itemCount}
|
||||||
tableRef={tableRef}
|
tableRef={tableRef}
|
||||||
/>
|
/>
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
|
|
|
@ -8,19 +8,20 @@ import { useGenreList } from '/@/renderer/features/genres/queries/genre-list-que
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared';
|
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
import { useListStoreByKey } from '../../../store/list.store';
|
import { useListStoreByKey } from '../../../store/list.store';
|
||||||
|
import { GenreListQuery } from '/@/renderer/api/types';
|
||||||
|
|
||||||
const GenreListRoute = () => {
|
const GenreListRoute = () => {
|
||||||
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
const tableRef = useRef<AgGridReactType | null>(null);
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const pageKey = 'genre';
|
const pageKey = 'genre';
|
||||||
const { filter } = useListStoreByKey({ key: pageKey });
|
const { filter } = useListStoreByKey<GenreListQuery>({ key: pageKey });
|
||||||
|
|
||||||
const itemCountCheck = useGenreList({
|
const itemCountCheck = useGenreList({
|
||||||
query: {
|
query: {
|
||||||
|
...filter,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
...filter,
|
|
||||||
},
|
},
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,7 +9,6 @@ import {
|
||||||
SongListSort,
|
SongListSort,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
ServerListItem,
|
ServerListItem,
|
||||||
ServerType,
|
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
|
|
||||||
export const getPlaylistSongsById = async (args: {
|
export const getPlaylistSongsById = async (args: {
|
||||||
|
@ -103,18 +102,7 @@ export const getGenreSongsById = async (args: {
|
||||||
};
|
};
|
||||||
for (const genreId of id) {
|
for (const genreId of id) {
|
||||||
const queryFilter: SongListQuery = {
|
const queryFilter: SongListQuery = {
|
||||||
_custom: {
|
genreIds: [genreId],
|
||||||
...(server?.type === ServerType.JELLYFIN && {
|
|
||||||
jellyfin: {
|
|
||||||
GenreIds: genreId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
...(server?.type === ServerType.NAVIDROME && {
|
|
||||||
navidrome: {
|
|
||||||
genre_id: genreId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
sortBy: SongListSort.GENRE,
|
sortBy: SongListSort.GENRE,
|
||||||
sortOrder: SortOrder.ASC,
|
sortOrder: SortOrder.ASC,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
|
@ -140,7 +128,9 @@ export const getGenreSongsById = async (args: {
|
||||||
);
|
);
|
||||||
|
|
||||||
data.items.push(...res!.items);
|
data.items.push(...res!.items);
|
||||||
data.totalRecordCount += res!.totalRecordCount;
|
if (data.totalRecordCount) {
|
||||||
|
data.totalRecordCount += res!.totalRecordCount || 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
@ -202,14 +192,15 @@ export const getSongsByQuery = async (args: {
|
||||||
|
|
||||||
const res = await queryClient.fetchQuery(
|
const res = await queryClient.fetchQuery(
|
||||||
queryKey,
|
queryKey,
|
||||||
async ({ signal }) =>
|
async ({ signal }) => {
|
||||||
api.controller.getSongList({
|
return api.controller.getSongList({
|
||||||
apiClientProps: {
|
apiClientProps: {
|
||||||
server,
|
server,
|
||||||
signal,
|
signal,
|
||||||
},
|
},
|
||||||
query: queryFilter,
|
query: queryFilter,
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
{
|
{
|
||||||
cacheTime: 1000 * 60,
|
cacheTime: 1000 * 60,
|
||||||
staleTime: 1000 * 60,
|
staleTime: 1000 * 60,
|
||||||
|
|
|
@ -151,7 +151,12 @@ export const AddToPlaylistContextModal = ({
|
||||||
server,
|
server,
|
||||||
signal,
|
signal,
|
||||||
},
|
},
|
||||||
query: { id: playlistId, startIndex: 0 },
|
query: {
|
||||||
|
id: playlistId,
|
||||||
|
sortBy: SongListSort.ID,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,252 +0,0 @@
|
||||||
import { MutableRefObject, useMemo, useRef } from 'react';
|
|
||||||
import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
|
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
|
||||||
import { Box, Group } from '@mantine/core';
|
|
||||||
import { closeAllModals, openModal } from '@mantine/modals';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { RiMoreFill } from 'react-icons/ri';
|
|
||||||
import { generatePath, useNavigate, useParams } from 'react-router';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { useListStoreByKey } from '../../../store/list.store';
|
|
||||||
import { LibraryItem, QueueSong } from '/@/renderer/api/types';
|
|
||||||
import { Button, ConfirmModal, DropdownMenu, MotionGroup, toast } from '/@/renderer/components';
|
|
||||||
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
|
|
||||||
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
|
|
||||||
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
|
|
||||||
import {
|
|
||||||
PLAYLIST_SONG_CONTEXT_MENU_ITEMS,
|
|
||||||
SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS,
|
|
||||||
} from '/@/renderer/features/context-menu/context-menu-items';
|
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
|
||||||
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
|
|
||||||
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
|
|
||||||
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
|
||||||
import { usePlaylistSongListInfinite } from '/@/renderer/features/playlists/queries/playlist-song-list-query';
|
|
||||||
import { PlayButton, PLAY_TYPES } from '/@/renderer/features/shared';
|
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
|
||||||
import { Play } from '/@/renderer/types';
|
|
||||||
|
|
||||||
const ContentContainer = styled.div`
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 1rem 2rem 5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.ag-theme-alpine-dark {
|
|
||||||
--ag-header-background-color: rgb(0 0 0 / 0%) !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface PlaylistDetailContentProps {
|
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
|
||||||
const { table } = useListStoreByKey({ key: LibraryItem.SONG });
|
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
|
||||||
const server = useCurrentServer();
|
|
||||||
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
|
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
|
||||||
|
|
||||||
const playlistSongsQueryInfinite = usePlaylistSongListInfinite({
|
|
||||||
options: {
|
|
||||||
cacheTime: 0,
|
|
||||||
keepPreviousData: false,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
id: playlistId,
|
|
||||||
limit: 50,
|
|
||||||
startIndex: 0,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleLoadMore = () => {
|
|
||||||
playlistSongsQueryInfinite.fetchNextPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
const columnDefs: ColDef[] = useMemo(
|
|
||||||
() =>
|
|
||||||
getColumnDefs(table.columns).filter((c) => c.colId !== 'album' && c.colId !== 'artist'),
|
|
||||||
[table.columns],
|
|
||||||
);
|
|
||||||
|
|
||||||
const contextMenuItems = useMemo(() => {
|
|
||||||
if (detailQuery?.data?.rules) {
|
|
||||||
return SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS;
|
|
||||||
}
|
|
||||||
|
|
||||||
return PLAYLIST_SONG_CONTEXT_MENU_ITEMS;
|
|
||||||
}, [detailQuery?.data?.rules]);
|
|
||||||
|
|
||||||
const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, contextMenuItems, {
|
|
||||||
playlistId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const playlistSongData = useMemo(
|
|
||||||
() => playlistSongsQueryInfinite.data?.pages.flatMap((p) => p?.items),
|
|
||||||
[playlistSongsQueryInfinite.data?.pages],
|
|
||||||
);
|
|
||||||
|
|
||||||
const deletePlaylistMutation = useDeletePlaylist({});
|
|
||||||
|
|
||||||
const handleDeletePlaylist = () => {
|
|
||||||
deletePlaylistMutation.mutate(
|
|
||||||
{ query: { id: playlistId }, serverId: server?.id },
|
|
||||||
{
|
|
||||||
onError: (err) => {
|
|
||||||
toast.error({
|
|
||||||
message: err.message,
|
|
||||||
title: t('error.genericError', { postProcess: 'sentenceCase' }),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
closeAllModals();
|
|
||||||
navigate(AppRoute.PLAYLISTS);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openDeletePlaylist = () => {
|
|
||||||
openModal({
|
|
||||||
children: (
|
|
||||||
<ConfirmModal
|
|
||||||
loading={deletePlaylistMutation.isLoading}
|
|
||||||
onConfirm={handleDeletePlaylist}
|
|
||||||
>
|
|
||||||
Are you sure you want to delete this playlist?
|
|
||||||
</ConfirmModal>
|
|
||||||
),
|
|
||||||
title: t('form.deletePlaylist.title', { postProcess: 'sentenceCase' }),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePlay = (playType?: Play) => {
|
|
||||||
handlePlayQueueAdd?.({
|
|
||||||
byItemType: {
|
|
||||||
id: [playlistId],
|
|
||||||
type: LibraryItem.PLAYLIST,
|
|
||||||
},
|
|
||||||
playType: playType || playButtonBehavior,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
|
|
||||||
if (!e.data) return;
|
|
||||||
|
|
||||||
handlePlayQueueAdd?.({
|
|
||||||
byItemType: {
|
|
||||||
id: [playlistId],
|
|
||||||
type: LibraryItem.PLAYLIST,
|
|
||||||
},
|
|
||||||
initialSongId: e.data.id,
|
|
||||||
playType: playButtonBehavior,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const { rowClassRules } = useCurrentSongRowStyles({ tableRef });
|
|
||||||
|
|
||||||
const loadMoreRef = useRef<HTMLButtonElement | null>(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ContentContainer>
|
|
||||||
<Group
|
|
||||||
p="1rem"
|
|
||||||
position="apart"
|
|
||||||
>
|
|
||||||
<Group>
|
|
||||||
<PlayButton onClick={() => handlePlay()} />
|
|
||||||
<DropdownMenu position="bottom-start">
|
|
||||||
<DropdownMenu.Target>
|
|
||||||
<Button
|
|
||||||
compact
|
|
||||||
variant="subtle"
|
|
||||||
>
|
|
||||||
<RiMoreFill size={20} />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenu.Target>
|
|
||||||
<DropdownMenu.Dropdown>
|
|
||||||
{PLAY_TYPES.filter((type) => type.play !== playButtonBehavior).map(
|
|
||||||
(type) => (
|
|
||||||
<DropdownMenu.Item
|
|
||||||
key={`playtype-${type.play}`}
|
|
||||||
onClick={() => handlePlay(type.play)}
|
|
||||||
>
|
|
||||||
{type.label}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
<DropdownMenu.Divider />
|
|
||||||
<DropdownMenu.Item
|
|
||||||
onClick={() => {
|
|
||||||
if (!detailQuery.data || !server) return;
|
|
||||||
openUpdatePlaylistModal({ playlist: detailQuery.data, server });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit playlist
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item onClick={openDeletePlaylist}>
|
|
||||||
Delete playlist
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Dropdown>
|
|
||||||
</DropdownMenu>
|
|
||||||
<Button
|
|
||||||
compact
|
|
||||||
uppercase
|
|
||||||
component={Link}
|
|
||||||
to={generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId })}
|
|
||||||
variant="subtle"
|
|
||||||
>
|
|
||||||
View full playlist
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
<Box>
|
|
||||||
<VirtualTable
|
|
||||||
ref={tableRef}
|
|
||||||
autoFitColumns
|
|
||||||
autoHeight
|
|
||||||
deselectOnClickOutside
|
|
||||||
shouldUpdateSong
|
|
||||||
stickyHeader
|
|
||||||
suppressCellFocus
|
|
||||||
suppressHorizontalScroll
|
|
||||||
suppressLoadingOverlay
|
|
||||||
suppressRowDrag
|
|
||||||
columnDefs={columnDefs}
|
|
||||||
getRowId={(data) => `${data.data.uniqueId}-${data.data.pageIndex}`}
|
|
||||||
rowClassRules={rowClassRules}
|
|
||||||
rowData={playlistSongData}
|
|
||||||
rowHeight={60}
|
|
||||||
rowSelection="multiple"
|
|
||||||
onCellContextMenu={handleContextMenu}
|
|
||||||
onRowDoubleClicked={handleRowDoubleClick}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<MotionGroup
|
|
||||||
p="2rem"
|
|
||||||
position="center"
|
|
||||||
onViewportEnter={handleLoadMore}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
ref={loadMoreRef}
|
|
||||||
compact
|
|
||||||
disabled={!playlistSongsQueryInfinite.hasNextPage}
|
|
||||||
loading={playlistSongsQueryInfinite.isFetchingNextPage}
|
|
||||||
variant="subtle"
|
|
||||||
onClick={handleLoadMore}
|
|
||||||
>
|
|
||||||
{playlistSongsQueryInfinite.hasNextPage ? 'Load more' : 'End of playlist'}
|
|
||||||
</Button>
|
|
||||||
</MotionGroup>
|
|
||||||
</ContentContainer>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,79 +0,0 @@
|
||||||
import { forwardRef, Fragment, Ref } from 'react';
|
|
||||||
import { Group, Stack } from '@mantine/core';
|
|
||||||
import { useParams } from 'react-router';
|
|
||||||
import { Badge, Text } from '/@/renderer/components';
|
|
||||||
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
|
||||||
import { LibraryHeader } from '/@/renderer/features/shared';
|
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
|
||||||
import { formatDurationString } from '/@/renderer/utils';
|
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
|
||||||
import { useCurrentServer } from '../../../store/auth.store';
|
|
||||||
|
|
||||||
interface PlaylistDetailHeaderProps {
|
|
||||||
background: string;
|
|
||||||
imagePlaceholderUrl?: string | null;
|
|
||||||
imageUrl?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PlaylistDetailHeader = forwardRef(
|
|
||||||
(
|
|
||||||
{ background, imageUrl, imagePlaceholderUrl }: PlaylistDetailHeaderProps,
|
|
||||||
ref: Ref<HTMLDivElement>,
|
|
||||||
) => {
|
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
|
||||||
const server = useCurrentServer();
|
|
||||||
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
|
|
||||||
|
|
||||||
const metadataItems = [
|
|
||||||
{
|
|
||||||
id: 'songCount',
|
|
||||||
secondary: false,
|
|
||||||
value: `${detailQuery?.data?.songCount || 0} songs`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'duration',
|
|
||||||
secondary: true,
|
|
||||||
value:
|
|
||||||
detailQuery?.data?.duration && formatDurationString(detailQuery.data.duration),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const isSmartPlaylist = detailQuery?.data?.rules;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<LibraryHeader
|
|
||||||
ref={ref}
|
|
||||||
background={background}
|
|
||||||
imagePlaceholderUrl={imagePlaceholderUrl}
|
|
||||||
imageUrl={imageUrl}
|
|
||||||
item={{ route: AppRoute.PLAYLISTS, type: LibraryItem.PLAYLIST }}
|
|
||||||
title={detailQuery?.data?.name || ''}
|
|
||||||
>
|
|
||||||
<Stack>
|
|
||||||
<Group spacing="sm">
|
|
||||||
{metadataItems.map((item, index) => (
|
|
||||||
<Fragment key={`item-${item.id}-${index}`}>
|
|
||||||
{index > 0 && <Text $noSelect>•</Text>}
|
|
||||||
<Text $secondary={item.secondary}>{item.value}</Text>
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
{isSmartPlaylist && (
|
|
||||||
<>
|
|
||||||
<Text $noSelect>•</Text>
|
|
||||||
<Badge
|
|
||||||
radius="sm"
|
|
||||||
size="md"
|
|
||||||
>
|
|
||||||
Smart Playlist
|
|
||||||
</Badge>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
<Text lineClamp={3}>{detailQuery?.data?.description}</Text>
|
|
||||||
</Stack>
|
|
||||||
</LibraryHeader>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
|
@ -44,15 +44,16 @@ import {
|
||||||
useSetPlaylistDetailTablePagination,
|
useSetPlaylistDetailTablePagination,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||||
import { ListDisplayType } from '/@/renderer/types';
|
import { ListDisplayType, ServerType } from '/@/renderer/types';
|
||||||
import { useAppFocus } from '/@/renderer/hooks';
|
import { useAppFocus } from '/@/renderer/hooks';
|
||||||
import { toast } from '/@/renderer/components';
|
import { toast } from '/@/renderer/components';
|
||||||
|
|
||||||
interface PlaylistDetailContentProps {
|
interface PlaylistDetailContentProps {
|
||||||
|
songs?: Song[];
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailContentProps) => {
|
export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetailContentProps) => {
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
const { playlistId } = useParams() as { playlistId: string };
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const status = useCurrentStatus();
|
const status = useCurrentStatus();
|
||||||
|
@ -85,7 +86,12 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
|
||||||
|
|
||||||
const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED;
|
const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED;
|
||||||
|
|
||||||
|
const iSClientSide = server?.type === ServerType.SUBSONIC;
|
||||||
|
|
||||||
const checkPlaylistList = usePlaylistSongList({
|
const checkPlaylistList = usePlaylistSongList({
|
||||||
|
options: {
|
||||||
|
enabled: !iSClientSide,
|
||||||
|
},
|
||||||
query: {
|
query: {
|
||||||
id: playlistId,
|
id: playlistId,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
@ -101,44 +107,51 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
|
||||||
|
|
||||||
const onGridReady = useCallback(
|
const onGridReady = useCallback(
|
||||||
(params: GridReadyEvent) => {
|
(params: GridReadyEvent) => {
|
||||||
const dataSource: IDatasource = {
|
if (!iSClientSide) {
|
||||||
getRows: async (params) => {
|
const dataSource: IDatasource = {
|
||||||
const limit = params.endRow - params.startRow;
|
getRows: async (params) => {
|
||||||
const startIndex = params.startRow;
|
const limit = params.endRow - params.startRow;
|
||||||
|
const startIndex = params.startRow;
|
||||||
|
|
||||||
const query: PlaylistSongListQuery = {
|
const query: PlaylistSongListQuery = {
|
||||||
id: playlistId,
|
id: playlistId,
|
||||||
limit,
|
limit,
|
||||||
startIndex,
|
startIndex,
|
||||||
...filters,
|
...filters,
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryKey = queryKeys.playlists.songList(
|
const queryKey = queryKeys.playlists.songList(
|
||||||
server?.id || '',
|
server?.id || '',
|
||||||
playlistId,
|
playlistId,
|
||||||
query,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!server) return;
|
|
||||||
|
|
||||||
const songsRes = await queryClient.fetchQuery(queryKey, async ({ signal }) =>
|
|
||||||
api.controller.getPlaylistSongList({
|
|
||||||
apiClientProps: {
|
|
||||||
server,
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
query,
|
query,
|
||||||
}),
|
);
|
||||||
);
|
|
||||||
|
|
||||||
params.successCallback(songsRes?.items || [], songsRes?.totalRecordCount || 0);
|
if (!server) return;
|
||||||
},
|
|
||||||
rowCount: undefined,
|
const songsRes = await queryClient.fetchQuery(
|
||||||
};
|
queryKey,
|
||||||
params.api.setDatasource(dataSource);
|
async ({ signal }) =>
|
||||||
|
api.controller.getPlaylistSongList({
|
||||||
|
apiClientProps: {
|
||||||
|
server,
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
query,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
params.successCallback(
|
||||||
|
songsRes?.items || [],
|
||||||
|
songsRes?.totalRecordCount || 0,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
rowCount: undefined,
|
||||||
|
};
|
||||||
|
params.api.setDatasource(dataSource);
|
||||||
|
}
|
||||||
params.api?.ensureIndexVisible(pagination.scrollOffset, 'top');
|
params.api?.ensureIndexVisible(pagination.scrollOffset, 'top');
|
||||||
},
|
},
|
||||||
[filters, pagination.scrollOffset, playlistId, queryClient, server],
|
[filters, iSClientSide, pagination.scrollOffset, playlistId, queryClient, server],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDragEnd = useCallback(
|
const handleDragEnd = useCallback(
|
||||||
|
@ -270,6 +283,9 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
|
||||||
|
|
||||||
const { rowClassRules } = useCurrentSongRowStyles({ tableRef });
|
const { rowClassRules } = useCurrentSongRowStyles({ tableRef });
|
||||||
|
|
||||||
|
const canDrag =
|
||||||
|
filters.sortBy === SongListSort.ID && !detailQuery?.data?.rules && !iSClientSide;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<VirtualGridAutoSizerContainer>
|
<VirtualGridAutoSizerContainer>
|
||||||
|
@ -289,16 +305,17 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
|
||||||
status,
|
status,
|
||||||
}}
|
}}
|
||||||
getRowId={(data) => data.data.uniqueId}
|
getRowId={(data) => data.data.uniqueId}
|
||||||
infiniteInitialRowCount={checkPlaylistList.data?.totalRecordCount || 100}
|
infiniteInitialRowCount={
|
||||||
|
iSClientSide ? undefined : checkPlaylistList.data?.totalRecordCount || 100
|
||||||
|
}
|
||||||
pagination={isPaginationEnabled}
|
pagination={isPaginationEnabled}
|
||||||
paginationAutoPageSize={isPaginationEnabled}
|
paginationAutoPageSize={isPaginationEnabled}
|
||||||
paginationPageSize={pagination.itemsPerPage || 100}
|
paginationPageSize={pagination.itemsPerPage || 100}
|
||||||
rowClassRules={rowClassRules}
|
rowClassRules={rowClassRules}
|
||||||
rowDragEntireRow={
|
rowData={songs}
|
||||||
filters.sortBy === SongListSort.ID && !detailQuery?.data?.rules
|
rowDragEntireRow={canDrag}
|
||||||
}
|
|
||||||
rowHeight={page.table.rowHeight || 40}
|
rowHeight={page.table.rowHeight || 40}
|
||||||
rowModelType="infinite"
|
rowModelType={iSClientSide ? 'clientSide' : 'infinite'}
|
||||||
onBodyScrollEnd={handleScroll}
|
onBodyScrollEnd={handleScroll}
|
||||||
onCellContextMenu={handleContextMenu}
|
onCellContextMenu={handleContextMenu}
|
||||||
onColumnMoved={handleColumnChange}
|
onColumnMoved={handleColumnChange}
|
||||||
|
|
|
@ -195,6 +195,68 @@ const FILTERS = {
|
||||||
value: SongListSort.YEAR,
|
value: SongListSort.YEAR,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
subsonic: [
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.id', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.album', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.ALBUM,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.ALBUM_ARTIST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.ARTIST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.DURATION,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.FAVORITED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.genre', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.GENRE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.NAME,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.RATING,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.RECENTLY_ADDED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.RECENTLY_PLAYED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.YEAR,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PlaylistDetailSongListHeaderFiltersProps {
|
interface PlaylistDetailSongListHeaderFiltersProps {
|
||||||
|
@ -241,43 +303,55 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
||||||
|
|
||||||
const handleFilterChange = useCallback(
|
const handleFilterChange = useCallback(
|
||||||
async (filters: SongListFilter) => {
|
async (filters: SongListFilter) => {
|
||||||
const dataSource: IDatasource = {
|
if (server?.type !== ServerType.SUBSONIC) {
|
||||||
getRows: async (params) => {
|
const dataSource: IDatasource = {
|
||||||
const limit = params.endRow - params.startRow;
|
getRows: async (params) => {
|
||||||
const startIndex = params.startRow;
|
const limit = params.endRow - params.startRow;
|
||||||
|
const startIndex = params.startRow;
|
||||||
|
|
||||||
const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId, {
|
const queryKey = queryKeys.playlists.songList(
|
||||||
id: playlistId,
|
server?.id || '',
|
||||||
limit,
|
playlistId,
|
||||||
startIndex,
|
{
|
||||||
...filters,
|
id: playlistId,
|
||||||
});
|
limit,
|
||||||
|
startIndex,
|
||||||
|
...filters,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const songsRes = await queryClient.fetchQuery(
|
const songsRes = await queryClient.fetchQuery(
|
||||||
queryKey,
|
queryKey,
|
||||||
async ({ signal }) =>
|
async ({ signal }) =>
|
||||||
api.controller.getPlaylistSongList({
|
api.controller.getPlaylistSongList({
|
||||||
apiClientProps: {
|
apiClientProps: {
|
||||||
server,
|
server,
|
||||||
signal,
|
signal,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
id: playlistId,
|
id: playlistId,
|
||||||
limit,
|
limit,
|
||||||
startIndex,
|
startIndex,
|
||||||
...filters,
|
...filters,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{ cacheTime: 1000 * 60 * 1 },
|
{ cacheTime: 1000 * 60 * 1 },
|
||||||
);
|
);
|
||||||
|
|
||||||
params.successCallback(songsRes?.items || [], songsRes?.totalRecordCount || 0);
|
params.successCallback(
|
||||||
},
|
songsRes?.items || [],
|
||||||
rowCount: undefined,
|
songsRes?.totalRecordCount || 0,
|
||||||
};
|
);
|
||||||
tableRef.current?.api.setDatasource(dataSource);
|
},
|
||||||
tableRef.current?.api.purgeInfiniteCache();
|
rowCount: undefined,
|
||||||
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
};
|
||||||
|
tableRef.current?.api.setDatasource(dataSource);
|
||||||
|
tableRef.current?.api.purgeInfiniteCache();
|
||||||
|
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||||
|
} else {
|
||||||
|
tableRef.current?.api.redrawRows();
|
||||||
|
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||||
|
}
|
||||||
|
|
||||||
if (page.display === ListDisplayType.TABLE_PAGINATED) {
|
if (page.display === ListDisplayType.TABLE_PAGINATED) {
|
||||||
setPagination({ data: { currentPage: 0 } });
|
setPagination({ data: { currentPage: 0 } });
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
} from '/@/renderer/components/virtual-grid';
|
} from '/@/renderer/components/virtual-grid';
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useCurrentServer, useGeneralSettings, useListStoreByKey } from '/@/renderer/store';
|
import { useCurrentServer, useListStoreByKey } from '/@/renderer/store';
|
||||||
import { CardRow, ListDisplayType } from '/@/renderer/types';
|
import { CardRow, ListDisplayType } from '/@/renderer/types';
|
||||||
import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite';
|
import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite';
|
||||||
|
|
||||||
|
@ -35,15 +35,12 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
const { display, grid, filter } = useListStoreByKey({ key: pageKey });
|
const { display, grid, filter } = useListStoreByKey<PlaylistListQuery>({ key: pageKey });
|
||||||
const { setGrid } = useListStoreActions();
|
const { setGrid } = useListStoreActions();
|
||||||
const { defaultFullPlaylist } = useGeneralSettings();
|
|
||||||
const handleFavorite = useHandleFavorite({ gridRef, server });
|
const handleFavorite = useHandleFavorite({ gridRef, server });
|
||||||
|
|
||||||
const cardRows = useMemo(() => {
|
const cardRows = useMemo(() => {
|
||||||
const rows: CardRow<Playlist>[] = defaultFullPlaylist
|
const rows: CardRow<Playlist>[] = [PLAYLIST_CARD_ROWS.nameFull];
|
||||||
? [PLAYLIST_CARD_ROWS.nameFull]
|
|
||||||
: [PLAYLIST_CARD_ROWS.name];
|
|
||||||
|
|
||||||
switch (filter.sortBy) {
|
switch (filter.sortBy) {
|
||||||
case PlaylistListSort.DURATION:
|
case PlaylistListSort.DURATION:
|
||||||
|
@ -66,7 +63,7 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
|
||||||
}
|
}
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
}, [defaultFullPlaylist, filter.sortBy]);
|
}, [filter.sortBy]);
|
||||||
|
|
||||||
const handleGridScroll = useCallback(
|
const handleGridScroll = useCallback(
|
||||||
(e: ListOnScrollProps) => {
|
(e: ListOnScrollProps) => {
|
||||||
|
@ -116,9 +113,9 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
|
||||||
|
|
||||||
const query: PlaylistListQuery = {
|
const query: PlaylistListQuery = {
|
||||||
limit: take,
|
limit: take,
|
||||||
startIndex: skip,
|
|
||||||
...filter,
|
...filter,
|
||||||
_custom: {},
|
_custom: {},
|
||||||
|
startIndex: skip,
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryKey = queryKeys.playlists.list(server?.id || '', query);
|
const queryKey = queryKeys.playlists.list(server?.id || '', query);
|
||||||
|
@ -160,9 +157,7 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
|
||||||
loading={itemCount === undefined || itemCount === null}
|
loading={itemCount === undefined || itemCount === null}
|
||||||
minimumBatchSize={40}
|
minimumBatchSize={40}
|
||||||
route={{
|
route={{
|
||||||
route: defaultFullPlaylist
|
route: AppRoute.PLAYLISTS_DETAIL_SONGS,
|
||||||
? AppRoute.PLAYLISTS_DETAIL_SONGS
|
|
||||||
: AppRoute.PLAYLISTS_DETAIL,
|
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'playlistId' }],
|
slugs: [{ idProperty: 'id', slugProperty: 'playlistId' }],
|
||||||
}}
|
}}
|
||||||
width={width}
|
width={width}
|
||||||
|
|
|
@ -69,6 +69,38 @@ const FILTERS = {
|
||||||
value: PlaylistListSort.UPDATED_AT,
|
value: PlaylistListSort.UPDATED_AT,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
subsonic: [
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
|
||||||
|
value: PlaylistListSort.DURATION,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||||
|
value: PlaylistListSort.NAME,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.owner', { postProcess: 'titleCase' }),
|
||||||
|
value: PlaylistListSort.OWNER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.isPublic', { postProcess: 'titleCase' }),
|
||||||
|
value: PlaylistListSort.PUBLIC,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
|
||||||
|
value: PlaylistListSort.SONG_COUNT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: i18n.t('filter.recentlyUpdated', { postProcess: 'titleCase' }),
|
||||||
|
value: PlaylistListSort.UPDATED_AT,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PlaylistListHeaderFiltersProps {
|
interface PlaylistListHeaderFiltersProps {
|
||||||
|
@ -86,7 +118,7 @@ export const PlaylistListHeaderFilters = ({
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { setFilter, setTable, setTablePagination, setGrid, setDisplayType } =
|
const { setFilter, setTable, setTablePagination, setGrid, setDisplayType } =
|
||||||
useListStoreActions();
|
useListStoreActions();
|
||||||
const { display, filter, table, grid } = useListStoreByKey({ key: pageKey });
|
const { display, filter, table, grid } = useListStoreByKey<PlaylistListQuery>({ key: pageKey });
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
|
|
||||||
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER;
|
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER;
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { PlaylistListFilter, useCurrentServer } from '/@/renderer/store';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { RiFileAddFill } from 'react-icons/ri';
|
import { RiFileAddFill } from 'react-icons/ri';
|
||||||
import { LibraryItem, ServerType } from '/@/renderer/api/types';
|
import { LibraryItem, PlaylistListQuery, ServerType } from '/@/renderer/api/types';
|
||||||
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
|
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
|
||||||
|
|
||||||
interface PlaylistListHeaderProps {
|
interface PlaylistListHeaderProps {
|
||||||
|
@ -37,8 +37,9 @@ export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistLis
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { filter, refresh, search } = useDisplayRefresh({
|
const { filter, refresh, search } = useDisplayRefresh<PlaylistListQuery>({
|
||||||
gridRef,
|
gridRef,
|
||||||
|
itemCount,
|
||||||
itemType: LibraryItem.PLAYLIST,
|
itemType: LibraryItem.PLAYLIST,
|
||||||
server,
|
server,
|
||||||
tableRef,
|
tableRef,
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { VirtualTable } from '/@/renderer/components/virtual-table';
|
||||||
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
|
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
|
||||||
import { PLAYLIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
import { PLAYLIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
|
||||||
interface PlaylistListTableViewProps {
|
interface PlaylistListTableViewProps {
|
||||||
itemCount?: number;
|
itemCount?: number;
|
||||||
|
@ -18,16 +18,11 @@ interface PlaylistListTableViewProps {
|
||||||
export const PlaylistListTableView = ({ tableRef, itemCount }: PlaylistListTableViewProps) => {
|
export const PlaylistListTableView = ({ tableRef, itemCount }: PlaylistListTableViewProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { defaultFullPlaylist } = useGeneralSettings();
|
|
||||||
const pageKey = 'playlist';
|
const pageKey = 'playlist';
|
||||||
|
|
||||||
const handleRowDoubleClick = (e: RowDoubleClickedEvent) => {
|
const handleRowDoubleClick = (e: RowDoubleClickedEvent) => {
|
||||||
if (!e.data) return;
|
if (!e.data) return;
|
||||||
if (defaultFullPlaylist) {
|
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: e.data.id }));
|
||||||
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: e.data.id }));
|
|
||||||
} else {
|
|
||||||
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL, { playlistId: e.data.id }));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableProps = useVirtualTable({
|
const tableProps = useVirtualTable({
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import type { PlaylistSongListQuery, PlaylistSongListResponse } from '/@/renderer/api/types';
|
import type { PlaylistSongListQuery } from '/@/renderer/api/types';
|
||||||
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
|
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||||
import { getServerById } from '/@/renderer/store';
|
import { getServerById } from '/@/renderer/store';
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
|
@ -22,32 +22,3 @@ export const usePlaylistSongList = (args: QueryHookArgs<PlaylistSongListQuery>)
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePlaylistSongListInfinite = (args: QueryHookArgs<PlaylistSongListQuery>) => {
|
|
||||||
const { options, query, serverId } = args || {};
|
|
||||||
const server = getServerById(serverId);
|
|
||||||
|
|
||||||
return useInfiniteQuery({
|
|
||||||
enabled: !!server,
|
|
||||||
getNextPageParam: (lastPage: PlaylistSongListResponse | undefined, pages) => {
|
|
||||||
if (!lastPage?.items) return undefined;
|
|
||||||
if (lastPage?.items?.length >= (query?.limit || 50)) {
|
|
||||||
return pages?.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
queryFn: ({ pageParam = 0, signal }) => {
|
|
||||||
return api.controller.getPlaylistSongList({
|
|
||||||
apiClientProps: { server, signal },
|
|
||||||
query: {
|
|
||||||
...query,
|
|
||||||
limit: query.limit || 50,
|
|
||||||
startIndex: pageParam * (query.limit || 50),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
queryKey: queryKeys.playlists.detailSongList(server?.id || '', query.id, query),
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
import { useRef } from 'react';
|
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
|
||||||
import { useParams } from 'react-router';
|
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
|
||||||
import { NativeScrollArea, Spinner } from '/@/renderer/components';
|
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
|
||||||
import { PlaylistDetailContent } from '/@/renderer/features/playlists/components/playlist-detail-content';
|
|
||||||
import { PlaylistDetailHeader } from '/@/renderer/features/playlists/components/playlist-detail-header';
|
|
||||||
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
|
||||||
import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared';
|
|
||||||
import { useFastAverageColor } from '/@/renderer/hooks';
|
|
||||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
|
||||||
import { useCurrentServer } from '../../../store/auth.store';
|
|
||||||
|
|
||||||
const PlaylistDetailRoute = () => {
|
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
|
||||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
|
||||||
const headerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const { playlistId } = useParams() as { playlistId: string };
|
|
||||||
const server = useCurrentServer();
|
|
||||||
|
|
||||||
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
|
|
||||||
const { color: background, colorId } = useFastAverageColor({
|
|
||||||
algorithm: 'sqrt',
|
|
||||||
id: playlistId,
|
|
||||||
src: detailQuery?.data?.imageUrl,
|
|
||||||
srcLoaded: !detailQuery?.isLoading,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
|
||||||
|
|
||||||
const handlePlay = () => {
|
|
||||||
handlePlayQueueAdd?.({
|
|
||||||
byItemType: {
|
|
||||||
id: [playlistId],
|
|
||||||
type: LibraryItem.PLAYLIST,
|
|
||||||
},
|
|
||||||
playType: playButtonBehavior,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!background || colorId !== playlistId) {
|
|
||||||
return <Spinner container />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AnimatedPage key={`playlist-detail-${playlistId}`}>
|
|
||||||
<NativeScrollArea
|
|
||||||
ref={scrollAreaRef}
|
|
||||||
pageHeaderProps={{
|
|
||||||
backgroundColor: background,
|
|
||||||
children: (
|
|
||||||
<LibraryHeaderBar>
|
|
||||||
<LibraryHeaderBar.PlayButton onClick={handlePlay} />
|
|
||||||
<LibraryHeaderBar.Title>
|
|
||||||
{detailQuery?.data?.name}
|
|
||||||
</LibraryHeaderBar.Title>
|
|
||||||
</LibraryHeaderBar>
|
|
||||||
),
|
|
||||||
offset: 200,
|
|
||||||
target: headerRef,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlaylistDetailHeader
|
|
||||||
ref={headerRef}
|
|
||||||
background={background}
|
|
||||||
imagePlaceholderUrl={detailQuery?.data?.imageUrl}
|
|
||||||
imageUrl={detailQuery?.data?.imageUrl}
|
|
||||||
/>
|
|
||||||
<PlaylistDetailContent tableRef={tableRef} />
|
|
||||||
</NativeScrollArea>
|
|
||||||
</AnimatedPage>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PlaylistDetailRoute;
|
|
|
@ -144,10 +144,6 @@ const PlaylistDetailSongListRoute = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemCountCheck = usePlaylistSongList({
|
const itemCountCheck = usePlaylistSongList({
|
||||||
options: {
|
|
||||||
cacheTime: 1000 * 60 * 60 * 2,
|
|
||||||
staleTime: 1000 * 60 * 60 * 2,
|
|
||||||
},
|
|
||||||
query: {
|
query: {
|
||||||
id: playlistId,
|
id: playlistId,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
@ -157,10 +153,7 @@ const PlaylistDetailSongListRoute = () => {
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemCount =
|
const itemCount = itemCountCheck.data?.totalRecordCount || itemCountCheck.data?.items.length;
|
||||||
itemCountCheck.data?.totalRecordCount === null
|
|
||||||
? undefined
|
|
||||||
: itemCountCheck.data?.totalRecordCount;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
<AnimatedPage key={`playlist-detail-songList-${playlistId}`}>
|
||||||
|
@ -207,7 +200,12 @@ const PlaylistDetailSongListRoute = () => {
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<PlaylistDetailSongListContent tableRef={tableRef} />
|
<PlaylistDetailSongListContent
|
||||||
|
songs={
|
||||||
|
server?.type === ServerType.SUBSONIC ? itemCountCheck.data?.items : undefined
|
||||||
|
}
|
||||||
|
tableRef={tableRef}
|
||||||
|
/>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { PlaylistListSort, SortOrder } from '/@/renderer/api/types';
|
import { PlaylistListSort, PlaylistSongListQuery, SortOrder } from '/@/renderer/api/types';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
import { ListContext } from '/@/renderer/context/list-context';
|
import { ListContext } from '/@/renderer/context/list-context';
|
||||||
import { PlaylistListContent } from '/@/renderer/features/playlists/components/playlist-list-content';
|
import { PlaylistListContent } from '/@/renderer/features/playlists/components/playlist-list-content';
|
||||||
|
@ -16,7 +16,7 @@ const PlaylistListRoute = () => {
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { playlistId } = useParams();
|
const { playlistId } = useParams();
|
||||||
const pageKey = 'playlist';
|
const pageKey = 'playlist';
|
||||||
const { filter } = useListStoreByKey({ key: pageKey });
|
const { filter } = useListStoreByKey<PlaylistSongListQuery>({ key: pageKey });
|
||||||
|
|
||||||
const itemCountCheck = usePlaylistList({
|
const itemCountCheck = usePlaylistList({
|
||||||
options: {
|
options: {
|
||||||
|
|
|
@ -5,7 +5,12 @@ import debounce from 'lodash/debounce';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { generatePath, Link, useParams, useSearchParams } from 'react-router-dom';
|
import { generatePath, Link, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { useCurrentServer } from '../../../store/auth.store';
|
import { useCurrentServer } from '../../../store/auth.store';
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
import {
|
||||||
|
AlbumArtistListQuery,
|
||||||
|
AlbumListQuery,
|
||||||
|
LibraryItem,
|
||||||
|
SongListQuery,
|
||||||
|
} from '/@/renderer/api/types';
|
||||||
import { Button, PageHeader, SearchInput } from '/@/renderer/components';
|
import { Button, PageHeader, SearchInput } from '/@/renderer/components';
|
||||||
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
|
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
|
@ -24,7 +29,9 @@ export const SearchHeader = ({ tableRef, navigationId }: SearchHeaderProps) => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { filter } = useListStoreByKey({ key: itemType });
|
const { filter } = useListStoreByKey<AlbumListQuery | AlbumArtistListQuery | SongListQuery>({
|
||||||
|
key: itemType,
|
||||||
|
});
|
||||||
|
|
||||||
const { handleRefreshTable } = useListFilterRefresh({
|
const { handleRefreshTable } = useListFilterRefresh({
|
||||||
itemType,
|
itemType,
|
||||||
|
|
|
@ -17,7 +17,7 @@ const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||||
const SERVER_TYPES = [
|
const SERVER_TYPES = [
|
||||||
{ label: 'Jellyfin', value: ServerType.JELLYFIN },
|
{ label: 'Jellyfin', value: ServerType.JELLYFIN },
|
||||||
{ label: 'Navidrome', value: ServerType.NAVIDROME },
|
{ label: 'Navidrome', value: ServerType.NAVIDROME },
|
||||||
// { label: 'Subsonic', value: ServerType.SUBSONIC },
|
{ label: 'Subsonic', value: ServerType.SUBSONIC },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface AddServerFormProps {
|
interface AddServerFormProps {
|
||||||
|
|
|
@ -375,28 +375,6 @@ export const ControlSettings = () => {
|
||||||
isHidden: !isElectron(),
|
isHidden: !isElectron(),
|
||||||
title: t('setting.savePlayQueue', { postProcess: 'sentenceCase' }),
|
title: t('setting.savePlayQueue', { postProcess: 'sentenceCase' }),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
control: (
|
|
||||||
<Switch
|
|
||||||
aria-label="Go to playlist songs page by default"
|
|
||||||
defaultChecked={settings.defaultFullPlaylist}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSettings({
|
|
||||||
general: {
|
|
||||||
...settings,
|
|
||||||
defaultFullPlaylist: e.currentTarget.checked,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
description: t('setting.skipPlaylistPage', {
|
|
||||||
context: 'description',
|
|
||||||
postProcess: 'sentenceCase',
|
|
||||||
}),
|
|
||||||
isHidden: false,
|
|
||||||
title: t('setting.skipPlaylistPage', { postProcess: 'sentenceCase' }),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
|
|
|
@ -35,7 +35,7 @@ export const useSetRating = (args: MutationHookArgs) => {
|
||||||
mutationFn: (args) => {
|
mutationFn: (args) => {
|
||||||
const server = getServerById(args.serverId);
|
const server = getServerById(args.serverId);
|
||||||
if (!server) throw new Error('Server not found');
|
if (!server) throw new Error('Server not found');
|
||||||
return api.controller.updateRating({ ...args, apiClientProps: { server } });
|
return api.controller.setRating({ ...args, apiClientProps: { server } });
|
||||||
},
|
},
|
||||||
onError: (_error, _variables, context) => {
|
onError: (_error, _variables, context) => {
|
||||||
for (const item of context?.previous?.items || []) {
|
for (const item of context?.previous?.items || []) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
} from 'react-icons/ri';
|
} from 'react-icons/ri';
|
||||||
import { generatePath } from 'react-router';
|
import { generatePath } from 'react-router';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { LibraryItem, Playlist } from '/@/renderer/api/types';
|
import { LibraryItem, Playlist, PlaylistListSort, SortOrder } from '/@/renderer/api/types';
|
||||||
import { Button, Text } from '/@/renderer/components';
|
import { Button, Text } from '/@/renderer/components';
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
import { usePlaylistList } from '/@/renderer/features/playlists';
|
import { usePlaylistList } from '/@/renderer/features/playlists';
|
||||||
|
@ -24,10 +24,6 @@ import { useCurrentServer, useGeneralSettings, useSettingsStoreActions } from '/
|
||||||
import { openContextMenu } from '/@/renderer/features/context-menu';
|
import { openContextMenu } from '/@/renderer/features/context-menu';
|
||||||
import { PLAYLIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
import { PLAYLIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
||||||
|
|
||||||
interface SidebarPlaylistListProps {
|
|
||||||
data: ReturnType<typeof usePlaylistList>['data'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const PlaylistRow = ({ index, data, style }: ListChildComponentProps) => {
|
const PlaylistRow = ({ index, data, style }: ListChildComponentProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
@ -66,11 +62,7 @@ const PlaylistRow = ({ index, data, style }: ListChildComponentProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = data?.items[index].id
|
const path = data?.items[index].id
|
||||||
? data.defaultFullPlaylist
|
? generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: data.items[index].id })
|
||||||
? generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: data.items[index].id })
|
|
||||||
: generatePath(AppRoute.PLAYLISTS_DETAIL, {
|
|
||||||
playlistId: data?.items[index].id,
|
|
||||||
})
|
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -181,12 +173,21 @@ const PlaylistRow = ({ index, data, style }: ListChildComponentProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => {
|
export const SidebarPlaylistList = () => {
|
||||||
const { isScrollbarHidden, hideScrollbarElementProps } = useHideScrollbar(0);
|
const { isScrollbarHidden, hideScrollbarElementProps } = useHideScrollbar(0);
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
const { defaultFullPlaylist, sidebarCollapseShared } = useGeneralSettings();
|
const { sidebarCollapseShared } = useGeneralSettings();
|
||||||
const { toggleSidebarCollapseShare } = useSettingsStoreActions();
|
const { toggleSidebarCollapseShare } = useSettingsStoreActions();
|
||||||
const { type, username } = useCurrentServer() || {};
|
const server = useCurrentServer();
|
||||||
|
|
||||||
|
const playlistsQuery = usePlaylistList({
|
||||||
|
query: {
|
||||||
|
sortBy: PlaylistListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId: server?.id,
|
||||||
|
});
|
||||||
|
|
||||||
const [rect, setRect] = useState({
|
const [rect, setRect] = useState({
|
||||||
height: 0,
|
height: 0,
|
||||||
|
@ -208,10 +209,12 @@ export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => {
|
||||||
[handlePlayQueueAdd],
|
[handlePlayQueueAdd],
|
||||||
);
|
);
|
||||||
|
|
||||||
const memoizedItemData = useMemo(() => {
|
const data = playlistsQuery.data;
|
||||||
const base = { defaultFullPlaylist, handlePlay: handlePlayPlaylist };
|
|
||||||
|
|
||||||
if (!type || !username || !data?.items) {
|
const memoizedItemData = useMemo(() => {
|
||||||
|
const base = { handlePlay: handlePlayPlaylist };
|
||||||
|
|
||||||
|
if (!server?.type || !server?.username || !data?.items) {
|
||||||
return { ...base, items: data?.items };
|
return { ...base, items: data?.items };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +222,7 @@ export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => {
|
||||||
const shared: Playlist[] = [];
|
const shared: Playlist[] = [];
|
||||||
|
|
||||||
for (const playlist of data.items) {
|
for (const playlist of data.items) {
|
||||||
if (playlist.owner && playlist.owner !== username) {
|
if (playlist.owner && playlist.owner !== server.username) {
|
||||||
shared.push(playlist);
|
shared.push(playlist);
|
||||||
} else {
|
} else {
|
||||||
owned.push(playlist);
|
owned.push(playlist);
|
||||||
|
@ -234,12 +237,11 @@ export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => {
|
||||||
|
|
||||||
return { ...base, items: final };
|
return { ...base, items: final };
|
||||||
}, [
|
}, [
|
||||||
sidebarCollapseShared,
|
|
||||||
data?.items,
|
data?.items,
|
||||||
defaultFullPlaylist,
|
|
||||||
handlePlayPlaylist,
|
handlePlayPlaylist,
|
||||||
type,
|
server?.type,
|
||||||
username,
|
server?.username,
|
||||||
|
sidebarCollapseShared,
|
||||||
toggleSidebarCollapseShare,
|
toggleSidebarCollapseShare,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,9 @@ import {
|
||||||
useGeneralSettings,
|
useGeneralSettings,
|
||||||
useWindowSettings,
|
useWindowSettings,
|
||||||
} from '../../../store/settings.store';
|
} from '../../../store/settings.store';
|
||||||
import { PlaylistListSort, ServerType, SortOrder } from '/@/renderer/api/types';
|
import { ServerType } from '/@/renderer/api/types';
|
||||||
import { Button, MotionStack, Spinner, Tooltip } from '/@/renderer/components';
|
import { Button, MotionStack, Tooltip } from '/@/renderer/components';
|
||||||
import { CreatePlaylistForm, usePlaylistList } from '/@/renderer/features/playlists';
|
import { CreatePlaylistForm } from '/@/renderer/features/playlists';
|
||||||
import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar';
|
import { ActionBar } from '/@/renderer/features/sidebar/components/action-bar';
|
||||||
import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon';
|
import { SidebarIcon } from '/@/renderer/features/sidebar/components/sidebar-icon';
|
||||||
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
|
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
|
||||||
|
@ -110,15 +110,6 @@ export const Sidebar = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const playlistsQuery = usePlaylistList({
|
|
||||||
query: {
|
|
||||||
sortBy: PlaylistListSort.NAME,
|
|
||||||
sortOrder: SortOrder.ASC,
|
|
||||||
startIndex: 0,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
|
const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
|
||||||
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
|
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
|
||||||
const expandFullScreenPlayer = () => {
|
const expandFullScreenPlayer = () => {
|
||||||
|
@ -198,7 +189,6 @@ export const Sidebar = () => {
|
||||||
>
|
>
|
||||||
{t('page.sidebar.playlists', { postProcess: 'titleCase' })}
|
{t('page.sidebar.playlists', { postProcess: 'titleCase' })}
|
||||||
</Box>
|
</Box>
|
||||||
{playlistsQuery.isLoading && <Spinner />}
|
|
||||||
</Group>
|
</Group>
|
||||||
<Group spacing="sm">
|
<Group spacing="sm">
|
||||||
<Button
|
<Button
|
||||||
|
@ -233,7 +223,7 @@ export const Sidebar = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<SidebarPlaylistList data={playlistsQuery.data} />
|
<SidebarPlaylistList />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</MotionStack>
|
</MotionStack>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ChangeEvent, useMemo } from 'react';
|
import { ChangeEvent, useMemo } from 'react';
|
||||||
import { Divider, Group, Stack } from '@mantine/core';
|
import { Divider, Group, Stack } from '@mantine/core';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/renderer/api/types';
|
||||||
import { MultiSelect, NumberInput, Switch, Text } from '/@/renderer/components';
|
import { MultiSelect, NumberInput, Switch, Text } from '/@/renderer/components';
|
||||||
import { useGenreList } from '/@/renderer/features/genres';
|
import { useGenreList } from '/@/renderer/features/genres';
|
||||||
import { SongListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store';
|
import { SongListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store';
|
||||||
|
@ -22,7 +22,7 @@ export const JellyfinSongFilters = ({
|
||||||
}: JellyfinSongFiltersProps) => {
|
}: JellyfinSongFiltersProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setFilter } = useListStoreActions();
|
const { setFilter } = useListStoreActions();
|
||||||
const filter = useListFilterByKey({ key: pageKey });
|
const filter = useListFilterByKey<SongListQuery>({ key: pageKey });
|
||||||
|
|
||||||
const isGenrePage = customFilters?._custom?.jellyfin?.GenreIds !== undefined;
|
const isGenrePage = customFilters?._custom?.jellyfin?.GenreIds !== undefined;
|
||||||
|
|
||||||
|
@ -61,16 +61,16 @@ export const JellyfinSongFilters = ({
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
...filter?._custom?.jellyfin,
|
...filter?._custom?.jellyfin,
|
||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
IsFavorite: e.currentTarget.checked ? true : undefined,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
favorite: e.currentTarget.checked ? true : undefined,
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.SONG,
|
itemType: LibraryItem.SONG,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as SongListFilter;
|
}) as SongListFilter;
|
||||||
onFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
},
|
},
|
||||||
value: filter?._custom?.jellyfin?.IsFavorite,
|
value: filter.favorite,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -84,9 +84,9 @@ export const JellyfinSongFilters = ({
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
...filter?._custom?.jellyfin,
|
...filter?._custom?.jellyfin,
|
||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
minYear: e === '' ? undefined : (e as number),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
minYear: e === '' ? undefined : (e as number),
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.SONG,
|
itemType: LibraryItem.SONG,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
|
@ -104,9 +104,9 @@ export const JellyfinSongFilters = ({
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
...filter?._custom?.jellyfin,
|
...filter?._custom?.jellyfin,
|
||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
maxYear: e === '' ? undefined : (e as number),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
maxYear: e === '' ? undefined : (e as number),
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.SONG,
|
itemType: LibraryItem.SONG,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
|
@ -115,7 +115,6 @@ export const JellyfinSongFilters = ({
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
const handleGenresFilter = debounce((e: string[] | undefined) => {
|
const handleGenresFilter = debounce((e: string[] | undefined) => {
|
||||||
const genreFilterString = e?.length ? e.join(',') : undefined;
|
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
|
@ -123,10 +122,10 @@ export const JellyfinSongFilters = ({
|
||||||
...filter?._custom,
|
...filter?._custom,
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
...filter?._custom?.jellyfin,
|
...filter?._custom?.jellyfin,
|
||||||
GenreIds: genreFilterString,
|
|
||||||
IncludeItemTypes: 'Audio',
|
IncludeItemTypes: 'Audio',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
genreIds: e,
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.SONG,
|
itemType: LibraryItem.SONG,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
|
@ -151,18 +150,19 @@ export const JellyfinSongFilters = ({
|
||||||
<Divider my="0.5rem" />
|
<Divider my="0.5rem" />
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
defaultValue={filter?.minYear}
|
||||||
defaultValue={filter?._custom?.jellyfin?.minYear}
|
|
||||||
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
||||||
max={2300}
|
max={2300}
|
||||||
min={1700}
|
min={1700}
|
||||||
|
required={!!filter?.minYear}
|
||||||
onChange={handleMinYearFilter}
|
onChange={handleMinYearFilter}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
defaultValue={filter?._custom?.jellyfin?.maxYear}
|
defaultValue={filter?.maxYear}
|
||||||
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
||||||
max={2300}
|
max={2300}
|
||||||
min={1700}
|
min={1700}
|
||||||
|
required={!!filter?.minYear}
|
||||||
onChange={handleMaxYearFilter}
|
onChange={handleMaxYearFilter}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ChangeEvent, useMemo } from 'react';
|
import { ChangeEvent, useMemo } from 'react';
|
||||||
import { Divider, Group, Stack } from '@mantine/core';
|
import { Divider, Group, Stack } from '@mantine/core';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/renderer/api/types';
|
||||||
import { NumberInput, Select, Switch, Text } from '/@/renderer/components';
|
import { NumberInput, Select, Switch, Text } from '/@/renderer/components';
|
||||||
import { useGenreList } from '/@/renderer/features/genres';
|
import { useGenreList } from '/@/renderer/features/genres';
|
||||||
import { SongListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store';
|
import { SongListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store';
|
||||||
|
@ -22,9 +22,9 @@ export const NavidromeSongFilters = ({
|
||||||
}: NavidromeSongFiltersProps) => {
|
}: NavidromeSongFiltersProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setFilter } = useListStoreActions();
|
const { setFilter } = useListStoreActions();
|
||||||
const filter = useListFilterByKey({ key: pageKey });
|
const filter = useListFilterByKey<SongListQuery>({ key: pageKey });
|
||||||
|
|
||||||
const isGenrePage = customFilters?._custom?.navidrome?.genre_id !== undefined;
|
const isGenrePage = customFilters?.genreIds !== undefined;
|
||||||
|
|
||||||
const genreListQuery = useGenreList({
|
const genreListQuery = useGenreList({
|
||||||
query: {
|
query: {
|
||||||
|
@ -47,12 +47,8 @@ export const NavidromeSongFilters = ({
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: filter._custom,
|
||||||
...filter._custom,
|
genreIds: e ? [e] : undefined,
|
||||||
navidrome: {
|
|
||||||
genre_id: e || undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.SONG,
|
itemType: LibraryItem.SONG,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
|
@ -68,12 +64,8 @@ export const NavidromeSongFilters = ({
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
customFilters,
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: filter._custom,
|
||||||
...filter._custom,
|
favorite: e.currentTarget.checked ? true : undefined,
|
||||||
navidrome: {
|
|
||||||
starred: e.currentTarget.checked ? true : undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.SONG,
|
itemType: LibraryItem.SONG,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
|
@ -81,7 +73,7 @@ export const NavidromeSongFilters = ({
|
||||||
|
|
||||||
onFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
},
|
},
|
||||||
value: filter._custom?.navidrome?.starred,
|
value: filter.favorite,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -133,7 +125,7 @@ export const NavidromeSongFilters = ({
|
||||||
clearable
|
clearable
|
||||||
searchable
|
searchable
|
||||||
data={genreList}
|
data={genreList}
|
||||||
defaultValue={filter._custom?.navidrome?.genre_id}
|
defaultValue={filter.genreIds ? filter.genreIds[0] : undefined}
|
||||||
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
||||||
width={150}
|
width={150}
|
||||||
onChange={handleGenresFilter}
|
onChange={handleGenresFilter}
|
||||||
|
|
|
@ -36,7 +36,7 @@ export const SongListGridView = ({ gridRef, itemCount }: SongListGridViewProps)
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
const { pageKey, customFilters, id } = useListContext();
|
const { pageKey, customFilters, id } = useListContext();
|
||||||
const { grid, display, filter } = useListStoreByKey({ key: pageKey });
|
const { grid, display, filter } = useListStoreByKey<SongListQuery>({ key: pageKey });
|
||||||
const { setGrid } = useListStoreActions();
|
const { setGrid } = useListStoreActions();
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
@ -174,9 +174,9 @@ export const SongListGridView = ({ gridRef, itemCount }: SongListGridViewProps)
|
||||||
const query: SongListQuery = {
|
const query: SongListQuery = {
|
||||||
imageSize: 250,
|
imageSize: 250,
|
||||||
limit: take,
|
limit: take,
|
||||||
startIndex: skip,
|
|
||||||
...filter,
|
...filter,
|
||||||
...customFilters,
|
...customFilters,
|
||||||
|
startIndex: skip,
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryKey = queryKeys.songs.list(server?.id || '', query, id);
|
const queryKey = queryKeys.songs.list(server?.id || '', query, id);
|
||||||
|
|
|
@ -15,7 +15,13 @@ import {
|
||||||
} from 'react-icons/ri';
|
} from 'react-icons/ri';
|
||||||
import { useListStoreByKey } from '../../../store/list.store';
|
import { useListStoreByKey } from '../../../store/list.store';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { LibraryItem, ServerType, SongListSort, SortOrder } from '/@/renderer/api/types';
|
import {
|
||||||
|
LibraryItem,
|
||||||
|
ServerType,
|
||||||
|
SongListQuery,
|
||||||
|
SongListSort,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/renderer/api/types';
|
||||||
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
|
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||||
|
@ -29,6 +35,7 @@ import { queryClient } from '/@/renderer/lib/react-query';
|
||||||
import { SongListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store';
|
import { SongListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store';
|
||||||
import { ListDisplayType, Play, TableColumn } from '/@/renderer/types';
|
import { ListDisplayType, Play, TableColumn } from '/@/renderer/types';
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
|
import { SubsonicSongFilters } from '/@/renderer/features/songs/components/subsonic-song-filter';
|
||||||
|
|
||||||
const FILTERS = {
|
const FILTERS = {
|
||||||
jellyfin: [
|
jellyfin: [
|
||||||
|
@ -165,25 +172,39 @@ const FILTERS = {
|
||||||
value: SongListSort.YEAR,
|
value: SongListSort.YEAR,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
subsonic: [
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||||
|
value: SongListSort.NAME,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SongListHeaderFiltersProps {
|
interface SongListHeaderFiltersProps {
|
||||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||||
|
itemCount?: number;
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SongListHeaderFilters = ({ gridRef, tableRef }: SongListHeaderFiltersProps) => {
|
export const SongListHeaderFilters = ({
|
||||||
|
gridRef,
|
||||||
|
itemCount,
|
||||||
|
tableRef,
|
||||||
|
}: SongListHeaderFiltersProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { pageKey, handlePlay, customFilters } = useListContext();
|
const { pageKey, handlePlay, customFilters } = useListContext();
|
||||||
const { display, table, filter, grid } = useListStoreByKey({
|
const { display, table, filter, grid } = useListStoreByKey<SongListQuery>({
|
||||||
filter: customFilters,
|
filter: customFilters,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { setFilter, setGrid, setTable, setTablePagination, setDisplayType } =
|
const { setFilter, setGrid, setTable, setTablePagination, setDisplayType } =
|
||||||
useListStoreActions();
|
useListStoreActions();
|
||||||
|
|
||||||
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
|
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
|
||||||
|
itemCount,
|
||||||
itemType: LibraryItem.SONG,
|
itemType: LibraryItem.SONG,
|
||||||
server,
|
server,
|
||||||
});
|
});
|
||||||
|
@ -392,25 +413,32 @@ export const SongListHeaderFilters = ({ gridRef, tableRef }: SongListHeaderFilte
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenFiltersModal = () => {
|
const handleOpenFiltersModal = () => {
|
||||||
|
let FilterComponent;
|
||||||
|
|
||||||
|
switch (server?.type) {
|
||||||
|
case ServerType.NAVIDROME:
|
||||||
|
FilterComponent = NavidromeSongFilters;
|
||||||
|
break;
|
||||||
|
case ServerType.JELLYFIN:
|
||||||
|
FilterComponent = JellyfinSongFilters;
|
||||||
|
break;
|
||||||
|
case ServerType.SUBSONIC:
|
||||||
|
FilterComponent = SubsonicSongFilters;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FilterComponent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
children: (
|
children: (
|
||||||
<>
|
<FilterComponent
|
||||||
{server?.type === ServerType.NAVIDROME ? (
|
customFilters={customFilters}
|
||||||
<NavidromeSongFilters
|
pageKey={pageKey}
|
||||||
customFilters={customFilters}
|
serverId={server?.id}
|
||||||
pageKey={pageKey}
|
onFilterChange={onFilterChange}
|
||||||
serverId={server?.id}
|
/>
|
||||||
onFilterChange={onFilterChange}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<JellyfinSongFilters
|
|
||||||
customFilters={customFilters}
|
|
||||||
pageKey={pageKey}
|
|
||||||
serverId={server?.id}
|
|
||||||
onFilterChange={onFilterChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
),
|
),
|
||||||
title: 'Song Filters',
|
title: 'Song Filters',
|
||||||
});
|
});
|
||||||
|
@ -429,8 +457,16 @@ export const SongListHeaderFilters = ({ gridRef, tableRef }: SongListHeaderFilte
|
||||||
.filter((value) => value !== 'Audio') // Don't account for includeItemTypes: Audio
|
.filter((value) => value !== 'Audio') // Don't account for includeItemTypes: Audio
|
||||||
.some((value) => value !== undefined);
|
.some((value) => value !== undefined);
|
||||||
|
|
||||||
return isNavidromeFilterApplied || isJellyfinFilterApplied;
|
const isGenericFilterApplied = filter?.favorite || filter?.genreIds?.length;
|
||||||
}, [filter?._custom?.jellyfin, filter?._custom?.navidrome, server?.type]);
|
|
||||||
|
return isNavidromeFilterApplied || isJellyfinFilterApplied || isGenericFilterApplied;
|
||||||
|
}, [
|
||||||
|
filter._custom?.jellyfin,
|
||||||
|
filter._custom?.navidrome,
|
||||||
|
filter?.favorite,
|
||||||
|
filter?.genreIds?.length,
|
||||||
|
server?.type,
|
||||||
|
]);
|
||||||
|
|
||||||
const isFolderFilterApplied = useMemo(() => {
|
const isFolderFilterApplied = useMemo(() => {
|
||||||
return filter.musicFolderId !== undefined;
|
return filter.musicFolderId !== undefined;
|
||||||
|
@ -467,11 +503,15 @@ export const SongListHeaderFilters = ({ gridRef, tableRef }: SongListHeaderFilte
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.Dropdown>
|
</DropdownMenu.Dropdown>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<Divider orientation="vertical" />
|
{server?.type !== ServerType.SUBSONIC && (
|
||||||
<OrderToggleButton
|
<>
|
||||||
sortOrder={filter.sortOrder}
|
<Divider orientation="vertical" />
|
||||||
onToggle={handleToggleSortOrder}
|
<OrderToggleButton
|
||||||
/>
|
sortOrder={filter.sortOrder}
|
||||||
|
onToggle={handleToggleSortOrder}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{server?.type === ServerType.JELLYFIN && (
|
{server?.type === ServerType.JELLYFIN && (
|
||||||
<>
|
<>
|
||||||
<Divider orientation="vertical" />
|
<Divider orientation="vertical" />
|
||||||
|
|
|
@ -3,7 +3,7 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
|
||||||
import { Flex, Group, Stack } from '@mantine/core';
|
import { Flex, Group, Stack } from '@mantine/core';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
import { LibraryItem, SongListQuery } from '/@/renderer/api/types';
|
||||||
import { PageHeader, SearchInput } from '/@/renderer/components';
|
import { PageHeader, SearchInput } from '/@/renderer/components';
|
||||||
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
|
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||||
import { SongListHeaderFilters } from '/@/renderer/features/songs/components/song-list-header-filters';
|
import { SongListHeaderFilters } from '/@/renderer/features/songs/components/song-list-header-filters';
|
||||||
|
@ -33,12 +33,15 @@ export const SongListHeader = ({
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
const genreRef = useRef<string>();
|
const genreRef = useRef<string>();
|
||||||
|
|
||||||
const { customFilters, filter, handlePlay, refresh, search } = useDisplayRefresh({
|
const { customFilters, filter, handlePlay, refresh, search } = useDisplayRefresh<SongListQuery>(
|
||||||
gridRef,
|
{
|
||||||
itemType: LibraryItem.SONG,
|
gridRef,
|
||||||
server,
|
itemCount,
|
||||||
tableRef,
|
itemType: LibraryItem.SONG,
|
||||||
});
|
server,
|
||||||
|
tableRef,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
|
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const updatedFilters = search(e) as SongListFilter;
|
const updatedFilters = search(e) as SongListFilter;
|
||||||
|
@ -96,6 +99,7 @@ export const SongListHeader = ({
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<SongListHeaderFilters
|
<SongListHeaderFilters
|
||||||
gridRef={gridRef}
|
gridRef={gridRef}
|
||||||
|
itemCount={itemCount}
|
||||||
tableRef={tableRef}
|
tableRef={tableRef}
|
||||||
/>
|
/>
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
|
|
112
src/renderer/features/songs/components/subsonic-song-filter.tsx
Normal file
112
src/renderer/features/songs/components/subsonic-song-filter.tsx
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
import { ChangeEvent, useMemo } from 'react';
|
||||||
|
import { Divider, Group, Stack } from '@mantine/core';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/renderer/api/types';
|
||||||
|
import { Select, Switch, Text } from '/@/renderer/components';
|
||||||
|
import { useGenreList } from '/@/renderer/features/genres';
|
||||||
|
import { SongListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface SubsonicSongFiltersProps {
|
||||||
|
customFilters?: Partial<SongListFilter>;
|
||||||
|
onFilterChange: (filters: SongListFilter) => void;
|
||||||
|
pageKey: string;
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubsonicSongFilters = ({
|
||||||
|
customFilters,
|
||||||
|
onFilterChange,
|
||||||
|
pageKey,
|
||||||
|
serverId,
|
||||||
|
}: SubsonicSongFiltersProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setFilter } = useListStoreActions();
|
||||||
|
const filter = useListFilterByKey<SongListQuery>({ key: pageKey });
|
||||||
|
|
||||||
|
const isGenrePage = customFilters?.genreIds !== undefined;
|
||||||
|
|
||||||
|
const genreListQuery = useGenreList({
|
||||||
|
query: {
|
||||||
|
sortBy: GenreListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
startIndex: 0,
|
||||||
|
},
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const genreList = useMemo(() => {
|
||||||
|
if (!genreListQuery?.data) return [];
|
||||||
|
return genreListQuery.data.items.map((genre) => ({
|
||||||
|
label: genre.name,
|
||||||
|
value: genre.id,
|
||||||
|
}));
|
||||||
|
}, [genreListQuery.data]);
|
||||||
|
|
||||||
|
const handleGenresFilter = debounce((e: string | null) => {
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
|
data: {
|
||||||
|
genreIds: e ? [e] : undefined,
|
||||||
|
},
|
||||||
|
itemType: LibraryItem.SONG,
|
||||||
|
key: pageKey,
|
||||||
|
}) as SongListFilter;
|
||||||
|
|
||||||
|
onFilterChange(updatedFilters);
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
const toggleFilters = [
|
||||||
|
{
|
||||||
|
disabled: filter.genreIds !== undefined || isGenrePage || !!filter.searchTerm,
|
||||||
|
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||||
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
|
data: {
|
||||||
|
favorite: e.target.checked,
|
||||||
|
},
|
||||||
|
itemType: LibraryItem.SONG,
|
||||||
|
key: pageKey,
|
||||||
|
}) as SongListFilter;
|
||||||
|
|
||||||
|
onFilterChange(updatedFilters);
|
||||||
|
},
|
||||||
|
value: filter.favorite,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack p="0.8rem">
|
||||||
|
{toggleFilters.map((filter) => (
|
||||||
|
<Group
|
||||||
|
key={`ss-filter-${filter.label}`}
|
||||||
|
position="apart"
|
||||||
|
>
|
||||||
|
<Text>{filter.label}</Text>
|
||||||
|
<Switch
|
||||||
|
checked={filter?.value || false}
|
||||||
|
disabled={filter.disabled}
|
||||||
|
size="xs"
|
||||||
|
onChange={filter.onChange}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
<Divider my="0.5rem" />
|
||||||
|
<Group grow>
|
||||||
|
{!isGenrePage && (
|
||||||
|
<Select
|
||||||
|
clearable
|
||||||
|
searchable
|
||||||
|
data={genreList}
|
||||||
|
defaultValue={filter.genreIds ? filter.genreIds[0] : undefined}
|
||||||
|
disabled={!!filter.searchTerm}
|
||||||
|
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
||||||
|
width={150}
|
||||||
|
onChange={handleGenresFilter}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
30
src/renderer/features/songs/queries/song-list-count-query.ts
Normal file
30
src/renderer/features/songs/queries/song-list-count-query.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import type { SongListQuery } from '/@/renderer/api/types';
|
||||||
|
import type { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||||
|
import { getServerById } from '/@/renderer/store';
|
||||||
|
|
||||||
|
export const useSongListCount = (args: QueryHookArgs<SongListQuery>) => {
|
||||||
|
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.getSongListCount({
|
||||||
|
apiClientProps: {
|
||||||
|
server,
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
queryKey: queryKeys.songs.count(
|
||||||
|
serverId || '',
|
||||||
|
Object.keys(query).length === 0 ? undefined : query,
|
||||||
|
),
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
|
@ -10,11 +10,11 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared';
|
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||||
import { SongListContent } from '/@/renderer/features/songs/components/song-list-content';
|
import { SongListContent } from '/@/renderer/features/songs/components/song-list-content';
|
||||||
import { SongListHeader } from '/@/renderer/features/songs/components/song-list-header';
|
import { SongListHeader } from '/@/renderer/features/songs/components/song-list-header';
|
||||||
import { useSongList } from '/@/renderer/features/songs/queries/song-list-query';
|
|
||||||
import { useCurrentServer, useListFilterByKey } from '/@/renderer/store';
|
import { useCurrentServer, useListFilterByKey } from '/@/renderer/store';
|
||||||
import { Play } from '/@/renderer/types';
|
import { Play } from '/@/renderer/types';
|
||||||
import { titleCase } from '/@/renderer/utils';
|
import { titleCase } from '/@/renderer/utils';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
|
import { useSongListCount } from '/@/renderer/features/songs/queries/song-list-count-query';
|
||||||
|
|
||||||
const TrackListRoute = () => {
|
const TrackListRoute = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -30,14 +30,7 @@ const TrackListRoute = () => {
|
||||||
const value = {
|
const value = {
|
||||||
...(albumArtistId && { artistIds: [albumArtistId] }),
|
...(albumArtistId && { artistIds: [albumArtistId] }),
|
||||||
...(genreId && {
|
...(genreId && {
|
||||||
_custom: {
|
genreIds: [genreId],
|
||||||
jellyfin: {
|
|
||||||
GenreIds: genreId,
|
|
||||||
},
|
|
||||||
navidrome: {
|
|
||||||
genre_id: genreId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -76,29 +69,22 @@ const TrackListRoute = () => {
|
||||||
return genre?.name;
|
return genre?.name;
|
||||||
}, [genreId, genreList.data]);
|
}, [genreId, genreList.data]);
|
||||||
|
|
||||||
const itemCountCheck = useSongList({
|
const itemCountCheck = useSongListCount({
|
||||||
options: {
|
options: {
|
||||||
cacheTime: 1000 * 60,
|
cacheTime: 1000 * 60,
|
||||||
staleTime: 1000 * 60,
|
staleTime: 1000 * 60,
|
||||||
},
|
},
|
||||||
query: {
|
query: songListFilter,
|
||||||
limit: 1,
|
|
||||||
startIndex: 0,
|
|
||||||
...songListFilter,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemCount =
|
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
|
||||||
itemCountCheck.data?.totalRecordCount === null
|
|
||||||
? undefined
|
|
||||||
: itemCountCheck.data?.totalRecordCount;
|
|
||||||
|
|
||||||
const handlePlay = useCallback(
|
const handlePlay = useCallback(
|
||||||
async (args: { initialSongId?: string; playType: Play }) => {
|
async (args: { initialSongId?: string; playType: Play }) => {
|
||||||
if (!itemCount || itemCount === 0) return;
|
if (!itemCount || itemCount === 0) return;
|
||||||
const { initialSongId, playType } = args;
|
const { initialSongId, playType } = args;
|
||||||
const query: SongListQuery = { startIndex: 0, ...songListFilter };
|
const query: SongListQuery = { ...songListFilter, limit: itemCount, startIndex: 0 };
|
||||||
|
|
||||||
if (albumArtistId) {
|
if (albumArtistId) {
|
||||||
handlePlayQueueAdd?.({
|
handlePlayQueueAdd?.({
|
||||||
|
|
|
@ -11,21 +11,24 @@ import { useListStoreActions, useListStoreByKey } from '/@/renderer/store';
|
||||||
|
|
||||||
export type UseDisplayRefreshProps = {
|
export type UseDisplayRefreshProps = {
|
||||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||||
|
itemCount?: number;
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
} & UseHandleListFilterChangeProps;
|
} & UseHandleListFilterChangeProps;
|
||||||
|
|
||||||
export const useDisplayRefresh = ({
|
export const useDisplayRefresh = <TFilter>({
|
||||||
isClientSideSort,
|
isClientSideSort,
|
||||||
|
itemCount,
|
||||||
gridRef,
|
gridRef,
|
||||||
itemType,
|
itemType,
|
||||||
server,
|
server,
|
||||||
tableRef,
|
tableRef,
|
||||||
}: UseDisplayRefreshProps) => {
|
}: UseDisplayRefreshProps) => {
|
||||||
const { customFilters, pageKey, handlePlay } = useListContext();
|
const { customFilters, pageKey, handlePlay } = useListContext();
|
||||||
const { display, filter } = useListStoreByKey({ key: pageKey });
|
const { display, filter } = useListStoreByKey<TFilter>({ key: pageKey });
|
||||||
|
|
||||||
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
|
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
|
||||||
isClientSideSort,
|
isClientSideSort,
|
||||||
|
itemCount,
|
||||||
itemType,
|
itemType,
|
||||||
server,
|
server,
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,12 +10,16 @@ import orderBy from 'lodash/orderBy';
|
||||||
|
|
||||||
export interface UseHandleListFilterChangeProps {
|
export interface UseHandleListFilterChangeProps {
|
||||||
isClientSideSort?: boolean;
|
isClientSideSort?: boolean;
|
||||||
|
itemCount?: number;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
server: ServerListItem | null;
|
server: ServerListItem | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BLOCK_SIZE = 500;
|
||||||
|
|
||||||
export const useListFilterRefresh = ({
|
export const useListFilterRefresh = ({
|
||||||
server,
|
server,
|
||||||
|
itemCount,
|
||||||
itemType,
|
itemType,
|
||||||
isClientSideSort,
|
isClientSideSort,
|
||||||
}: UseHandleListFilterChangeProps) => {
|
}: UseHandleListFilterChangeProps) => {
|
||||||
|
@ -78,7 +82,7 @@ export const useListFilterRefresh = ({
|
||||||
|
|
||||||
const queryKey = queryKeyFn(server?.id || '', query);
|
const queryKey = queryKeyFn(server?.id || '', query);
|
||||||
|
|
||||||
const res = await queryClient.fetchQuery({
|
const results = await queryClient.fetchQuery({
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
return queryFn({
|
return queryFn({
|
||||||
apiClientProps: {
|
apiClientProps: {
|
||||||
|
@ -91,18 +95,34 @@ export const useListFilterRefresh = ({
|
||||||
queryKey,
|
queryKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isClientSideSort && res?.items) {
|
if (isClientSideSort && results?.items) {
|
||||||
const sortedResults = orderBy(
|
const sortedResults = orderBy(
|
||||||
res.items,
|
results.items,
|
||||||
[(item) => String(item[filter.sortBy]).toLowerCase()],
|
[(item) => String(item[filter.sortBy]).toLowerCase()],
|
||||||
filter.sortOrder === 'DESC' ? ['desc'] : ['asc'],
|
filter.sortOrder === 'DESC' ? ['desc'] : ['asc'],
|
||||||
);
|
);
|
||||||
|
|
||||||
params.successCallback(sortedResults || [], res?.totalRecordCount || 0);
|
params.successCallback(
|
||||||
|
sortedResults || [],
|
||||||
|
results?.totalRecordCount || itemCount,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
params.successCallback(res?.items || [], res?.totalRecordCount || 0);
|
if (results?.totalRecordCount === null) {
|
||||||
|
const hasMoreRows = results?.items?.length === BLOCK_SIZE;
|
||||||
|
const lastRowIndex = hasMoreRows
|
||||||
|
? undefined
|
||||||
|
: (filter.offset || 0) + results.items.length;
|
||||||
|
|
||||||
|
params.successCallback(
|
||||||
|
results?.items || [],
|
||||||
|
hasMoreRows ? undefined : lastRowIndex,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
params.successCallback(results?.items || [], results?.totalRecordCount || 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
rowCount: undefined,
|
rowCount: undefined,
|
||||||
|
@ -112,7 +132,7 @@ export const useListFilterRefresh = ({
|
||||||
tableRef.current?.api.purgeInfiniteCache();
|
tableRef.current?.api.purgeInfiniteCache();
|
||||||
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||||
},
|
},
|
||||||
[isClientSideSort, queryClient, queryFn, queryKeyFn, server],
|
[isClientSideSort, itemCount, queryClient, queryFn, queryKeyFn, server],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRefreshGrid = useCallback(
|
const handleRefreshGrid = useCallback(
|
||||||
|
|
|
@ -17,10 +17,6 @@ const AlbumListRoute = lazy(() => import('/@/renderer/features/albums/routes/alb
|
||||||
|
|
||||||
const SongListRoute = lazy(() => import('/@/renderer/features/songs/routes/song-list-route'));
|
const SongListRoute = lazy(() => import('/@/renderer/features/songs/routes/song-list-route'));
|
||||||
|
|
||||||
const PlaylistDetailRoute = lazy(
|
|
||||||
() => import('/@/renderer/features/playlists/routes/playlist-detail-route'),
|
|
||||||
);
|
|
||||||
|
|
||||||
const PlaylistDetailSongListRoute = lazy(
|
const PlaylistDetailSongListRoute = lazy(
|
||||||
() => import('/@/renderer/features/playlists/routes/playlist-detail-song-list-route'),
|
() => import('/@/renderer/features/playlists/routes/playlist-detail-song-list-route'),
|
||||||
);
|
);
|
||||||
|
@ -163,11 +159,6 @@ export const AppRouter = () => {
|
||||||
errorElement={<RouteErrorBoundary />}
|
errorElement={<RouteErrorBoundary />}
|
||||||
path={AppRoute.PLAYLISTS}
|
path={AppRoute.PLAYLISTS}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
element={<PlaylistDetailRoute />}
|
|
||||||
errorElement={<RouteErrorBoundary />}
|
|
||||||
path={AppRoute.PLAYLISTS_DETAIL}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
element={<PlaylistDetailSongListRoute />}
|
element={<PlaylistDetailSongListRoute />}
|
||||||
errorElement={<RouteErrorBoundary />}
|
errorElement={<RouteErrorBoundary />}
|
||||||
|
|
|
@ -20,7 +20,6 @@ export enum AppRoute {
|
||||||
NOW_PLAYING = '/now-playing',
|
NOW_PLAYING = '/now-playing',
|
||||||
PLAYING = '/playing',
|
PLAYING = '/playing',
|
||||||
PLAYLISTS = '/playlists',
|
PLAYLISTS = '/playlists',
|
||||||
PLAYLISTS_DETAIL = '/playlists/:playlistId',
|
|
||||||
PLAYLISTS_DETAIL_SONGS = '/playlists/:playlistId/songs',
|
PLAYLISTS_DETAIL_SONGS = '/playlists/:playlistId/songs',
|
||||||
SEARCH = '/search/:itemType',
|
SEARCH = '/search/:itemType',
|
||||||
SERVERS = '/servers',
|
SERVERS = '/servers',
|
||||||
|
|
|
@ -627,7 +627,10 @@ export const useListStore = create<ListSlice>()(
|
||||||
|
|
||||||
export const useListStoreActions = () => useListStore((state) => state._actions);
|
export const useListStoreActions = () => useListStore((state) => state._actions);
|
||||||
|
|
||||||
export const useListStoreByKey = <TFilter>(args: { filter?: Partial<TFilter>; key: string }) => {
|
export const useListStoreByKey = <TFilter>(args: {
|
||||||
|
filter?: Partial<TFilter>;
|
||||||
|
key: string;
|
||||||
|
}): ListItemProps<TFilter> => {
|
||||||
const key = args.key as keyof ListState['item'];
|
const key = args.key as keyof ListState['item'];
|
||||||
return useListStore(
|
return useListStore(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
|
@ -644,7 +647,7 @@ export const useListStoreByKey = <TFilter>(args: { filter?: Partial<TFilter>; ke
|
||||||
export const useListFilterByKey = <TFilter>(args: {
|
export const useListFilterByKey = <TFilter>(args: {
|
||||||
filter?: Partial<TFilter> | any;
|
filter?: Partial<TFilter> | any;
|
||||||
key: string;
|
key: string;
|
||||||
}) => {
|
}): TFilter => {
|
||||||
const key = args.key as keyof ListState['item'];
|
const key = args.key as keyof ListState['item'];
|
||||||
return useListStore(
|
return useListStore(
|
||||||
(state) => {
|
(state) => {
|
||||||
|
|
|
@ -224,7 +224,6 @@ export interface SettingsState {
|
||||||
albumBackgroundBlur: number;
|
albumBackgroundBlur: number;
|
||||||
artistItems: SortableItem<ArtistItem>[];
|
artistItems: SortableItem<ArtistItem>[];
|
||||||
buttonSize: number;
|
buttonSize: number;
|
||||||
defaultFullPlaylist: boolean;
|
|
||||||
disabledContextMenu: { [k in ContextMenuItemType]?: boolean };
|
disabledContextMenu: { [k in ContextMenuItemType]?: boolean };
|
||||||
doubleClickQueueAll: boolean;
|
doubleClickQueueAll: boolean;
|
||||||
externalLinks: boolean;
|
externalLinks: boolean;
|
||||||
|
@ -370,7 +369,6 @@ const initialState: SettingsState = {
|
||||||
albumBackgroundBlur: 6,
|
albumBackgroundBlur: 6,
|
||||||
artistItems,
|
artistItems,
|
||||||
buttonSize: 20,
|
buttonSize: 20,
|
||||||
defaultFullPlaylist: true,
|
|
||||||
disabledContextMenu: {},
|
disabledContextMenu: {},
|
||||||
doubleClickQueueAll: true,
|
doubleClickQueueAll: true,
|
||||||
externalLinks: true,
|
externalLinks: true,
|
||||||
|
|
Reference in a new issue