restructure files onto electron-vite boilerplate

This commit is contained in:
jeffvli 2025-05-18 14:03:18 -07:00
parent 91ce2cd8a1
commit 1cf587bc8f
457 changed files with 9927 additions and 11705 deletions

View file

@ -1,10 +1,11 @@
import { useAuthStore } from '/@/renderer/store';
import { toast } from '/@/renderer/components/toast/index';
import type { ServerType, ControllerEndpoint, AuthenticationResponse } from '/@/renderer/api/types';
import type { AuthenticationResponse, ControllerEndpoint, ServerType } from '/@/renderer/api/types';
import i18n from '/@/i18n/i18n';
import { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller';
import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller';
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
import { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller';
import i18n from '/@/i18n/i18n';
import { toast } from '/@/renderer/components/toast/index';
import { useAuthStore } from '/@/renderer/store';
type ApiController = {
jellyfin: ControllerEndpoint;

View file

@ -1,97 +1,56 @@
export type JFBasePaginatedResponse = {
StartIndex: number;
TotalRecordCount: number;
};
export interface JFMusicFolderListResponse extends JFBasePaginatedResponse {
Items: JFMusicFolder[];
export enum JFAlbumArtistListSort {
ALBUM = 'Album,SortName',
DURATION = 'Runtime,AlbumArtist,Album,SortName',
NAME = 'SortName,Name',
RANDOM = 'Random,SortName',
RECENTLY_ADDED = 'DateCreated,SortName',
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
}
export type JFMusicFolderList = JFMusicFolder[];
export interface JFGenreListResponse extends JFBasePaginatedResponse {
Items: JFGenre[];
export enum JFAlbumListSort {
ALBUM_ARTIST = 'AlbumArtist,SortName',
COMMUNITY_RATING = 'CommunityRating,SortName',
CRITIC_RATING = 'CriticRating,SortName',
NAME = 'SortName',
PLAY_COUNT = 'PlayCount',
RANDOM = 'Random,SortName',
RECENTLY_ADDED = 'DateCreated,SortName',
RELEASE_DATE = 'ProductionYear,PremiereDate,SortName',
}
export type JFGenreList = JFGenreListResponse;
export enum JFArtistListSort {
ALBUM = 'Album,SortName',
DURATION = 'Runtime,AlbumArtist,Album,SortName',
NAME = 'SortName,Name',
RANDOM = 'Random,SortName',
RECENTLY_ADDED = 'DateCreated,SortName',
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
}
export enum JFCollectionType {
MUSIC = 'music',
PLAYLISTS = 'playlists',
}
export enum JFExternalType {
MUSICBRAINZ = 'MusicBrainz',
THEAUDIODB = 'TheAudioDb',
}
export enum JFGenreListSort {
NAME = 'SortName',
}
export type JFAlbumArtistDetailResponse = JFAlbumArtist;
export type JFAlbumArtistDetail = JFAlbumArtistDetailResponse;
export interface JFAlbumArtistListResponse extends JFBasePaginatedResponse {
Items: JFAlbumArtist[];
export enum JFImageType {
LOGO = 'Logo',
PRIMARY = 'Primary',
}
export type JFAlbumArtistList = {
items: JFAlbumArtist[];
startIndex: number;
totalRecordCount: number;
};
export interface JFArtistListResponse extends JFBasePaginatedResponse {
Items: JFAlbumArtist[];
export enum JFItemType {
AUDIO = 'Audio',
MUSICALBUM = 'MusicAlbum',
}
export type JFArtistList = JFArtistListResponse;
export interface JFAlbumListResponse extends JFBasePaginatedResponse {
Items: JFAlbum[];
}
export type JFAlbumList = {
items: JFAlbum[];
startIndex: number;
totalRecordCount: number;
};
export type JFAlbumDetailResponse = JFAlbum;
export type JFAlbumDetail = JFAlbum & { songs?: JFSong[] };
export interface JFSongListResponse extends JFBasePaginatedResponse {
Items: JFSong[];
}
export type JFSongList = {
items: JFSong[];
startIndex: number;
totalRecordCount: number;
};
export type JFAddToPlaylistResponse = {
added: number;
};
export type JFAddToPlaylistParams = {
ids: string[];
userId: string;
};
export type JFAddToPlaylist = null;
export type JFRemoveFromPlaylistResponse = null;
export type JFRemoveFromPlaylistParams = {
entryIds: string[];
};
export type JFRemoveFromPlaylist = null;
export interface JFPlaylistListResponse extends JFBasePaginatedResponse {
Items: JFPlaylist[];
}
export type JFPlaylistList = {
items: JFPlaylist[];
startIndex: number;
totalRecordCount: number;
};
export enum JFPlaylistListSort {
ALBUM_ARTIST = 'AlbumArtist,SortName',
DURATION = 'Runtime',
@ -100,124 +59,34 @@ export enum JFPlaylistListSort {
SONG_COUNT = 'ChildCount',
}
export type JFPlaylistDetailResponse = JFPlaylist;
export enum JFSongListSort {
ALBUM = 'Album,SortName',
ALBUM_ARTIST = 'AlbumArtist,Album,SortName',
ARTIST = 'Artist,Album,SortName',
COMMUNITY_RATING = 'CommunityRating,SortName',
DURATION = 'Runtime,AlbumArtist,Album,SortName',
NAME = 'Name',
PLAY_COUNT = 'PlayCount,SortName',
RANDOM = 'Random,SortName',
RECENTLY_ADDED = 'DateCreated,SortName',
RECENTLY_PLAYED = 'DatePlayed,SortName',
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
}
export type JFPlaylistDetail = JFPlaylist & { songs?: JFSong[] };
export enum JFSortOrder {
ASC = 'Ascending',
DESC = 'Descending',
}
export type JFPlaylist = {
BackdropImageTags: string[];
ChannelId: null;
ChildCount?: number;
DateCreated: string;
GenreItems: GenreItem[];
Genres: string[];
Id: string;
ImageBlurHashes: ImageBlurHashes;
ImageTags: ImageTags;
IsFolder: boolean;
LocationType: string;
MediaType: string;
Name: string;
Overview?: string;
RunTimeTicks: number;
ServerId: string;
Type: string;
UserData: UserData;
export type JFAddToPlaylist = null;
export type JFAddToPlaylistParams = {
ids: string[];
userId: string;
};
export type JFRequestParams = {
albumArtistIds?: string;
artistIds?: string;
enableImageTypes?: string;
enableTotalRecordCount?: boolean;
enableUserData?: boolean;
excludeItemTypes?: string;
fields?: string;
imageTypeLimit?: number;
includeItemTypes?: string;
isFavorite?: boolean;
limit?: number;
parentId?: string;
recursive?: boolean;
searchTerm?: string;
sortBy?: string;
sortOrder?: 'Ascending' | 'Descending';
startIndex?: number;
userId?: string;
};
export type JFMusicFolder = {
BackdropImageTags: string[];
ChannelId: null;
CollectionType: string;
Id: string;
ImageBlurHashes: ImageBlurHashes;
ImageTags: ImageTags;
IsFolder: boolean;
LocationType: string;
Name: string;
ServerId: string;
Type: string;
UserData: UserData;
};
export type JFGenre = {
BackdropImageTags: any[];
ChannelId: null;
Id: string;
ImageBlurHashes: any;
ImageTags: ImageTags;
LocationType: string;
Name: string;
ServerId: string;
Type: string;
};
export type JFAlbumArtist = {
BackdropImageTags: string[];
ChannelId: null;
DateCreated: string;
ExternalUrls: ExternalURL[];
GenreItems: GenreItem[];
Genres: string[];
Id: string;
ImageBlurHashes: any;
ImageTags: ImageTags;
LocationType: string;
Name: string;
Overview?: string;
RunTimeTicks: number;
ServerId: string;
Type: string;
UserData: {
IsFavorite: boolean;
Key: string;
PlayCount: number;
PlaybackPositionTicks: number;
Played: boolean;
};
} & {
similarArtists: {
items: JFAlbumArtist[];
};
};
export type JFArtist = {
BackdropImageTags: string[];
ChannelId: null;
DateCreated: string;
ExternalUrls: ExternalURL[];
GenreItems: GenreItem[];
Genres: string[];
Id: string;
ImageBlurHashes: any;
ImageTags: string[];
LocationType: string;
Name: string;
Overview?: string;
RunTimeTicks: number;
ServerId: string;
Type: string;
export type JFAddToPlaylistResponse = {
added: number;
};
export type JFAlbum = {
@ -251,6 +120,244 @@ export type JFAlbum = {
songs?: JFSong[];
};
export type JFAlbumArtist = {
BackdropImageTags: string[];
ChannelId: null;
DateCreated: string;
ExternalUrls: ExternalURL[];
GenreItems: GenreItem[];
Genres: string[];
Id: string;
ImageBlurHashes: any;
ImageTags: ImageTags;
LocationType: string;
Name: string;
Overview?: string;
RunTimeTicks: number;
ServerId: string;
Type: string;
UserData: {
IsFavorite: boolean;
Key: string;
PlaybackPositionTicks: number;
PlayCount: number;
Played: boolean;
};
} & {
similarArtists: {
items: JFAlbumArtist[];
};
};
export type JFAlbumArtistDetail = JFAlbumArtistDetailResponse;
export type JFAlbumArtistDetailResponse = JFAlbumArtist;
export type JFAlbumArtistList = {
items: JFAlbumArtist[];
startIndex: number;
totalRecordCount: number;
};
export type JFAlbumArtistListParams = JFBaseParams &
JFPaginationParams & {
filters?: string;
genres?: string;
sortBy?: JFAlbumArtistListSort;
years?: string;
};
export interface JFAlbumArtistListResponse extends JFBasePaginatedResponse {
Items: JFAlbumArtist[];
}
export type JFAlbumDetail = JFAlbum & { songs?: JFSong[] };
export type JFAlbumDetailResponse = JFAlbum;
export type JFAlbumList = {
items: JFAlbum[];
startIndex: number;
totalRecordCount: number;
};
export type JFAlbumListParams = JFBaseParams &
JFPaginationParams & {
albumArtistIds?: string;
artistIds?: string;
filters?: string;
genreIds?: string;
genres?: string;
includeItemTypes: 'MusicAlbum';
isFavorite?: boolean;
searchTerm?: string;
sortBy?: JFAlbumListSort;
tags?: string;
years?: string;
};
export interface JFAlbumListResponse extends JFBasePaginatedResponse {
Items: JFAlbum[];
}
export type JFArtist = {
BackdropImageTags: string[];
ChannelId: null;
DateCreated: string;
ExternalUrls: ExternalURL[];
GenreItems: GenreItem[];
Genres: string[];
Id: string;
ImageBlurHashes: any;
ImageTags: string[];
LocationType: string;
Name: string;
Overview?: string;
RunTimeTicks: number;
ServerId: string;
Type: string;
};
export type JFArtistList = JFArtistListResponse;
export type JFArtistListParams = JFBaseParams &
JFPaginationParams & {
filters?: string;
genres?: string;
sortBy?: JFArtistListSort;
years?: string;
};
export interface JFArtistListResponse extends JFBasePaginatedResponse {
Items: JFAlbumArtist[];
}
export interface JFAuthenticate {
AccessToken: string;
ServerId: string;
SessionInfo: SessionInfo;
User: User;
}
export type JFBasePaginatedResponse = {
StartIndex: number;
TotalRecordCount: number;
};
export type JFCreatePlaylist = JFCreatePlaylistResponse;
export type JFCreatePlaylistResponse = {
Id: string;
};
export type JFGenericItem = {
Id: string;
Name: string;
};
export type JFGenre = {
BackdropImageTags: any[];
ChannelId: null;
Id: string;
ImageBlurHashes: any;
ImageTags: ImageTags;
LocationType: string;
Name: string;
ServerId: string;
Type: string;
};
export type JFGenreList = JFGenreListResponse;
export interface JFGenreListResponse extends JFBasePaginatedResponse {
Items: JFGenre[];
}
export type JFMusicFolder = {
BackdropImageTags: string[];
ChannelId: null;
CollectionType: string;
Id: string;
ImageBlurHashes: ImageBlurHashes;
ImageTags: ImageTags;
IsFolder: boolean;
LocationType: string;
Name: string;
ServerId: string;
Type: string;
UserData: UserData;
};
export type JFMusicFolderList = JFMusicFolder[];
export interface JFMusicFolderListResponse extends JFBasePaginatedResponse {
Items: JFMusicFolder[];
}
export type JFPlaylist = {
BackdropImageTags: string[];
ChannelId: null;
ChildCount?: number;
DateCreated: string;
GenreItems: GenreItem[];
Genres: string[];
Id: string;
ImageBlurHashes: ImageBlurHashes;
ImageTags: ImageTags;
IsFolder: boolean;
LocationType: string;
MediaType: string;
Name: string;
Overview?: string;
RunTimeTicks: number;
ServerId: string;
Type: string;
UserData: UserData;
};
export type JFPlaylistDetail = JFPlaylist & { songs?: JFSong[] };
export type JFPlaylistDetailResponse = JFPlaylist;
export type JFPlaylistList = {
items: JFPlaylist[];
startIndex: number;
totalRecordCount: number;
};
export interface JFPlaylistListResponse extends JFBasePaginatedResponse {
Items: JFPlaylist[];
}
export type JFRemoveFromPlaylist = null;
export type JFRemoveFromPlaylistParams = {
entryIds: string[];
};
export type JFRemoveFromPlaylistResponse = null;
export type JFRequestParams = {
albumArtistIds?: string;
artistIds?: string;
enableImageTypes?: string;
enableTotalRecordCount?: boolean;
enableUserData?: boolean;
excludeItemTypes?: string;
fields?: string;
imageTypeLimit?: number;
includeItemTypes?: string;
isFavorite?: boolean;
limit?: number;
parentId?: string;
recursive?: boolean;
searchTerm?: string;
sortBy?: string;
sortOrder?: 'Ascending' | 'Descending';
startIndex?: number;
userId?: string;
};
export type JFSong = {
Album: string;
AlbumArtist: string;
@ -285,23 +392,56 @@ export type JFSong = {
UserData?: UserData;
};
type ImageBlurHashes = {
Backdrop?: any;
Logo?: any;
Primary?: any;
export type JFSongList = {
items: JFSong[];
startIndex: number;
totalRecordCount: number;
};
type ImageTags = {
Logo?: string;
Primary?: string;
export type JFSongListParams = JFBaseParams &
JFPaginationParams & {
albumArtistIds?: string;
albumIds?: string;
artistIds?: string;
contributingArtistIds?: string;
filters?: string;
genreIds?: string;
genres?: string;
ids?: string;
includeItemTypes: 'Audio';
searchTerm?: string;
sortBy?: JFSongListSort;
years?: string;
};
export interface JFSongListResponse extends JFBasePaginatedResponse {
Items: JFSong[];
}
type Capabilities = {
PlayableMediaTypes: any[];
SupportedCommands: any[];
SupportsContentUploading: boolean;
SupportsMediaControl: boolean;
SupportsPersistentIdentifier: boolean;
SupportsSync: boolean;
};
type UserData = {
IsFavorite: boolean;
Key: string;
PlayCount: number;
PlaybackPositionTicks: number;
Played: boolean;
type Configuration = {
DisplayCollectionsView: boolean;
DisplayMissingEpisodes: boolean;
EnableLocalPassword: boolean;
EnableNextEpisodeAutoPlay: boolean;
GroupedFolders: any[];
HidePlayedInLatest: boolean;
LatestItemsExcludes: any[];
MyMediaExcludes: any[];
OrderedViews: any[];
PlayDefaultAudioTrack: boolean;
RememberAudioSelections: boolean;
RememberSubtitleSelections: boolean;
SubtitleLanguagePreference: string;
SubtitleMode: string;
};
type ExternalURL = {
@ -314,9 +454,32 @@ type GenreItem = {
Name: string;
};
export type JFGenericItem = {
Id: string;
Name: string;
type ImageBlurHashes = {
Backdrop?: any;
Logo?: any;
Primary?: any;
};
type ImageTags = {
Logo?: string;
Primary?: string;
};
type JFBaseParams = {
enableImageTypes?: JFImageType[];
fields?: string;
imageTypeLimit?: number;
parentId?: string;
recursive?: boolean;
searchTerm?: string;
userId?: string;
};
type JFPaginationParams = {
limit?: number;
nameStartsWith?: string;
sortOrder?: JFSortOrder;
startIndex?: number;
};
type MediaSources = {
@ -380,32 +543,53 @@ type MediaStream = {
Width?: number;
};
export enum JFExternalType {
MUSICBRAINZ = 'MusicBrainz',
THEAUDIODB = 'TheAudioDb',
}
type PlayState = {
CanSeek: boolean;
IsMuted: boolean;
IsPaused: boolean;
RepeatMode: string;
};
export enum JFImageType {
LOGO = 'Logo',
PRIMARY = 'Primary',
}
export enum JFItemType {
AUDIO = 'Audio',
MUSICALBUM = 'MusicAlbum',
}
export enum JFCollectionType {
MUSIC = 'music',
PLAYLISTS = 'playlists',
}
export interface JFAuthenticate {
AccessToken: string;
ServerId: string;
SessionInfo: SessionInfo;
User: User;
}
type Policy = {
AccessSchedules: any[];
AuthenticationProviderId: string;
BlockedChannels: any[];
BlockedMediaFolders: any[];
BlockedTags: any[];
BlockUnratedItems: any[];
EnableAllChannels: boolean;
EnableAllDevices: boolean;
EnableAllFolders: boolean;
EnableAudioPlaybackTranscoding: boolean;
EnableContentDeletion: boolean;
EnableContentDeletionFromFolders: any[];
EnableContentDownloading: boolean;
EnabledChannels: any[];
EnabledDevices: any[];
EnabledFolders: any[];
EnableLiveTvAccess: boolean;
EnableLiveTvManagement: boolean;
EnableMediaConversion: boolean;
EnableMediaPlayback: boolean;
EnablePlaybackRemuxing: boolean;
EnablePublicSharing: boolean;
EnableRemoteAccess: boolean;
EnableRemoteControlOfOtherUsers: boolean;
EnableSharedDeviceControl: boolean;
EnableSyncTranscoding: boolean;
EnableUserPreferenceAccess: boolean;
EnableVideoPlaybackTranscoding: boolean;
ForceRemoteSourceTranscoding: boolean;
InvalidLoginAttemptCount: number;
IsAdministrator: boolean;
IsDisabled: boolean;
IsHidden: boolean;
LoginAttemptsBeforeLockout: number;
MaxActiveSessions: number;
PasswordResetProviderId: string;
RemoteClientBitrateLimit: number;
SyncPlayAccess: string;
};
type SessionInfo = {
AdditionalUsers: any[];
@ -421,8 +605,8 @@ type SessionInfo = {
LastPlaybackCheckIn: string;
NowPlayingQueue: any[];
NowPlayingQueueFullItems: any[];
PlayState: PlayState;
PlayableMediaTypes: any[];
PlayState: PlayState;
RemoteEndPoint: string;
ServerId: string;
SupportedCommands: any[];
@ -432,22 +616,6 @@ type SessionInfo = {
UserName: string;
};
type Capabilities = {
PlayableMediaTypes: any[];
SupportedCommands: any[];
SupportsContentUploading: boolean;
SupportsMediaControl: boolean;
SupportsPersistentIdentifier: boolean;
SupportsSync: boolean;
};
type PlayState = {
CanSeek: boolean;
IsMuted: boolean;
IsPaused: boolean;
RepeatMode: string;
};
type User = {
Configuration: Configuration;
EnableAutoLogin: boolean;
@ -462,178 +630,10 @@ type User = {
ServerId: string;
};
type Configuration = {
DisplayCollectionsView: boolean;
DisplayMissingEpisodes: boolean;
EnableLocalPassword: boolean;
EnableNextEpisodeAutoPlay: boolean;
GroupedFolders: any[];
HidePlayedInLatest: boolean;
LatestItemsExcludes: any[];
MyMediaExcludes: any[];
OrderedViews: any[];
PlayDefaultAudioTrack: boolean;
RememberAudioSelections: boolean;
RememberSubtitleSelections: boolean;
SubtitleLanguagePreference: string;
SubtitleMode: string;
type UserData = {
IsFavorite: boolean;
Key: string;
PlaybackPositionTicks: number;
PlayCount: number;
Played: boolean;
};
type Policy = {
AccessSchedules: any[];
AuthenticationProviderId: string;
BlockUnratedItems: any[];
BlockedChannels: any[];
BlockedMediaFolders: any[];
BlockedTags: any[];
EnableAllChannels: boolean;
EnableAllDevices: boolean;
EnableAllFolders: boolean;
EnableAudioPlaybackTranscoding: boolean;
EnableContentDeletion: boolean;
EnableContentDeletionFromFolders: any[];
EnableContentDownloading: boolean;
EnableLiveTvAccess: boolean;
EnableLiveTvManagement: boolean;
EnableMediaConversion: boolean;
EnableMediaPlayback: boolean;
EnablePlaybackRemuxing: boolean;
EnablePublicSharing: boolean;
EnableRemoteAccess: boolean;
EnableRemoteControlOfOtherUsers: boolean;
EnableSharedDeviceControl: boolean;
EnableSyncTranscoding: boolean;
EnableUserPreferenceAccess: boolean;
EnableVideoPlaybackTranscoding: boolean;
EnabledChannels: any[];
EnabledDevices: any[];
EnabledFolders: any[];
ForceRemoteSourceTranscoding: boolean;
InvalidLoginAttemptCount: number;
IsAdministrator: boolean;
IsDisabled: boolean;
IsHidden: boolean;
LoginAttemptsBeforeLockout: number;
MaxActiveSessions: number;
PasswordResetProviderId: string;
RemoteClientBitrateLimit: number;
SyncPlayAccess: string;
};
type JFBaseParams = {
enableImageTypes?: JFImageType[];
fields?: string;
imageTypeLimit?: number;
parentId?: string;
recursive?: boolean;
searchTerm?: string;
userId?: string;
};
type JFPaginationParams = {
limit?: number;
nameStartsWith?: string;
sortOrder?: JFSortOrder;
startIndex?: number;
};
export enum JFSortOrder {
ASC = 'Ascending',
DESC = 'Descending',
}
export enum JFAlbumListSort {
ALBUM_ARTIST = 'AlbumArtist,SortName',
COMMUNITY_RATING = 'CommunityRating,SortName',
CRITIC_RATING = 'CriticRating,SortName',
NAME = 'SortName',
PLAY_COUNT = 'PlayCount',
RANDOM = 'Random,SortName',
RECENTLY_ADDED = 'DateCreated,SortName',
RELEASE_DATE = 'ProductionYear,PremiereDate,SortName',
}
export type JFAlbumListParams = {
albumArtistIds?: string;
artistIds?: string;
filters?: string;
genreIds?: string;
genres?: string;
includeItemTypes: 'MusicAlbum';
isFavorite?: boolean;
searchTerm?: string;
sortBy?: JFAlbumListSort;
tags?: string;
years?: string;
} & JFBaseParams &
JFPaginationParams;
export enum JFSongListSort {
ALBUM = 'Album,SortName',
ALBUM_ARTIST = 'AlbumArtist,Album,SortName',
ARTIST = 'Artist,Album,SortName',
COMMUNITY_RATING = 'CommunityRating,SortName',
DURATION = 'Runtime,AlbumArtist,Album,SortName',
NAME = 'Name',
PLAY_COUNT = 'PlayCount,SortName',
RANDOM = 'Random,SortName',
RECENTLY_ADDED = 'DateCreated,SortName',
RECENTLY_PLAYED = 'DatePlayed,SortName',
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
}
export type JFSongListParams = {
albumArtistIds?: string;
albumIds?: string;
artistIds?: string;
contributingArtistIds?: string;
filters?: string;
genreIds?: string;
genres?: string;
ids?: string;
includeItemTypes: 'Audio';
searchTerm?: string;
sortBy?: JFSongListSort;
years?: string;
} & JFBaseParams &
JFPaginationParams;
export enum JFAlbumArtistListSort {
ALBUM = 'Album,SortName',
DURATION = 'Runtime,AlbumArtist,Album,SortName',
NAME = 'SortName,Name',
RANDOM = 'Random,SortName',
RECENTLY_ADDED = 'DateCreated,SortName',
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
}
export type JFAlbumArtistListParams = {
filters?: string;
genres?: string;
sortBy?: JFAlbumArtistListSort;
years?: string;
} & JFBaseParams &
JFPaginationParams;
export enum JFArtistListSort {
ALBUM = 'Album,SortName',
DURATION = 'Runtime,AlbumArtist,Album,SortName',
NAME = 'SortName,Name',
RANDOM = 'Random,SortName',
RECENTLY_ADDED = 'DateCreated,SortName',
RELEASE_DATE = 'PremiereDate,AlbumArtist,Album,SortName',
}
export type JFArtistListParams = {
filters?: string;
genres?: string;
sortBy?: JFArtistListSort;
years?: string;
} & JFBaseParams &
JFPaginationParams;
export type JFCreatePlaylistResponse = {
Id: string;
};
export type JFCreatePlaylist = JFCreatePlaylistResponse;

View file

@ -1,15 +1,17 @@
import { useAuthStore } from '/@/renderer/store';
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
import { initClient, initContract } from '@ts-rest/core';
import axios, { AxiosError, AxiosResponse, isAxiosError, Method } from 'axios';
import qs from 'qs';
import { ServerListItem } from '/@/renderer/api/types';
import omitBy from 'lodash/omitBy';
import qs from 'qs';
import { z } from 'zod';
import { authenticationFailure, getClientType } from '/@/renderer/api/utils';
import i18n from '/@/i18n/i18n';
import packageJson from '../../../../package.json';
import i18n from '/@/i18n/i18n';
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
import { ServerListItem } from '/@/renderer/api/types';
import { authenticationFailure, getClientType } from '/@/renderer/api/utils';
import { useAuthStore } from '/@/renderer/store';
const c = initContract();
export const contract = c.router({
@ -356,14 +358,14 @@ export const createAuthHeader = (): string => {
};
export const jfApiClient = (args: {
server: ServerListItem | null;
server: null | ServerListItem;
signal?: AbortSignal;
url?: string;
}) => {
const { server, url, signal } = args;
const { server, signal, url } = args;
return initClient(contract, {
api: async ({ path, method, headers, body }) => {
api: async ({ body, headers, method, path }) => {
let baseUrl: string | undefined;
let token: string | undefined;
@ -395,7 +397,7 @@ export const jfApiClient = (args: {
headers: result.headers as any,
status: result.status,
};
} catch (e: Error | AxiosError | any) {
} catch (e: any | AxiosError | Error) {
if (isAxiosError(e)) {
if (e.code === 'ERR_NETWORK') {
throw new Error(

View file

@ -1,23 +1,25 @@
import chunk from 'lodash/chunk';
import { z } from 'zod';
import { jfNormalize } from './jellyfin-normalize';
import { ServerFeature } from '/@/renderer/api/features-types';
import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types';
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
import {
albumArtistListSortMap,
sortOrderMap,
albumListSortMap,
songListSortMap,
playlistListSortMap,
genreListSortMap,
Song,
Played,
ControllerEndpoint,
genreListSortMap,
LibraryItem,
Played,
playlistListSortMap,
Song,
songListSortMap,
sortOrderMap,
} from '/@/renderer/api/types';
import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api';
import { jfNormalize } from './jellyfin-normalize';
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
import { z } from 'zod';
import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types';
import { ServerFeature } from '/@/renderer/api/features-types';
import { VersionInfo, getFeatures, hasFeature } from '/@/renderer/api/utils';
import chunk from 'lodash/chunk';
import { getFeatures, hasFeature, VersionInfo } from '/@/renderer/api/utils';
const formatCommaDelimitedString = (value: string[]) => {
return value.join(',');
@ -41,7 +43,7 @@ const VERSION_INFO: VersionInfo = [
export const JellyfinController: ControllerEndpoint = {
addToPlaylist: async (args) => {
const { query, body, apiClientProps } = args;
const { apiClientProps, body, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
@ -89,7 +91,7 @@ export const JellyfinController: ControllerEndpoint = {
};
},
createFavorite: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
@ -108,7 +110,7 @@ export const JellyfinController: ControllerEndpoint = {
return null;
},
createPlaylist: async (args) => {
const { body, apiClientProps } = args;
const { apiClientProps, body } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
@ -132,7 +134,7 @@ export const JellyfinController: ControllerEndpoint = {
};
},
deleteFavorite: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
@ -151,7 +153,7 @@ export const JellyfinController: ControllerEndpoint = {
return null;
},
deletePlaylist: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await jfApiClient(apiClientProps).deletePlaylist({
params: {
@ -166,7 +168,7 @@ export const JellyfinController: ControllerEndpoint = {
return null;
},
getAlbumArtistDetail: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
@ -201,7 +203,7 @@ export const JellyfinController: ControllerEndpoint = {
);
},
getAlbumArtistList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await jfApiClient(apiClientProps).getAlbumArtistList({
query: {
@ -236,7 +238,7 @@ export const JellyfinController: ControllerEndpoint = {
query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!),
getAlbumDetail: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
@ -274,7 +276,7 @@ export const JellyfinController: ControllerEndpoint = {
);
},
getAlbumList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
@ -334,7 +336,7 @@ export const JellyfinController: ControllerEndpoint = {
query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!),
getArtistList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await jfApiClient(apiClientProps).getArtistList({
query: {
@ -404,7 +406,7 @@ export const JellyfinController: ControllerEndpoint = {
};
},
getLyrics: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
@ -453,7 +455,7 @@ export const JellyfinController: ControllerEndpoint = {
};
},
getPlaylistDetail: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
@ -477,7 +479,7 @@ export const JellyfinController: ControllerEndpoint = {
return jfNormalize.playlist(res.body, apiClientProps.server);
},
getPlaylistList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
@ -515,7 +517,7 @@ export const JellyfinController: ControllerEndpoint = {
query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!),
getPlaylistSongList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
@ -547,7 +549,7 @@ export const JellyfinController: ControllerEndpoint = {
};
},
getRandomSongList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
@ -668,7 +670,7 @@ export const JellyfinController: ControllerEndpoint = {
}, []);
},
getSongDetail: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await jfApiClient(apiClientProps).getSongDetail({
params: {
@ -684,7 +686,7 @@ export const JellyfinController: ControllerEndpoint = {
return jfNormalize.song(res.body, apiClientProps.server, '');
},
getSongList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
@ -864,7 +866,7 @@ export const JellyfinController: ControllerEndpoint = {
};
},
getTranscodingUrl: (args) => {
const { base, format, bitrate } = args.query;
const { base, bitrate, format } = args.query;
let url = base.replace('transcodingProtocol=hls', 'transcodingProtocol=http');
if (format) {
url = url.replace('audioCodec=aac', `audioCodec=${format}`);
@ -892,7 +894,7 @@ export const JellyfinController: ControllerEndpoint = {
}
},
removeFromPlaylist: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const chunks = chunk(query.songId, MAX_ITEMS_PER_PLAYLIST_ADD);
@ -914,7 +916,7 @@ export const JellyfinController: ControllerEndpoint = {
return null;
},
scrobble: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const position = query.position && Math.round(query.position);
@ -978,7 +980,7 @@ export const JellyfinController: ControllerEndpoint = {
return null;
},
search: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');
@ -1071,7 +1073,7 @@ export const JellyfinController: ControllerEndpoint = {
};
},
updatePlaylist: async (args) => {
const { query, body, apiClientProps } = args;
const { apiClientProps, body, query } = args;
if (!apiClientProps.server?.userId) {
throw new Error('No userId found');

View file

@ -1,18 +1,19 @@
import { nanoid } from 'nanoid';
import { z } from 'zod';
import { JFAlbum, JFPlaylist, JFMusicFolder, JFGenre } from '/@/renderer/api/jellyfin.types';
import { JFAlbum, JFGenre, JFMusicFolder, JFPlaylist } from '/@/renderer/api/jellyfin.types';
import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types';
import {
Song,
LibraryItem,
Album,
AlbumArtist,
Playlist,
MusicFolder,
Genre,
LibraryItem,
MusicFolder,
Playlist,
RelatedArtist,
ServerListItem,
ServerType,
RelatedArtist,
Song,
} from '/@/renderer/api/types';
const getStreamUrl = (args: {
@ -21,9 +22,9 @@ const getStreamUrl = (args: {
eTag?: string;
id: string;
mediaSourceId?: string;
server: ServerListItem | null;
server: null | ServerListItem;
}) => {
const { id, server, deviceId } = args;
const { deviceId, id, server } = args;
return (
`${server?.url}/audio` +
@ -122,9 +123,9 @@ const getPlaylistCoverArtUrl = (args: { baseUrl: string; item: JFPlaylist; size:
);
};
type AlbumOrSong = z.infer<typeof jfType._response.song> | z.infer<typeof jfType._response.album>;
type AlbumOrSong = z.infer<typeof jfType._response.album> | z.infer<typeof jfType._response.song>;
const getPeople = (item: AlbumOrSong): Record<string, RelatedArtist[]> | null => {
const getPeople = (item: AlbumOrSong): null | Record<string, RelatedArtist[]> => {
if (item.People) {
const participants: Record<string, RelatedArtist[]> = {};
@ -151,7 +152,7 @@ const getPeople = (item: AlbumOrSong): Record<string, RelatedArtist[]> | null =>
return null;
};
const getTags = (item: AlbumOrSong): Record<string, string[]> | null => {
const getTags = (item: AlbumOrSong): null | Record<string, string[]> => {
if (item.Tags) {
const tags: Record<string, string[]> = {};
for (const tag of item.Tags) {
@ -166,7 +167,7 @@ const getTags = (item: AlbumOrSong): Record<string, string[]> | null => {
const normalizeSong = (
item: z.infer<typeof jfType._response.song>,
server: ServerListItem | null,
server: null | ServerListItem,
deviceId: string,
imageSize?: number,
): Song => {
@ -252,7 +253,7 @@ const normalizeSong = (
const normalizeAlbum = (
item: z.infer<typeof jfType._response.album>,
server: ServerListItem | null,
server: null | ServerListItem,
imageSize?: number,
): Album => {
return {
@ -312,7 +313,7 @@ const normalizeAlbumArtist = (
item: z.infer<typeof jfType._response.albumArtist> & {
similarArtists?: z.infer<typeof jfType._response.albumArtistList>;
},
server: ServerListItem | null,
server: null | ServerListItem,
imageSize?: number,
): AlbumArtist => {
const similarArtists =
@ -361,7 +362,7 @@ const normalizeAlbumArtist = (
const normalizePlaylist = (
item: z.infer<typeof jfType._response.playlist>,
server: ServerListItem | null,
server: null | ServerListItem,
imageSize?: number,
): Playlist => {
const imageUrl = getPlaylistCoverArtUrl({
@ -445,7 +446,7 @@ const getGenreCoverArtUrl = (args: {
);
};
const normalizeGenre = (item: JFGenre, server: ServerListItem | null): Genre => {
const normalizeGenre = (item: JFGenre, server: null | ServerListItem): Genre => {
return {
albumCount: undefined,
id: item.Id,

View file

@ -95,8 +95,8 @@ const imageBlurHashes = z.object({
const userData = z.object({
IsFavorite: z.boolean(),
Key: z.string(),
PlayCount: z.number(),
PlaybackPositionTicks: z.number(),
PlayCount: z.number(),
Played: z.boolean(),
});
@ -187,13 +187,13 @@ const sessionInfo = z.object({
LastPlaybackCheckIn: z.string(),
NowPlayingQueue: z.array(z.any()),
NowPlayingQueueFullItems: z.array(z.any()),
PlayableMediaTypes: z.array(z.any()),
PlayState: z.object({
CanSeek: z.boolean(),
IsMuted: z.boolean(),
IsPaused: z.boolean(),
RepeatMode: z.string(),
}),
PlayableMediaTypes: z.array(z.any()),
RemoteEndPoint: z.string(),
ServerId: z.string(),
SupportedCommands: z.array(z.any()),
@ -223,10 +223,10 @@ const configuration = z.object({
const policy = z.object({
AccessSchedules: z.array(z.any()),
AuthenticationProviderId: z.string(),
BlockUnratedItems: z.array(z.any()),
BlockedChannels: z.array(z.any()),
BlockedMediaFolders: z.array(z.any()),
BlockedTags: z.array(z.any()),
BlockUnratedItems: z.array(z.any()),
EnableAllChannels: z.boolean(),
EnableAllDevices: z.boolean(),
EnableAllFolders: z.boolean(),
@ -234,6 +234,9 @@ const policy = z.object({
EnableContentDeletion: z.boolean(),
EnableContentDeletionFromFolders: z.array(z.any()),
EnableContentDownloading: z.boolean(),
EnabledChannels: z.array(z.any()),
EnabledDevices: z.array(z.any()),
EnabledFolders: z.array(z.any()),
EnableLiveTvAccess: z.boolean(),
EnableLiveTvManagement: z.boolean(),
EnableMediaConversion: z.boolean(),
@ -246,9 +249,6 @@ const policy = z.object({
EnableSyncTranscoding: z.boolean(),
EnableUserPreferenceAccess: z.boolean(),
EnableVideoPlaybackTranscoding: z.boolean(),
EnabledChannels: z.array(z.any()),
EnabledDevices: z.array(z.any()),
EnabledFolders: z.array(z.any()),
ForceRemoteSourceTranscoding: z.boolean(),
InvalidLoginAttemptCount: z.number(),
IsAdministrator: z.boolean(),
@ -414,8 +414,8 @@ const song = z.object({
ImageTags: imageTags,
IndexNumber: z.number(),
IsFolder: z.boolean(),
LUFS: z.number().optional(),
LocationType: z.string(),
LUFS: z.number().optional(),
MediaSources: z.array(mediaSources),
MediaType: z.string(),
Name: z.string(),
@ -654,8 +654,8 @@ const favorite = z.object({
Key: z.string(),
LastPlayedDate: z.string(),
Likes: z.boolean(),
PlayCount: z.number(),
PlaybackPositionTicks: z.number(),
PlayCount: z.number(),
Played: z.boolean(),
PlayedPercentage: z.number(),
Rating: z.number(),

View file

@ -1,30 +1,77 @@
import { SSArtistInfo } from '/@/renderer/api/subsonic.types';
export type NDAuthenticate = {
id: string;
isAdmin: boolean;
name: string;
subsonicSalt: string;
subsonicToken: string;
token: string;
username: string;
export enum NDAlbumArtistListSort {
ALBUM_COUNT = 'albumCount',
FAVORITED = 'starred_at',
NAME = 'name',
PLAY_COUNT = 'playCount',
RATING = 'rating',
SONG_COUNT = 'songCount',
}
export enum NDAlbumListSort {
ALBUM_ARTIST = 'album_artist',
ARTIST = 'artist',
DURATION = 'duration',
NAME = 'name',
PLAY_COUNT = 'play_count',
PLAY_DATE = 'play_date',
RANDOM = 'random',
RATING = 'rating',
RECENTLY_ADDED = 'recently_added',
SONG_COUNT = 'songCount',
STARRED = 'starred_at',
YEAR = 'max_year',
}
export enum NDGenreListSort {
NAME = 'name',
}
export enum NDPlaylistListSort {
DURATION = 'duration',
NAME = 'name',
OWNER = 'owner_name',
PUBLIC = 'public',
SONG_COUNT = 'songCount',
UPDATED_AT = 'updatedAt',
}
export enum NDSongListSort {
ALBUM = 'album',
ALBUM_ARTIST = 'order_album_artist_name',
ALBUM_SONGS = 'album',
ARTIST = 'artist',
BPM = 'bpm',
CHANNELS = 'channels',
COMMENT = 'comment',
DURATION = 'duration',
FAVORITED = 'starred_at',
GENRE = 'genre',
ID = 'id',
PLAY_COUNT = 'playCount',
PLAY_DATE = 'playDate',
RANDOM = 'random',
RATING = 'rating',
RECENTLY_ADDED = 'createdAt',
TITLE = 'title',
TRACK = 'track',
YEAR = 'year',
}
export enum NDSortOrder {
ASC = 'ASC',
DESC = 'DESC',
}
export type NDAddToPlaylist = null;
export type NDAddToPlaylistBody = {
ids: string[];
};
export type NDUser = {
createdAt: string;
email: string;
id: string;
isAdmin: boolean;
lastAccessAt: string;
lastLoginAt: string;
name: string;
updatedAt: string;
userName: string;
};
export type NDGenre = {
id: string;
name: string;
export type NDAddToPlaylistResponse = {
added: number;
};
export type NDAlbum = {
@ -61,6 +108,193 @@ export type NDAlbum = {
updatedAt: string;
} & { songs?: NDSong[] };
export type NDAlbumArtist = {
albumCount: number;
biography: string;
externalInfoUpdatedAt: string;
externalUrl: string;
fullText: string;
genres: NDGenre[];
id: string;
largeImageUrl?: string;
mbzArtistId: string;
mediumImageUrl?: string;
name: string;
orderArtistName: string;
playCount: number;
playDate: string;
rating: number;
size: number;
smallImageUrl?: string;
songCount: number;
starred: boolean;
starredAt: string;
} & {
similarArtists?: SSArtistInfo['similarArtist'];
};
export type NDAlbumArtistDetail = NDAlbumArtist;
export type NDAlbumArtistDetailResponse = NDAlbumArtist;
export type NDAlbumArtistList = {
items: NDAlbumArtist[];
startIndex: number;
totalRecordCount: number;
};
export type NDAlbumArtistListParams = NDOrder &
NDPagination & {
_sort?: NDAlbumArtistListSort;
genre_id?: string;
starred?: boolean;
};
export type NDAlbumDetail = NDAlbum & { songs?: NDSongListResponse };
export type NDAlbumDetailResponse = NDAlbum;
export type NDAlbumList = {
items: NDAlbum[];
startIndex: number;
totalRecordCount: number;
};
export type NDAlbumListParams = NDOrder &
NDPagination & {
_sort?: NDAlbumListSort;
album_id?: string;
artist_id?: string;
compilation?: boolean;
genre_id?: string;
has_rating?: boolean;
id?: string;
name?: string;
recently_played?: boolean;
starred?: boolean;
year?: number;
};
export type NDAlbumListResponse = NDAlbum[];
export type NDArtistListResponse = NDAlbumArtist[];
export type NDAuthenticate = {
id: string;
isAdmin: boolean;
name: string;
subsonicSalt: string;
subsonicToken: string;
token: string;
username: string;
};
export type NDAuthenticationResponse = NDAuthenticate;
export type NDCreatePlaylist = NDCreatePlaylistResponse;
export type NDCreatePlaylistParams = {
comment?: string;
name: string;
public?: boolean;
rules?: null | Record<string, any>;
};
export type NDCreatePlaylistResponse = {
id: string;
};
export type NDDeletePlaylist = NDDeletePlaylistResponse;
export type NDDeletePlaylistParams = {
id: string;
};
export type NDDeletePlaylistResponse = null;
export type NDGenre = {
id: string;
name: string;
};
export type NDGenreList = NDGenre[];
export type NDGenreListParams = NDOrder &
NDPagination & {
_sort?: NDGenreListSort;
id?: string;
};
export type NDGenreListResponse = NDGenre[];
export type NDOrder = {
_order?: NDSortOrder;
};
export type NDPagination = {
_end?: number;
_start?: number;
};
export type NDPlaylist = {
comment: string;
createdAt: string;
duration: number;
evaluatedAt: string;
id: string;
name: string;
ownerId: string;
ownerName: string;
path: string;
public: boolean;
rules: null | Record<string, any>;
size: number;
songCount: number;
sync: boolean;
updatedAt: string;
};
export type NDPlaylistDetail = NDPlaylist;
export type NDPlaylistDetailResponse = NDPlaylist;
export type NDPlaylistList = {
items: NDPlaylist[];
startIndex: number;
totalRecordCount: number;
};
export type NDPlaylistListParams = NDOrder &
NDPagination & {
_sort?: NDPlaylistListSort;
owner_id?: string;
};
export type NDPlaylistListResponse = NDPlaylist[];
export type NDPlaylistSong = NDSong & {
mediaFileId: string;
playlistId: string;
};
export type NDPlaylistSongList = {
items: NDPlaylistSong[];
startIndex: number;
totalRecordCount: number;
};
export type NDPlaylistSongListResponse = NDPlaylistSong[];
export type NDRemoveFromPlaylist = null;
export type NDRemoveFromPlaylistParams = {
id: string[];
};
export type NDRemoveFromPlaylistResponse = {
ids: string[];
};
export type NDSong = {
album: string;
albumArtist: string;
@ -107,275 +341,41 @@ export type NDSong = {
year: number;
};
export type NDAlbumArtist = {
albumCount: number;
biography: string;
externalInfoUpdatedAt: string;
externalUrl: string;
fullText: string;
genres: NDGenre[];
id: string;
largeImageUrl?: string;
mbzArtistId: string;
mediumImageUrl?: string;
name: string;
orderArtistName: string;
playCount: number;
playDate: string;
rating: number;
size: number;
smallImageUrl?: string;
songCount: number;
starred: boolean;
starredAt: string;
} & {
similarArtists?: SSArtistInfo['similarArtist'];
};
export type NDAuthenticationResponse = NDAuthenticate;
export type NDAlbumArtistList = {
items: NDAlbumArtist[];
startIndex: number;
totalRecordCount: number;
};
export type NDAlbumArtistDetail = NDAlbumArtist;
export type NDAlbumArtistDetailResponse = NDAlbumArtist;
export type NDGenreList = NDGenre[];
export type NDGenreListResponse = NDGenre[];
export type NDAlbumDetailResponse = NDAlbum;
export type NDAlbumDetail = NDAlbum & { songs?: NDSongListResponse };
export type NDAlbumListResponse = NDAlbum[];
export type NDAlbumList = {
items: NDAlbum[];
startIndex: number;
totalRecordCount: number;
};
export type NDSongDetail = NDSong;
export type NDSongDetailResponse = NDSong;
export type NDSongListResponse = NDSong[];
export type NDSongList = {
items: NDSong[];
startIndex: number;
totalRecordCount: number;
};
export type NDArtistListResponse = NDAlbumArtist[];
export type NDSongListParams = NDOrder &
NDPagination & {
_sort?: NDSongListSort;
album_id?: string[];
artist_id?: string[];
genre_id?: string;
starred?: boolean;
};
export type NDPagination = {
_end?: number;
_start?: number;
};
export enum NDSortOrder {
ASC = 'ASC',
DESC = 'DESC',
}
export type NDOrder = {
_order?: NDSortOrder;
};
export enum NDGenreListSort {
NAME = 'name',
}
export type NDGenreListParams = {
_sort?: NDGenreListSort;
id?: string;
} & NDPagination &
NDOrder;
export enum NDAlbumListSort {
ALBUM_ARTIST = 'album_artist',
ARTIST = 'artist',
DURATION = 'duration',
NAME = 'name',
PLAY_COUNT = 'play_count',
PLAY_DATE = 'play_date',
RANDOM = 'random',
RATING = 'rating',
RECENTLY_ADDED = 'recently_added',
SONG_COUNT = 'songCount',
STARRED = 'starred_at',
YEAR = 'max_year',
}
export type NDAlbumListParams = {
_sort?: NDAlbumListSort;
album_id?: string;
artist_id?: string;
compilation?: boolean;
genre_id?: string;
has_rating?: boolean;
id?: string;
name?: string;
recently_played?: boolean;
starred?: boolean;
year?: number;
} & NDPagination &
NDOrder;
export enum NDSongListSort {
ALBUM = 'album',
ALBUM_ARTIST = 'order_album_artist_name',
ALBUM_SONGS = 'album',
ARTIST = 'artist',
BPM = 'bpm',
CHANNELS = 'channels',
COMMENT = 'comment',
DURATION = 'duration',
FAVORITED = 'starred_at',
GENRE = 'genre',
ID = 'id',
PLAY_COUNT = 'playCount',
PLAY_DATE = 'playDate',
RANDOM = 'random',
RATING = 'rating',
RECENTLY_ADDED = 'createdAt',
TITLE = 'title',
TRACK = 'track',
YEAR = 'year',
}
export type NDSongListParams = {
_sort?: NDSongListSort;
album_id?: string[];
artist_id?: string[];
genre_id?: string;
starred?: boolean;
} & NDPagination &
NDOrder;
export enum NDAlbumArtistListSort {
ALBUM_COUNT = 'albumCount',
FAVORITED = 'starred_at',
NAME = 'name',
PLAY_COUNT = 'playCount',
RATING = 'rating',
SONG_COUNT = 'songCount',
}
export type NDAlbumArtistListParams = {
_sort?: NDAlbumArtistListSort;
genre_id?: string;
starred?: boolean;
} & NDPagination &
NDOrder;
export type NDAddToPlaylistResponse = {
added: number;
};
export type NDAddToPlaylistBody = {
ids: string[];
};
export type NDAddToPlaylist = null;
export type NDRemoveFromPlaylistResponse = {
ids: string[];
};
export type NDRemoveFromPlaylistParams = {
id: string[];
};
export type NDRemoveFromPlaylist = null;
export type NDCreatePlaylistParams = {
comment?: string;
name: string;
public?: boolean;
rules?: Record<string, any> | null;
};
export type NDCreatePlaylistResponse = {
id: string;
};
export type NDCreatePlaylist = NDCreatePlaylistResponse;
export type NDSongListResponse = NDSong[];
export type NDUpdatePlaylistParams = Partial<NDPlaylist>;
export type NDUpdatePlaylistResponse = NDPlaylist;
export type NDDeletePlaylistParams = {
id: string;
};
export type NDDeletePlaylistResponse = null;
export type NDDeletePlaylist = NDDeletePlaylistResponse;
export type NDPlaylist = {
comment: string;
export type NDUser = {
createdAt: string;
duration: number;
evaluatedAt: string;
email: string;
id: string;
isAdmin: boolean;
lastAccessAt: string;
lastLoginAt: string;
name: string;
ownerId: string;
ownerName: string;
path: string;
public: boolean;
rules: Record<string, any> | null;
size: number;
songCount: number;
sync: boolean;
updatedAt: string;
};
export type NDPlaylistDetail = NDPlaylist;
export type NDPlaylistDetailResponse = NDPlaylist;
export type NDPlaylistList = {
items: NDPlaylist[];
startIndex: number;
totalRecordCount: number;
};
export type NDPlaylistListResponse = NDPlaylist[];
export enum NDPlaylistListSort {
DURATION = 'duration',
NAME = 'name',
OWNER = 'owner_name',
PUBLIC = 'public',
SONG_COUNT = 'songCount',
UPDATED_AT = 'updatedAt',
}
export type NDPlaylistListParams = {
_sort?: NDPlaylistListSort;
owner_id?: string;
} & NDPagination &
NDOrder;
export type NDPlaylistSong = NDSong & {
mediaFileId: string;
playlistId: string;
};
export type NDPlaylistSongListResponse = NDPlaylistSong[];
export type NDPlaylistSongList = {
items: NDPlaylistSong[];
startIndex: number;
totalRecordCount: number;
userName: string;
};
export const NDSongQueryFields = [
@ -515,12 +515,9 @@ export const NDSongQueryNumberOperators = [
{ label: 'is in the range', value: 'inTheRange' },
];
export type NDUserListParams = {
_sort?: NDUserListSort;
} & NDPagination &
NDOrder;
export type NDUserListResponse = NDUser[];
export enum NDUserListSort {
NAME = 'name',
}
export type NDUserList = {
items: NDUser[];
@ -528,6 +525,9 @@ export type NDUserList = {
totalRecordCount: number;
};
export enum NDUserListSort {
NAME = 'name',
}
export type NDUserListParams = NDOrder &
NDPagination & {
_sort?: NDUserListSort;
};
export type NDUserListResponse = NDUser[];

View file

@ -1,17 +1,19 @@
import { initClient, initContract } from '@ts-rest/core';
import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from 'axios';
import axios, { AxiosError, AxiosResponse, isAxiosError, Method } from 'axios';
import isElectron from 'is-electron';
import debounce from 'lodash/debounce';
import omitBy from 'lodash/omitBy';
import qs from 'qs';
import { ndType } from './navidrome-types';
import { authenticationFailure, resultWithHeaders } from '/@/renderer/api/utils';
import { useAuthStore } from '/@/renderer/store';
import { ServerListItem } from '/@/renderer/api/types';
import { toast } from '/@/renderer/components/toast';
import i18n from '/@/i18n/i18n';
const localSettings = isElectron() ? window.electron.localSettings : null;
import { ndType } from './navidrome-types';
import i18n from '/@/i18n/i18n';
import { ServerListItem } from '/@/renderer/api/types';
import { authenticationFailure, resultWithHeaders } from '/@/renderer/api/utils';
import { toast } from '/@/renderer/components/toast';
import { useAuthStore } from '/@/renderer/store';
const localSettings = isElectron() ? window.api.localSettings : null;
const c = initContract();
@ -275,7 +277,7 @@ axiosClient.interceptors.response.use(
// eslint-disable-next-line promise/no-promise-in-callback
return localSettings
.passwordGet(currentServer.id)
.then(async (password: string | null) => {
.then(async (password: null | string) => {
authSuccess = false;
if (password === null) {
@ -367,14 +369,14 @@ axiosClient.interceptors.response.use(
);
export const ndApiClient = (args: {
server: ServerListItem | null;
server: null | ServerListItem;
signal?: AbortSignal;
url?: string;
}) => {
const { server, url, signal } = args;
const { server, signal, url } = args;
return initClient(contract, {
api: async ({ path, method, headers, body }) => {
api: async ({ body, headers, method, path }) => {
let baseUrl: string | undefined;
let token: string | undefined;
@ -406,7 +408,7 @@ export const ndApiClient = (args: {
headers: result.headers as any,
status: result.status,
};
} catch (e: Error | AxiosError | any) {
} catch (e: any | AxiosError | Error) {
if (isAxiosError(e)) {
if (e.code === 'ERR_NETWORK') {
throw new Error(

View file

@ -1,28 +1,29 @@
import {
albumArtistListSortMap,
albumListSortMap,
AuthenticationResponse,
ControllerEndpoint,
genreListSortMap,
playlistListSortMap,
PlaylistSongListArgs,
PlaylistSongListResponse,
ServerListItem,
Song,
songListSortMap,
sortOrderMap,
userListSortMap,
} from '../types';
import { ServerFeature, ServerFeatures } from '/@/renderer/api/features-types';
import { NDSongListSort } from '/@/renderer/api/navidrome.types';
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
import { ndNormalize } from '/@/renderer/api/navidrome/navidrome-normalize';
import { ndType } from '/@/renderer/api/navidrome/navidrome-types';
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
import {
albumArtistListSortMap,
sortOrderMap,
AuthenticationResponse,
userListSortMap,
albumListSortMap,
songListSortMap,
playlistListSortMap,
PlaylistSongListArgs,
PlaylistSongListResponse,
genreListSortMap,
Song,
ControllerEndpoint,
ServerListItem,
} from '../types';
import { VersionInfo, getFeatures, hasFeature } from '/@/renderer/api/utils';
import { ServerFeature, ServerFeatures } from '/@/renderer/api/features-types';
import { SubsonicExtensions } from '/@/renderer/api/subsonic/subsonic-types';
import { NDSongListSort } from '/@/renderer/api/navidrome.types';
import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize';
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize';
import { SubsonicExtensions } from '/@/renderer/api/subsonic/subsonic-types';
import { getFeatures, hasFeature, VersionInfo } from '/@/renderer/api/utils';
const VERSION_INFO: VersionInfo = [
['0.55.0', { [ServerFeature.BFR]: [1] }],
@ -48,7 +49,7 @@ const NAVIDROME_ROLES: Array<string | { label: string; value: string }> = [
const EXCLUDED_TAGS = new Set<string>(['disctotal', 'genre', 'tracktotal']);
const excludeMissing = (server: ServerListItem | null) => {
const excludeMissing = (server: null | ServerListItem) => {
if (hasFeature(server, ServerFeature.BFR)) {
return { missing: false };
}
@ -58,7 +59,7 @@ const excludeMissing = (server: ServerListItem | null) => {
export const NavidromeController: ControllerEndpoint = {
addToPlaylist: async (args) => {
const { body, query, apiClientProps } = args;
const { apiClientProps, body, query } = args;
const res = await ndApiClient(apiClientProps).addToPlaylist({
body: {
@ -98,7 +99,7 @@ export const NavidromeController: ControllerEndpoint = {
},
createFavorite: SubsonicController.createFavorite,
createPlaylist: async (args) => {
const { body, apiClientProps } = args;
const { apiClientProps, body } = args;
const res = await ndApiClient(apiClientProps).createPlaylist({
body: {
@ -120,7 +121,7 @@ export const NavidromeController: ControllerEndpoint = {
},
deleteFavorite: SubsonicController.deleteFavorite,
deletePlaylist: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).deletePlaylist({
params: {
@ -135,7 +136,7 @@ export const NavidromeController: ControllerEndpoint = {
return null;
},
getAlbumArtistDetail: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).getAlbumArtistDetail({
params: {
@ -176,7 +177,7 @@ export const NavidromeController: ControllerEndpoint = {
);
},
getAlbumArtistList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
query: {
@ -217,7 +218,7 @@ export const NavidromeController: ControllerEndpoint = {
query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!),
getAlbumDetail: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const albumRes = await ndApiClient(apiClientProps).getAlbumDetail({
params: {
@ -245,7 +246,7 @@ export const NavidromeController: ControllerEndpoint = {
);
},
getAlbumInfo: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const albumInfo = await ssApiClient(apiClientProps).getAlbumInfo2({
query: {
@ -265,7 +266,7 @@ export const NavidromeController: ControllerEndpoint = {
};
},
getAlbumList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).getAlbumList({
query: {
@ -299,7 +300,7 @@ export const NavidromeController: ControllerEndpoint = {
query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!),
getArtistList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).getAlbumArtistList({
query: {
@ -341,7 +342,7 @@ export const NavidromeController: ControllerEndpoint = {
}).then((result) => result!.totalRecordCount!),
getDownloadUrl: SubsonicController.getDownloadUrl,
getGenreList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).getGenreList({
query: {
@ -366,7 +367,7 @@ export const NavidromeController: ControllerEndpoint = {
getLyrics: SubsonicController.getLyrics,
getMusicFolderList: SubsonicController.getMusicFolderList,
getPlaylistDetail: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).getPlaylistDetail({
params: {
@ -381,7 +382,7 @@ export const NavidromeController: ControllerEndpoint = {
return ndNormalize.playlist(res.body.data, apiClientProps.server);
},
getPlaylistList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const customQuery = query._custom?.navidrome;
// Smart playlists only became available in 0.48.0. Do not filter for previous versions
@ -420,7 +421,7 @@ export const NavidromeController: ControllerEndpoint = {
query: { ...query, limit: 1, startIndex: 0 },
}).then((result) => result!.totalRecordCount!),
getPlaylistSongList: async (args: PlaylistSongListArgs): Promise<PlaylistSongListResponse> => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).getPlaylistSongList({
params: {
@ -548,7 +549,7 @@ export const NavidromeController: ControllerEndpoint = {
}, []);
},
getSongDetail: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).getSongDetail({
params: {
@ -563,7 +564,7 @@ export const NavidromeController: ControllerEndpoint = {
return ndNormalize.song(res.body.data, apiClientProps.server);
},
getSongList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).getSongList({
query: {
@ -642,7 +643,7 @@ export const NavidromeController: ControllerEndpoint = {
getTopSongs: SubsonicController.getTopSongs,
getTranscodingUrl: SubsonicController.getTranscodingUrl,
getUserList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).getUserList({
query: {
@ -682,7 +683,7 @@ export const NavidromeController: ControllerEndpoint = {
}
},
removeFromPlaylist: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ndApiClient(apiClientProps).removeFromPlaylist({
params: {
@ -703,7 +704,7 @@ export const NavidromeController: ControllerEndpoint = {
search: SubsonicController.search,
setRating: SubsonicController.setRating,
shareItem: async (args) => {
const { body, apiClientProps } = args;
const { apiClientProps, body } = args;
const res = await ndApiClient(apiClientProps).shareItem({
body: {
@ -724,7 +725,7 @@ export const NavidromeController: ControllerEndpoint = {
};
},
updatePlaylist: async (args) => {
const { query, body, apiClientProps } = args;
const { apiClientProps, body, query } = args;
const res = await ndApiClient(apiClientProps).updatePlaylist({
body: {

View file

@ -1,22 +1,24 @@
import { nanoid } from 'nanoid';
import z from 'zod';
import { ndType } from './navidrome-types';
import { NDGenre } from '/@/renderer/api/navidrome.types';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
import {
Song,
LibraryItem,
Album,
Playlist,
User,
AlbumArtist,
Genre,
LibraryItem,
Playlist,
RelatedArtist,
ServerListItem,
ServerType,
RelatedArtist,
Song,
User,
} from '/@/renderer/api/types';
import z from 'zod';
import { ndType } from './navidrome-types';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
import { NDGenre } from '/@/renderer/api/navidrome.types';
const getImageUrl = (args: { url: string | null }) => {
const getImageUrl = (args: { url: null | string }) => {
const { url } = args;
if (url === '/app/artist-placeholder.webp') {
return null;
@ -51,19 +53,19 @@ interface WithDate {
playDate?: string;
}
const normalizePlayDate = (item: WithDate): string | null => {
const normalizePlayDate = (item: WithDate): null | string => {
return !item.playDate || item.playDate.includes('0001-') ? null : item.playDate;
};
const getArtists = (
item:
| z.infer<typeof ndType._response.song>
| z.infer<typeof ndType._response.album>
| z.infer<typeof ndType._response.playlistSong>
| z.infer<typeof ndType._response.album>,
| z.infer<typeof ndType._response.song>,
) => {
let albumArtists: RelatedArtist[] | undefined;
let artists: RelatedArtist[] | undefined;
let participants: Record<string, RelatedArtist[]> | null = null;
let participants: null | Record<string, RelatedArtist[]> = null;
if (item.participants) {
participants = {};
@ -120,8 +122,8 @@ const getArtists = (
};
const normalizeSong = (
item: z.infer<typeof ndType._response.song> | z.infer<typeof ndType._response.playlistSong>,
server: ServerListItem | null,
item: z.infer<typeof ndType._response.playlistSong> | z.infer<typeof ndType._response.song>,
server: null | ServerListItem,
imageSize?: number,
): Song => {
let id;
@ -204,7 +206,7 @@ const normalizeAlbum = (
item: z.infer<typeof ndType._response.album> & {
songs?: z.infer<typeof ndType._response.songList>;
},
server: ServerListItem | null,
server: null | ServerListItem,
imageSize?: number,
): Album => {
const imageUrl = getCoverArtUrl({
@ -268,7 +270,7 @@ const normalizeAlbumArtist = (
item: z.infer<typeof ndType._response.albumArtist> & {
similarArtists?: z.infer<typeof ssType._response.artistInfo>['artistInfo']['similarArtist'];
},
server: ServerListItem | null,
server: null | ServerListItem,
): AlbumArtist => {
let imageUrl = getImageUrl({ url: item?.largeImageUrl || null });
@ -332,7 +334,7 @@ const normalizeAlbumArtist = (
const normalizePlaylist = (
item: z.infer<typeof ndType._response.playlist>,
server: ServerListItem | null,
server: null | ServerListItem,
imageSize?: number,
): Playlist => {
const imageUrl = getCoverArtUrl({

View file

@ -1,4 +1,5 @@
import { z } from 'zod';
import {
NDAlbumArtistListSort,
NDAlbumListSort,

View file

@ -1,28 +1,30 @@
import { QueryFunctionContext } from '@tanstack/react-query';
import { LyricSource } from './types';
import type {
AlbumListQuery,
SongListQuery,
AlbumDetailQuery,
AlbumArtistListQuery,
ArtistListQuery,
PlaylistListQuery,
PlaylistDetailQuery,
PlaylistSongListQuery,
UserListQuery,
AlbumArtistDetailQuery,
TopSongListQuery,
SearchQuery,
SongDetailQuery,
RandomSongListQuery,
LyricsQuery,
LyricSearchQuery,
AlbumArtistListQuery,
AlbumDetailQuery,
AlbumListQuery,
ArtistListQuery,
GenreListQuery,
LyricSearchQuery,
LyricsQuery,
PlaylistDetailQuery,
PlaylistListQuery,
PlaylistSongListQuery,
RandomSongListQuery,
SearchQuery,
SimilarSongsQuery,
SongDetailQuery,
SongListQuery,
TopSongListQuery,
UserListQuery,
} from './types';
import { LyricSource } from './types';
export const splitPaginatedQuery = (key: any) => {
const { startIndex, limit, ...filter } = key || {};
const { limit, startIndex, ...filter } = key || {};
if (startIndex !== undefined || limit !== undefined) {
return {
@ -51,7 +53,7 @@ export const queryKeys: Record<
> = {
albumArtists: {
count: (serverId: string, query?: AlbumArtistListQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'albumArtists', 'count', filter, pagination] as const;
@ -68,7 +70,7 @@ export const queryKeys: Record<
return [serverId, 'albumArtists', 'detail'] as const;
},
list: (serverId: string, query?: AlbumArtistListQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'albumArtists', 'list', filter, pagination] as const;
}
@ -87,7 +89,7 @@ export const queryKeys: Record<
},
albums: {
count: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
const { pagination, filter } = splitPaginatedQuery(query);
const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination && artistId) {
return [serverId, 'albums', 'count', artistId, filter, pagination] as const;
@ -110,7 +112,7 @@ export const queryKeys: Record<
detail: (serverId: string, query?: AlbumDetailQuery) =>
[serverId, 'albums', 'detail', query] as const,
list: (serverId: string, query?: AlbumListQuery, artistId?: string) => {
const { pagination, filter } = splitPaginatedQuery(query);
const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination && artistId) {
return [serverId, 'albums', 'list', artistId, filter, pagination] as const;
@ -144,7 +146,7 @@ export const queryKeys: Record<
},
artists: {
list: (serverId: string, query?: ArtistListQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'artists', 'list', filter, pagination] as const;
}
@ -159,7 +161,7 @@ export const queryKeys: Record<
},
genres: {
list: (serverId: string, query?: GenreListQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'genres', 'list', filter, pagination] as const;
}
@ -177,7 +179,7 @@ export const queryKeys: Record<
},
playlists: {
detail: (serverId: string, id?: string, query?: PlaylistDetailQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'playlists', id, 'detail', filter, pagination] as const;
}
@ -190,7 +192,7 @@ export const queryKeys: Record<
return [serverId, 'playlists', 'detail'] as const;
},
detailSongList: (serverId: string, id: string, query?: PlaylistSongListQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
const { filter, pagination } = splitPaginatedQuery(query);
if (query && id && pagination) {
return [serverId, 'playlists', id, 'detailSongList', filter, pagination] as const;
@ -205,7 +207,7 @@ export const queryKeys: Record<
return [serverId, 'playlists', 'detailSongList'] as const;
},
list: (serverId: string, query?: PlaylistListQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'playlists', 'list', filter, pagination] as const;
}
@ -218,7 +220,7 @@ export const queryKeys: Record<
},
root: (serverId: string) => [serverId, 'playlists'] as const,
songList: (serverId: string, id?: string, query?: PlaylistSongListQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
const { filter, pagination } = splitPaginatedQuery(query);
if (query && id && pagination) {
return [serverId, 'playlists', id, 'songList', filter, pagination] as const;
}
@ -246,7 +248,7 @@ export const queryKeys: Record<
},
songs: {
count: (serverId: string, query?: SongListQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'songs', 'count', filter, pagination] as const;
}
@ -262,7 +264,7 @@ export const queryKeys: Record<
return [serverId, 'songs', 'detail'] as const;
},
list: (serverId: string, query?: SongListQuery) => {
const { pagination, filter } = splitPaginatedQuery(query);
const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'songs', 'list', filter, pagination] as const;
}

View file

@ -1,32 +1,13 @@
export type SSBaseResponse = {
serverVersion?: 'string';
status: 'string';
type?: 'string';
version: 'string';
export type SSAlbum = SSAlbumListEntry & {
song: SSSong[];
};
export type SSMusicFolderList = SSMusicFolder[];
export type SSMusicFolderListResponse = {
musicFolders: {
musicFolder: SSMusicFolder[];
};
};
export type SSGenreList = SSGenre[];
export type SSGenreListResponse = {
genres: {
genre: SSGenre[];
};
};
export type SSAlbumArtistDetail = SSAlbumArtistListEntry & { album: SSAlbumListEntry[] };
export type SSAlbumArtistDetailParams = {
id: string;
};
export type SSAlbumArtistDetail = SSAlbumArtistListEntry & { album: SSAlbumListEntry[] };
export type SSAlbumArtistDetailResponse = {
artist: SSAlbumArtistListEntry & {
album: SSAlbumListEntry[];
@ -36,7 +17,19 @@ export type SSAlbumArtistDetailResponse = {
export type SSAlbumArtistList = {
items: SSAlbumArtistListEntry[];
startIndex: number;
totalRecordCount: number | null;
totalRecordCount: null | number;
};
export type SSAlbumArtistListEntry = {
albumCount: string;
artistImageUrl?: string;
coverArt?: string;
id: string;
name: string;
};
export type SSAlbumArtistListParams = {
musicFolderId?: string;
};
export type SSAlbumArtistListResponse = {
@ -47,72 +40,16 @@ export type SSAlbumArtistListResponse = {
};
};
export type SSAlbumList = {
items: SSAlbumListEntry[];
startIndex: number;
totalRecordCount: number | null;
};
export type SSAlbumListResponse = {
albumList2: {
album: SSAlbumListEntry[];
};
};
export type SSAlbumDetail = Omit<SSAlbum, 'song'> & { songs: SSSong[] };
export type SSAlbumDetailResponse = {
album: SSAlbum;
};
export type SSArtistInfoParams = {
count?: number;
id: string;
includeNotPresent?: boolean;
};
export type SSArtistInfoResponse = {
artistInfo2: SSArtistInfo;
};
export type SSArtistInfo = {
biography: string;
largeImageUrl?: string;
lastFmUrl?: string;
mediumImageUrl?: string;
musicBrainzId?: string;
similarArtist?: {
albumCount: string;
artistImageUrl?: string;
coverArt?: string;
id: string;
name: string;
}[];
smallImageUrl?: string;
};
export type SSMusicFolder = {
id: number;
name: string;
};
export type SSGenre = {
albumCount?: number;
songCount?: number;
value: string;
};
export type SSArtistIndex = {
artist: SSAlbumArtistListEntry[];
name: string;
};
export type SSAlbumArtistListEntry = {
albumCount: string;
artistImageUrl?: string;
coverArt?: string;
id: string;
name: string;
export type SSAlbumList = {
items: SSAlbumListEntry[];
startIndex: number;
totalRecordCount: null | number;
};
export type SSAlbumListEntry = {
@ -135,9 +72,111 @@ export type SSAlbumListEntry = {
year: number;
};
export type SSAlbum = {
song: SSSong[];
} & SSAlbumListEntry;
export type SSAlbumListParams = {
fromYear?: number;
genre?: string;
musicFolderId?: string;
offset?: number;
size?: number;
toYear?: number;
type: string;
};
export type SSAlbumListResponse = {
albumList2: {
album: SSAlbumListEntry[];
};
};
export type SSArtistIndex = {
artist: SSAlbumArtistListEntry[];
name: string;
};
export type SSArtistInfo = {
biography: string;
largeImageUrl?: string;
lastFmUrl?: string;
mediumImageUrl?: string;
musicBrainzId?: string;
similarArtist?: {
albumCount: string;
artistImageUrl?: string;
coverArt?: string;
id: string;
name: string;
}[];
smallImageUrl?: string;
};
export type SSArtistInfoParams = {
count?: number;
id: string;
includeNotPresent?: boolean;
};
export type SSArtistInfoResponse = {
artistInfo2: SSArtistInfo;
};
export type SSBaseResponse = {
serverVersion?: 'string';
status: 'string';
type?: 'string';
version: 'string';
};
export type SSFavorite = null;
export type SSFavoriteParams = {
albumId?: string;
artistId?: string;
id?: string;
};
export type SSFavoriteResponse = null;
export type SSGenre = {
albumCount?: number;
songCount?: number;
value: string;
};
export type SSGenreList = SSGenre[];
export type SSGenreListResponse = {
genres: {
genre: SSGenre[];
};
};
export type SSMusicFolder = {
id: number;
name: string;
};
export type SSMusicFolderList = SSMusicFolder[];
export type SSMusicFolderListResponse = {
musicFolders: {
musicFolder: SSMusicFolder[];
};
};
export type SSRating = null;
export type SSRatingParams = {
id: string;
rating: number;
};
export type SSRatingResponse = null;
export type SSScrobbleParams = {
id: string;
submission?: boolean;
time?: number;
};
export type SSSong = {
album: string;
@ -167,39 +206,12 @@ export type SSSong = {
year: number;
};
export type SSAlbumListParams = {
fromYear?: number;
genre?: string;
musicFolderId?: string;
offset?: number;
size?: number;
toYear?: number;
type: string;
export type SSTopSongList = {
items: SSSong[];
startIndex: number;
totalRecordCount: null | number;
};
export type SSAlbumArtistListParams = {
musicFolderId?: string;
};
export type SSFavoriteParams = {
albumId?: string;
artistId?: string;
id?: string;
};
export type SSFavorite = null;
export type SSFavoriteResponse = null;
export type SSRatingParams = {
id: string;
rating: number;
};
export type SSRating = null;
export type SSRatingResponse = null;
export type SSTopSongListParams = {
artist: string;
count?: number;
@ -210,15 +222,3 @@ export type SSTopSongListResponse = {
song: SSSong[];
};
};
export type SSTopSongList = {
items: SSSong[];
startIndex: number;
totalRecordCount: number | null;
};
export type SSScrobbleParams = {
id: string;
submission?: boolean;
time?: number;
};

View file

@ -1,12 +1,13 @@
import { initClient, initContract } from '@ts-rest/core';
import axios, { Method, AxiosError, isAxiosError, AxiosResponse } from 'axios';
import axios, { AxiosError, AxiosResponse, isAxiosError, Method } from 'axios';
import omitBy from 'lodash/omitBy';
import qs from 'qs';
import { z } from 'zod';
import i18n from '/@/i18n/i18n';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
import { ServerListItem } from '/@/renderer/api/types';
import { toast } from '/@/renderer/components/toast/index';
import i18n from '/@/i18n/i18n';
const c = initContract();
@ -284,15 +285,15 @@ const silentlyTransformResponse = (data: any) => {
};
export const ssApiClient = (args: {
server: ServerListItem | null;
server: null | ServerListItem;
signal?: AbortSignal;
silent?: boolean;
url?: string;
}) => {
const { server, url, signal, silent } = args;
const { server, signal, silent, url } = args;
return initClient(contract, {
api: async ({ path, method, headers, body }) => {
api: async ({ body, headers, method, path }) => {
let baseUrl: string | undefined;
const authParams: Record<string, any> = {};
@ -339,7 +340,7 @@ export const ssApiClient = (args: {
headers: result.headers as any,
status: result.status,
};
} catch (e: Error | AxiosError | any) {
} catch (e: any | AxiosError | Error) {
if (isAxiosError(e)) {
if (e.code === 'ERR_NETWORK') {
throw new Error(

View file

@ -2,44 +2,45 @@ import dayjs from 'dayjs';
import filter from 'lodash/filter';
import orderBy from 'lodash/orderBy';
import md5 from 'md5';
import { ServerFeatures } from '/@/renderer/api/features-types';
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize';
import { AlbumListSortType, SubsonicExtensions } from '/@/renderer/api/subsonic/subsonic-types';
import {
LibraryItem,
Song,
ControllerEndpoint,
sortSongList,
sortAlbumArtistList,
PlaylistListSort,
GenreListSort,
AlbumListSort,
ControllerEndpoint,
GenreListSort,
LibraryItem,
PlaylistListSort,
Song,
sortAlbumArtistList,
sortAlbumList,
SortOrder,
sortSongList,
} from '/@/renderer/api/types';
import { randomString } from '/@/renderer/utils';
import { ServerFeatures } from '/@/renderer/api/features-types';
const ALBUM_LIST_SORT_MAPPING: Record<AlbumListSort, AlbumListSortType | undefined> = {
[AlbumListSort.RANDOM]: AlbumListSortType.RANDOM,
[AlbumListSort.ALBUM_ARTIST]: AlbumListSortType.ALPHABETICAL_BY_ARTIST,
[AlbumListSort.PLAY_COUNT]: AlbumListSortType.FREQUENT,
[AlbumListSort.RECENTLY_ADDED]: AlbumListSortType.NEWEST,
[AlbumListSort.FAVORITED]: AlbumListSortType.STARRED,
[AlbumListSort.YEAR]: AlbumListSortType.BY_YEAR,
[AlbumListSort.NAME]: AlbumListSortType.ALPHABETICAL_BY_NAME,
[AlbumListSort.COMMUNITY_RATING]: undefined,
[AlbumListSort.DURATION]: undefined,
[AlbumListSort.CRITIC_RATING]: undefined,
[AlbumListSort.RATING]: undefined,
[AlbumListSort.ARTIST]: undefined,
[AlbumListSort.COMMUNITY_RATING]: undefined,
[AlbumListSort.CRITIC_RATING]: undefined,
[AlbumListSort.DURATION]: undefined,
[AlbumListSort.FAVORITED]: AlbumListSortType.STARRED,
[AlbumListSort.NAME]: AlbumListSortType.ALPHABETICAL_BY_NAME,
[AlbumListSort.PLAY_COUNT]: AlbumListSortType.FREQUENT,
[AlbumListSort.RANDOM]: AlbumListSortType.RANDOM,
[AlbumListSort.RATING]: undefined,
[AlbumListSort.RECENTLY_ADDED]: AlbumListSortType.NEWEST,
[AlbumListSort.RECENTLY_PLAYED]: AlbumListSortType.RECENT,
[AlbumListSort.RELEASE_DATE]: undefined,
[AlbumListSort.SONG_COUNT]: undefined,
[AlbumListSort.YEAR]: AlbumListSortType.BY_YEAR,
};
export const SubsonicController: ControllerEndpoint = {
addToPlaylist: async ({ body, query, apiClientProps }) => {
addToPlaylist: async ({ apiClientProps, body, query }) => {
const res = await ssApiClient(apiClientProps).updatePlaylist({
query: {
playlistId: query.id,
@ -98,7 +99,7 @@ export const SubsonicController: ControllerEndpoint = {
};
},
createFavorite: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).createFavorite({
query: {
@ -114,7 +115,7 @@ export const SubsonicController: ControllerEndpoint = {
return null;
},
createPlaylist: async ({ body, apiClientProps }) => {
createPlaylist: async ({ apiClientProps, body }) => {
const res = await ssApiClient(apiClientProps).createPlaylist({
query: {
name: body.name,
@ -131,7 +132,7 @@ export const SubsonicController: ControllerEndpoint = {
};
},
deleteFavorite: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).removeFavorite({
query: {
@ -148,7 +149,7 @@ export const SubsonicController: ControllerEndpoint = {
return null;
},
deletePlaylist: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).deletePlaylist({
query: {
@ -163,7 +164,7 @@ export const SubsonicController: ControllerEndpoint = {
return null;
},
getAlbumArtistDetail: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
query: {
@ -198,7 +199,7 @@ export const SubsonicController: ControllerEndpoint = {
};
},
getAlbumArtistList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).getArtists({
query: {
@ -237,7 +238,7 @@ export const SubsonicController: ControllerEndpoint = {
getAlbumArtistListCount: (args) =>
SubsonicController.getAlbumArtistList(args).then((res) => res!.totalRecordCount!),
getAlbumDetail: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).getAlbum({
query: {
@ -252,7 +253,7 @@ export const SubsonicController: ControllerEndpoint = {
return ssNormalize.album(res.body.album, apiClientProps.server);
},
getAlbumList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
if (query.searchTerm) {
const res = await ssApiClient(apiClientProps).search3({
@ -398,7 +399,7 @@ export const SubsonicController: ControllerEndpoint = {
};
},
getAlbumListCount: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
if (query.searchTerm) {
let fetchNextPage = true;
@ -516,7 +517,7 @@ export const SubsonicController: ControllerEndpoint = {
return totalRecordCount;
},
getArtistList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).getArtists({
query: {
@ -570,7 +571,7 @@ export const SubsonicController: ControllerEndpoint = {
'&c=Feishin'
);
},
getGenreList: async ({ query, apiClientProps }) => {
getGenreList: async ({ apiClientProps, query }) => {
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
const res = await ssApiClient(apiClientProps).getGenres({});
@ -624,7 +625,7 @@ export const SubsonicController: ControllerEndpoint = {
};
},
getPlaylistDetail: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).getPlaylist({
query: {
@ -638,7 +639,7 @@ export const SubsonicController: ControllerEndpoint = {
return ssNormalize.playlist(res.body.playlist, apiClientProps.server);
},
getPlaylistList: async ({ query, apiClientProps }) => {
getPlaylistList: async ({ apiClientProps, query }) => {
const sortOrder = query.sortOrder.toLowerCase() as 'asc' | 'desc';
const res = await ssApiClient(apiClientProps).getPlaylists({});
@ -686,7 +687,7 @@ export const SubsonicController: ControllerEndpoint = {
totalRecordCount: results.length,
};
},
getPlaylistListCount: async ({ query, apiClientProps }) => {
getPlaylistListCount: async ({ apiClientProps, query }) => {
const res = await ssApiClient(apiClientProps).getPlaylists({});
if (res.status !== 200) {
@ -705,7 +706,7 @@ export const SubsonicController: ControllerEndpoint = {
return results.length;
},
getPlaylistSongList: async ({ query, apiClientProps }) => {
getPlaylistSongList: async ({ apiClientProps, query }) => {
const res = await ssApiClient(apiClientProps).getPlaylist({
query: {
id: query.id,
@ -731,7 +732,7 @@ export const SubsonicController: ControllerEndpoint = {
};
},
getRandomSongList: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).getRandomSongList({
query: {
@ -842,7 +843,7 @@ export const SubsonicController: ControllerEndpoint = {
}, []);
},
getSongDetail: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).getSong({
query: {
@ -856,7 +857,7 @@ export const SubsonicController: ControllerEndpoint = {
return ssNormalize.song(res.body.song, apiClientProps.server);
},
getSongList: async ({ query, apiClientProps }) => {
getSongList: async ({ apiClientProps, query }) => {
const fromAlbumPromises = [];
const artistDetailPromises = [];
let results: any[] = [];
@ -1028,7 +1029,7 @@ export const SubsonicController: ControllerEndpoint = {
};
},
getSongListCount: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
let fetchNextPage = true;
let startIndex = 0;
@ -1196,7 +1197,7 @@ export const SubsonicController: ControllerEndpoint = {
return totalRecordCount;
},
getStructuredLyrics: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).getStructuredLyrics({
query: {
@ -1238,7 +1239,7 @@ export const SubsonicController: ControllerEndpoint = {
});
},
getTopSongs: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).getTopSongsList({
query: {
@ -1261,7 +1262,7 @@ export const SubsonicController: ControllerEndpoint = {
};
},
getTranscodingUrl: (args) => {
const { base, format, bitrate } = args.query;
const { base, bitrate, format } = args.query;
let url = base;
if (format) {
url += `&format=${format}`;
@ -1272,7 +1273,7 @@ export const SubsonicController: ControllerEndpoint = {
return url;
},
removeFromPlaylist: async ({ query, apiClientProps }) => {
removeFromPlaylist: async ({ apiClientProps, query }) => {
const res = await ssApiClient(apiClientProps).updatePlaylist({
query: {
playlistId: query.id,
@ -1287,7 +1288,7 @@ export const SubsonicController: ControllerEndpoint = {
return null;
},
scrobble: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).scrobble({
query: {
@ -1304,7 +1305,7 @@ export const SubsonicController: ControllerEndpoint = {
},
search: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const res = await ssApiClient(apiClientProps).search3({
query: {
@ -1335,7 +1336,7 @@ export const SubsonicController: ControllerEndpoint = {
};
},
setRating: async (args) => {
const { query, apiClientProps } = args;
const { apiClientProps, query } = args;
const itemIds = query.item.map((item) => item.id);
@ -1351,7 +1352,7 @@ export const SubsonicController: ControllerEndpoint = {
return null;
},
updatePlaylist: async (args) => {
const { body, query, apiClientProps } = args;
const { apiClientProps, body, query } = args;
const res = await ssApiClient(apiClientProps).updatePlaylist({
query: {

View file

@ -1,16 +1,17 @@
import { nanoid } from 'nanoid';
import { z } from 'zod';
import { ssType } from '/@/renderer/api/subsonic/subsonic-types';
import {
QueueSong,
LibraryItem,
AlbumArtist,
Album,
AlbumArtist,
Genre,
LibraryItem,
Playlist,
QueueSong,
RelatedArtist,
ServerListItem,
ServerType,
Playlist,
Genre,
RelatedArtist,
} from '/@/renderer/api/types';
const getCoverArtUrl = (args: {
@ -37,9 +38,9 @@ const getCoverArtUrl = (args: {
const getArtists = (
item:
| z.infer<typeof ssType._response.song>
| z.infer<typeof ssType._response.album>
| z.infer<typeof ssType._response.albumListEntry>,
| z.infer<typeof ssType._response.albumListEntry>
| z.infer<typeof ssType._response.song>,
) => {
const albumArtists: RelatedArtist[] = item.albumArtists
? item.albumArtists.map((item) => ({
@ -69,7 +70,7 @@ const getArtists = (
},
];
let participants: Record<string, RelatedArtist[]> | null = null;
let participants: null | Record<string, RelatedArtist[]> = null;
if (item.contributors) {
participants = {};
@ -98,9 +99,9 @@ const getArtists = (
const getGenres = (
item:
| z.infer<typeof ssType._response.song>
| z.infer<typeof ssType._response.album>
| z.infer<typeof ssType._response.albumListEntry>,
| z.infer<typeof ssType._response.albumListEntry>
| z.infer<typeof ssType._response.song>,
): Genre[] => {
return item.genres
? item.genres.map((genre) => ({
@ -123,7 +124,7 @@ const getGenres = (
const normalizeSong = (
item: z.infer<typeof ssType._response.song>,
server: ServerListItem | null,
server: null | ServerListItem,
size?: number,
): QueueSong => {
const imageUrl =
@ -194,7 +195,7 @@ const normalizeAlbumArtist = (
item:
| z.infer<typeof ssType._response.albumArtist>
| z.infer<typeof ssType._response.artistListEntry>,
server: ServerListItem | null,
server: null | ServerListItem,
imageSize?: number,
): AlbumArtist => {
const imageUrl =
@ -229,7 +230,7 @@ const normalizeAlbumArtist = (
const normalizeAlbum = (
item: z.infer<typeof ssType._response.album> | z.infer<typeof ssType._response.albumListEntry>,
server: ServerListItem | null,
server: null | ServerListItem,
imageSize?: number,
): Album => {
const imageUrl =
@ -280,7 +281,7 @@ const normalizePlaylist = (
item:
| z.infer<typeof ssType._response.playlist>
| z.infer<typeof ssType._response.playlistListEntry>,
server: ServerListItem | null,
server: null | ServerListItem,
): Playlist => {
return {
description: item.comment || null,

File diff suppressed because it is too large Load diff

View file

@ -3,10 +3,11 @@ import isElectron from 'is-electron';
import semverCoerce from 'semver/functions/coerce';
import semverGte from 'semver/functions/gte';
import { z } from 'zod';
import { ServerFeature } from '/@/renderer/api/features-types';
import { ServerListItem } from '/@/renderer/api/types';
import { toast } from '/@/renderer/components/toast';
import { useAuthStore } from '/@/renderer/store';
import { ServerListItem } from '/@/renderer/api/types';
import { ServerFeature } from '/@/renderer/api/features-types';
// Since ts-rest client returns a strict response type, we need to add the headers to the body object
export const resultWithHeaders = <ItemType extends z.ZodTypeAny>(itemSchema: ItemType) => {
@ -29,7 +30,7 @@ export const resultSubsonicBaseResponse = <ItemType extends z.ZodRawShape>(
});
};
export const authenticationFailure = (currentServer: ServerListItem | null) => {
export const authenticationFailure = (currentServer: null | ServerListItem) => {
toast.error({
message: 'Your session has expired.',
});
@ -43,7 +44,7 @@ export const authenticationFailure = (currentServer: ServerListItem | null) => {
}
};
export const hasFeature = (server: ServerListItem | null, feature: ServerFeature): boolean => {
export const hasFeature = (server: null | ServerListItem, feature: ServerFeature): boolean => {
if (!server || !server.features) {
return false;
}

View file

@ -1,33 +1,39 @@
import { useEffect, useMemo, useState, useRef } from 'react';
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
import { ModuleRegistry } from '@ag-grid-community/core';
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
import { MantineProvider } from '@mantine/core';
import isElectron from 'is-electron';
import { useEffect, useMemo, useRef, useState } from 'react';
import { initSimpleImg } from 'react-simple-img';
import i18n from '../i18n/i18n';
import { toast } from './components';
import { useTheme } from './hooks';
import { IsUpdatedDialog } from './is-updated-dialog';
import { AppRouter } from './router/app-router';
import './styles/global.scss';
import '@ag-grid-community/styles/ag-grid.css';
import 'overlayscrollbars/overlayscrollbars.css';
import {
useCssSettings,
useHotkeySettings,
usePlaybackSettings,
useRemoteSettings,
useSettingsStore,
} from './store/settings.store';
import './styles/global.scss';
import { ContextMenuProvider } from '/@/renderer/features/context-menu';
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
import { PlayerState, useCssSettings, usePlayerStore, useQueueControls } from '/@/renderer/store';
import { FontType, PlaybackType, PlayerStatus, WebAudio } from '/@/renderer/types';
import '@ag-grid-community/styles/ag-grid.css';
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
import i18n from '/@/i18n/i18n';
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
import { PlayQueueHandlerContext } from '/@/renderer/features/player';
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
import { updateSong } from '/@/renderer/features/player/update-remote-song';
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
import { IsUpdatedDialog } from '/@/renderer/is-updated-dialog';
import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store';
import { FontType, PlaybackType, PlayerStatus, WebAudio } from '/@/renderer/types';
import { sanitizeCss } from '/@/renderer/utils/sanitize';
import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
@ -35,10 +41,10 @@ ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule
initSimpleImg({ threshold: 0.05 }, true);
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const ipc = isElectron() ? window.electron.ipc : null;
const remote = isElectron() ? window.electron.remote : null;
const utils = isElectron() ? window.electron.utils : null;
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
const ipc = isElectron() ? window.api.ipc : null;
const remote = isElectron() ? window.api.remote : null;
const utils = isElectron() ? window.api.utils : null;
export const App = () => {
const theme = useTheme();
@ -46,7 +52,7 @@ export const App = () => {
const language = useSettingsStore((store) => store.general.language);
const nativeImageAspect = useSettingsStore((store) => store.general.nativeAspectRatio);
const { builtIn, custom, system, type } = useSettingsStore((state) => state.font);
const { enabled, content } = useCssSettings();
const { content, enabled } = useCssSettings();
const { type: playbackType } = usePlaybackSettings();
const { bindings } = useHotkeySettings();
const handlePlayQueueAdd = useHandlePlayQueueAdd();
@ -230,10 +236,8 @@ export const App = () => {
return (
<MantineProvider
withGlobalStyles
withNormalizeCSS
theme={{
colorScheme: theme as 'light' | 'dark',
colorScheme: theme as 'dark' | 'light',
components: {
Modal: {
styles: {
@ -282,6 +286,8 @@ export const App = () => {
xs: '0rem',
},
}}
withGlobalStyles
withNormalizeCSS
>
<PlayQueueHandlerContext.Provider value={providerValue}>
<ContextMenuProvider>

31
src/renderer/assets/assets.d.ts vendored Normal file
View file

@ -0,0 +1,31 @@
type Styles = Record<string, string>;
declare module '*.svg' {
const content: string;
export default content;
}
declare module '*.png' {
const content: string;
export default content;
}
declare module '*.jpg' {
const content: string;
export default content;
}
declare module '*.scss' {
const content: Styles;
export default content;
}
declare module '*.sass' {
const content: Styles;
export default content;
}
declare module '*.css' {
const content: Styles;
export default content;
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

View file

@ -1,4 +1,5 @@
import type { AccordionProps as MantineAccordionProps } from '@mantine/core';
import { Accordion as MantineAccordion } from '@mantine/core';
import styled from 'styled-components';

View file

@ -1,27 +1,36 @@
import type { Song } from '/@/renderer/api/types';
import type { CrossfadeStyle } from '/@/renderer/types';
import type { ReactPlayerProps } from 'react-player';
import isElectron from 'is-electron';
import {
useImperativeHandle,
forwardRef,
useRef,
useState,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import isElectron from 'is-electron';
import type { ReactPlayerProps } from 'react-player';
import ReactPlayer from 'react-player/lazy';
import type { Song } from '/@/renderer/api/types';
import { api } from '/@/renderer/api';
import {
crossfadeHandler,
gaplessHandler,
} from '/@/renderer/components/audio-player/utils/list-handlers';
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import type { CrossfadeStyle } from '/@/renderer/types';
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
import { toast } from '/@/renderer/components/toast';
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
import { getServerById, TranscodingConfig, usePlaybackSettings, useSpeed } from '/@/renderer/store';
import { toast } from '/@/renderer/components/toast';
import { api } from '/@/renderer/api';
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
export type AudioPlayerProgress = {
loaded: number;
loadedSeconds: number;
played: number;
playedSeconds: number;
};
interface AudioPlayerProps extends ReactPlayerProps {
crossfadeDuration: number;
@ -34,13 +43,6 @@ interface AudioPlayerProps extends ReactPlayerProps {
volume: number;
}
export type AudioPlayerProgress = {
loaded: number;
loadedSeconds: number;
played: number;
playedSeconds: number;
};
const getDuration = (ref: any) => {
return ref.current?.player?.player?.player?.duration;
};
@ -53,7 +55,7 @@ const getDuration = (ref: any) => {
const EMPTY_SOURCE =
'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV';
const useSongUrl = (transcode: TranscodingConfig, current: boolean, song?: Song): string | null => {
const useSongUrl = (transcode: TranscodingConfig, current: boolean, song?: Song): null | string => {
const prior = useRef(['', '']);
return useMemo(() => {
@ -94,15 +96,15 @@ const useSongUrl = (transcode: TranscodingConfig, current: boolean, song?: Song)
export const AudioPlayer = forwardRef(
(
{
status,
playbackStyle,
crossfadeStyle,
crossfadeDuration,
currentPlayer,
autoNext,
crossfadeDuration,
crossfadeStyle,
currentPlayer,
muted,
playbackStyle,
player1,
player2,
muted,
status,
volume,
}: AudioPlayerProps,
ref: any,
@ -120,7 +122,7 @@ export const AudioPlayer = forwardRef(
const stream1 = useSongUrl(transcode, currentPlayer === 1, player1);
const stream2 = useSongUrl(transcode, currentPlayer === 2, player2);
const { webAudio, setWebAudio } = useWebAudio();
const { setWebAudio, webAudio } = useWebAudio();
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(
null,
);
@ -415,43 +417,43 @@ export const AudioPlayer = forwardRef(
return (
<>
<ReactPlayer
ref={player1Ref}
config={{
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
}}
height={0}
muted={muted}
playbackRate={playbackSpeed}
playing={currentPlayer === 1 && status === PlayerStatus.PLAYING}
progressInterval={isTransitioning ? 10 : 250}
url={stream1 || EMPTY_SOURCE}
volume={webAudio ? 1 : volume}
width={0}
// If there is no stream url, we do not need to handle when the audio finishes
onEnded={stream1 ? handleOnEnded : undefined}
onProgress={
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1
}
onReady={handlePlayer1Start}
playbackRate={playbackSpeed}
playing={currentPlayer === 1 && status === PlayerStatus.PLAYING}
progressInterval={isTransitioning ? 10 : 250}
ref={player1Ref}
url={stream1 || EMPTY_SOURCE}
volume={webAudio ? 1 : volume}
width={0}
/>
<ReactPlayer
ref={player2Ref}
config={{
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
}}
height={0}
muted={muted}
playbackRate={playbackSpeed}
playing={currentPlayer === 2 && status === PlayerStatus.PLAYING}
progressInterval={isTransitioning ? 10 : 250}
url={stream2 || EMPTY_SOURCE}
volume={webAudio ? 1 : volume}
width={0}
onEnded={stream2 ? handleOnEnded : undefined}
onProgress={
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2
}
onReady={handlePlayer2Start}
playbackRate={playbackSpeed}
playing={currentPlayer === 2 && status === PlayerStatus.PLAYING}
progressInterval={isTransitioning ? 10 : 250}
ref={player2Ref}
url={stream2 || EMPTY_SOURCE}
volume={webAudio ? 1 : volume}
width={0}
/>
</>
);

View file

@ -1,5 +1,5 @@
/* eslint-disable no-nested-ternary */
import type { Dispatch } from 'react';
import { CrossfadeStyle } from '/@/renderer/types';
export const gaplessHandler = (args: {
@ -10,7 +10,7 @@ export const gaplessHandler = (args: {
nextPlayerRef: any;
setIsTransitioning: Dispatch<boolean>;
}) => {
const { nextPlayerRef, currentTime, duration, isTransitioning, setIsTransitioning, isFlac } =
const { currentTime, duration, isFlac, isTransitioning, nextPlayerRef, setIsTransitioning } =
args;
if (!isTransitioning) {
@ -46,17 +46,17 @@ export const crossfadeHandler = (args: {
volume: number;
}) => {
const {
currentTime,
player,
currentPlayer,
currentPlayerRef,
nextPlayerRef,
currentTime,
duration,
fadeDuration,
fadeType,
duration,
volume,
isTransitioning,
nextPlayerRef,
player,
setIsTransitioning,
volume,
} = args;
if (!isTransitioning || currentPlayer !== player) {
@ -79,22 +79,18 @@ export const crossfadeHandler = (args: {
let percentageOfFadeLeft;
let n;
switch (fadeType) {
case 'equalPower':
// https://dsp.stackexchange.com/a/14755
percentageOfFadeLeft = (timeLeft / fadeDuration) * 2;
currentPlayerVolumeCalculation = Math.sqrt(0.5 * percentageOfFadeLeft) * volume;
nextPlayerVolumeCalculation = Math.sqrt(0.5 * (2 - percentageOfFadeLeft)) * volume;
break;
case 'linear':
currentPlayerVolumeCalculation = (timeLeft / fadeDuration) * volume;
nextPlayerVolumeCalculation = ((fadeDuration - timeLeft) / fadeDuration) * volume;
break;
case 'dipped':
// https://math.stackexchange.com/a/4622
percentageOfFadeLeft = timeLeft / fadeDuration;
currentPlayerVolumeCalculation = percentageOfFadeLeft ** 2 * volume;
nextPlayerVolumeCalculation = (percentageOfFadeLeft - 1) ** 2 * volume;
break;
case 'equalPower':
// https://dsp.stackexchange.com/a/14755
percentageOfFadeLeft = (timeLeft / fadeDuration) * 2;
currentPlayerVolumeCalculation = Math.sqrt(0.5 * percentageOfFadeLeft) * volume;
nextPlayerVolumeCalculation = Math.sqrt(0.5 * (2 - percentageOfFadeLeft)) * volume;
break;
case fadeType.match(/constantPower.*/)?.input:
// https://math.stackexchange.com/a/26159
n =
@ -114,6 +110,10 @@ export const crossfadeHandler = (args: {
Math.cos((Math.PI / 4) * ((2 * percentageOfFadeLeft - 1) ** (2 * n + 1) + 1)) *
volume;
break;
case 'linear':
currentPlayerVolumeCalculation = (timeLeft / fadeDuration) * volume;
nextPlayerVolumeCalculation = ((fadeDuration - timeLeft) / fadeDuration) * volume;
break;
default:
currentPlayerVolumeCalculation = (timeLeft / fadeDuration) * volume;

View file

@ -1,4 +1,5 @@
import type { BadgeProps as MantineBadgeProps } from '@mantine/core';
import { createPolymorphicComponent, Badge as MantineBadge } from '@mantine/core';
import styled from 'styled-components';

View file

@ -1,9 +1,11 @@
import type { Ref } from 'react';
import React, { useRef, useCallback, useState, forwardRef } from 'react';
import type { ButtonProps as MantineButtonProps, TooltipProps } from '@mantine/core';
import { Button as MantineButton, createPolymorphicComponent } from '@mantine/core';
import type { Ref } from 'react';
import { createPolymorphicComponent, Button as MantineButton } from '@mantine/core';
import { useTimeout } from '@mantine/hooks';
import React, { forwardRef, useCallback, useRef, useState } from 'react';
import styled from 'styled-components';
import { Spinner } from '/@/renderer/components/spinner';
import { Tooltip } from '/@/renderer/components/tooltip';
@ -94,8 +96,8 @@ export const _Button = forwardRef<HTMLButtonElement, ButtonProps>(
{...tooltip}
>
<StyledButton
ref={ref}
loaderPosition="center"
ref={ref}
{...props}
>
<ButtonChildWrapper $loading={props.loading}>{children}</ButtonChildWrapper>
@ -111,8 +113,8 @@ export const _Button = forwardRef<HTMLButtonElement, ButtonProps>(
return (
<StyledButton
ref={ref}
loaderPosition="center"
ref={ref}
{...props}
>
<ButtonChildWrapper $loading={props.loading}>{children}</ButtonChildWrapper>
@ -153,7 +155,7 @@ export const TimeoutButton = ({ timeoutProps, ...props }: HoldButtonProps) => {
setIsRunning(false);
};
const { start, clear } = useTimeout(callback, timeoutProps.duration);
const { clear, start } = useTimeout(callback, timeoutProps.duration);
const startTimeout = useCallback(() => {
if (isRunning) {
@ -174,8 +176,8 @@ export const TimeoutButton = ({ timeoutProps, ...props }: HoldButtonProps) => {
return (
<Button
sx={{ color: 'var(--danger-color)' }}
onClick={startTimeout}
sx={{ color: 'var(--danger-color)' }}
{...props}
>
{isRunning ? 'Cancel' : props.children}

View file

@ -1,14 +1,16 @@
import { useCallback } from 'react';
import type { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/renderer/types';
import { Center } from '@mantine/core';
import { useCallback } from 'react';
import { RiAlbumFill } from 'react-icons/ri';
import { generatePath, useNavigate } from 'react-router';
import { SimpleImg } from 'react-simple-img';
import styled from 'styled-components';
import type { CardRow, CardRoute, Play, PlayQueueAddOptions } from '/@/renderer/types';
import { Skeleton } from '/@/renderer/components/skeleton';
import { CardControls } from '/@/renderer/components/card/card-controls';
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
import { CardControls } from '/@/renderer/components/card/card-controls';
import { CardRows } from '/@/renderer/components/card/card-rows';
import { Skeleton } from '/@/renderer/components/skeleton';
const CardWrapper = styled.div<{
link?: boolean;
@ -104,7 +106,7 @@ const Row = styled.div<{ $secondary?: boolean }>`
interface BaseGridCardProps {
controls: {
cardRows: CardRow<Album | Artist | AlbumArtist>[];
cardRows: CardRow<Album | AlbumArtist | Artist>[];
itemType: LibraryItem;
playButtonBehavior: Play;
route: CardRoute;
@ -116,14 +118,14 @@ interface BaseGridCardProps {
}
export const AlbumCard = ({
controls,
data,
handlePlayQueueAdd,
loading,
size,
handlePlayQueueAdd,
data,
controls,
}: BaseGridCardProps) => {
const navigate = useNavigate();
const { itemType, cardRows, route } = controls;
const { cardRows, itemType, route } = controls;
const handleNavigate = useCallback(() => {
navigate(
@ -194,9 +196,9 @@ export const AlbumCard = ({
<CardWrapper>
<StyledCard style={{ alignItems: 'center', display: 'flex' }}>
<Skeleton
visible
height={size}
radius="sm"
visible
width={size}
>
<ImageSection />
@ -204,10 +206,10 @@ export const AlbumCard = ({
<DetailSection style={{ width: '100%' }}>
{(cardRows || []).map((_row: CardRow<Album>, index: number) => (
<Skeleton
visible
height={15}
my={3}
radius="md"
visible
width={!data ? (index > 0 ? '50%' : '90%') : '100%'}
>
<Row />

View file

@ -1,21 +1,23 @@
import type { MouseEvent } from 'react';
import React from 'react';
import type { UnstyledButtonProps } from '@mantine/core';
import { Group } from '@mantine/core';
import { RiPlayFill, RiMore2Fill, RiHeartFill, RiHeartLine } from 'react-icons/ri';
import styled from 'styled-components';
import { _Button } from '/@/renderer/components/button';
import type { PlayQueueAddOptions } from '/@/renderer/types';
import { Play } from '/@/renderer/types';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import type { UnstyledButtonProps } from '@mantine/core';
import type { MouseEvent } from 'react';
import { Group } from '@mantine/core';
import React from 'react';
import { RiHeartFill, RiHeartLine, RiMore2Fill, RiPlayFill } from 'react-icons/ri';
import styled from 'styled-components';
import { LibraryItem } from '/@/renderer/api/types';
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
import { _Button } from '/@/renderer/components/button';
import {
ALBUM_CONTEXT_MENU_ITEMS,
ARTIST_CONTEXT_MENU_ITEMS,
} from '/@/renderer/features/context-menu/context-menu-items';
import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { Play } from '/@/renderer/types';
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
type PlayButtonType = React.ComponentPropsWithoutRef<'button'> & UnstyledButtonProps;
const PlayButton = styled.button<PlayButtonType>`
display: flex;
@ -104,9 +106,9 @@ const FavoriteWrapper = styled.span<{ isFavorite: boolean }>`
`;
export const CardControls = ({
handlePlayQueueAdd,
itemData,
itemType,
handlePlayQueueAdd,
}: {
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
itemData: any;
@ -156,14 +158,14 @@ export const CardControls = ({
</FavoriteWrapper>
</SecondaryButton>
<SecondaryButton
p={5}
sx={{ svg: { fill: 'white !important' } }}
variant="subtle"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleContextMenu(e, [itemData]);
}}
p={5}
sx={{ svg: { fill: 'white !important' } }}
variant="subtle"
>
<RiMore2Fill
color="white"

View file

@ -1,8 +1,9 @@
import React from 'react';
import formatDuration from 'format-duration';
import React from 'react';
import { generatePath } from 'react-router';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { Album, AlbumArtist, Artist, Playlist, Song } from '/@/renderer/api/types';
import { Text } from '/@/renderer/components/text';
import { AppRoute } from '/@/renderer/router/routes';
@ -23,7 +24,7 @@ const Row = styled.div<{ $secondary?: boolean }>`
interface CardRowsProps {
data: any;
rows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
rows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
}
export const CardRows = ({ data, rows }: CardRowsProps) => {
@ -33,8 +34,8 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
if (row.arrayProperty && row.route) {
return (
<Row
key={`row-${row.property}-${index}`}
$secondary={index > 0}
key={`row-${row.property}-${index}`}
>
{data[row.property].map((item: any, itemIndex: number) => (
<React.Fragment key={`${data.id}-${item.id}`}>
@ -55,6 +56,7 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
$noSelect
$secondary={index > 0}
component={Link}
onClick={(e) => e.stopPropagation()}
overflow="hidden"
size={index > 0 ? 'sm' : 'md'}
to={generatePath(
@ -69,7 +71,6 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
};
}, {}),
)}
onClick={(e) => e.stopPropagation()}
>
{row.arrayProperty &&
(row.format
@ -87,9 +88,9 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
<Row key={`row-${row.property}`}>
{data[row.property].map((item: any) => (
<Text
key={`${data.id}-${item.id}`}
$noSelect
$secondary={index > 0}
key={`${data.id}-${item.id}`}
overflow="hidden"
size={index > 0 ? 'sm' : 'md'}
>
@ -108,6 +109,7 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
$link
$noSelect
component={Link}
onClick={(e) => e.stopPropagation()}
overflow="hidden"
to={generatePath(
row.route.route,
@ -118,7 +120,6 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
};
}, {}),
)}
onClick={(e) => e.stopPropagation()}
>
{data && (row.format ? row.format(data) : data[row.property])}
</Text>

View file

@ -3,15 +3,16 @@ import { RiAlbumFill, RiPlayListFill, RiUserVoiceFill } from 'react-icons/ri';
import { generatePath, Link } from 'react-router-dom';
import { SimpleImg } from 'react-simple-img';
import styled, { css } from 'styled-components';
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
import { CardRows } from '/@/renderer/components/card';
import { Skeleton } from '/@/renderer/components/skeleton';
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types';
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/renderer/types';
interface BaseGridCardProps {
controls: {
cardRows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
cardRows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
handleFavorite: (options: {
id: string[];
isFavorite: boolean;
@ -101,8 +102,8 @@ const DetailContainer = styled.div`
`;
export const PosterCard = ({
data,
controls,
data,
isLoading,
uniqueId,
}: BaseGridCardProps & { uniqueId: string }) => {
@ -123,10 +124,10 @@ export const PosterCard = ({
case LibraryItem.ALBUM:
Placeholder = RiAlbumFill;
break;
case LibraryItem.ARTIST:
case LibraryItem.ALBUM_ARTIST:
Placeholder = RiUserVoiceFill;
break;
case LibraryItem.ALBUM_ARTIST:
case LibraryItem.ARTIST:
Placeholder = RiUserVoiceFill;
break;
case LibraryItem.PLAYLIST:
@ -184,8 +185,8 @@ export const PosterCard = ({
return (
<PosterCardContainer key={`placeholder-${uniqueId}-${data.id}`}>
<Skeleton
visible
radius="sm"
visible
>
<ImageContainerSkeleton />
</Skeleton>
@ -193,10 +194,10 @@ export const PosterCard = ({
<Stack spacing="sm">
{(controls?.cardRows || []).map((row, index) => (
<Skeleton
key={`${index}-${row.arrayProperty}`}
visible
height={14}
key={`${index}-${row.arrayProperty}`}
radius="sm"
visible
/>
))}
</Stack>

View file

@ -1,5 +1,5 @@
import { CheckboxProps, Checkbox as MantineCheckbox } from '@mantine/core';
import { forwardRef } from 'react';
import { Checkbox as MantineCheckbox, CheckboxProps } from '@mantine/core';
import styled from 'styled-components';
const StyledCheckbox = styled(MantineCheckbox)`

View file

@ -1,6 +1,6 @@
import { ComponentPropsWithoutRef, forwardRef, ReactNode, Ref } from 'react';
import { Box, Group, UnstyledButton, UnstyledButtonProps } from '@mantine/core';
import { motion, Variants } from 'framer-motion';
import { ComponentPropsWithoutRef, forwardRef, ReactNode, Ref } from 'react';
import styled from 'styled-components';
interface ContextMenuProps {
@ -61,11 +61,11 @@ export const ContextMenuButton = forwardRef(
(
{
children,
rightIcon,
leftIcon,
rightIcon,
...props
}: UnstyledButtonProps &
ComponentPropsWithoutRef<'button'> & {
}: ComponentPropsWithoutRef<'button'> &
UnstyledButtonProps & {
leftIcon?: ReactNode;
rightIcon?: ReactNode;
},
@ -74,11 +74,11 @@ export const ContextMenuButton = forwardRef(
return (
<StyledContextMenuButton
{...props}
key={props.key}
ref={ref}
as="button"
disabled={props.disabled}
key={props.key}
onClick={props.onClick}
ref={ref}
>
<Group position="apart">
<Group spacing="md">
@ -108,14 +108,14 @@ const variants: Variants = {
};
export const ContextMenu = forwardRef(
({ yPos, xPos, minWidth, maxWidth, children }: ContextMenuProps, ref: Ref<HTMLDivElement>) => {
({ children, maxWidth, minWidth, xPos, yPos }: ContextMenuProps, ref: Ref<HTMLDivElement>) => {
return (
<ContextMenuContainer
ref={ref}
animate="open"
initial="closed"
maxWidth={maxWidth}
minWidth={minWidth}
ref={ref}
variants={variants}
xPos={xPos}
yPos={yPos}

View file

@ -1,4 +1,5 @@
import type { DatePickerProps as MantineDatePickerProps } from '@mantine/dates';
import { DatePicker as MantineDatePicker } from '@mantine/dates';
import styled from 'styled-components';
@ -34,7 +35,7 @@ const StyledDatePicker = styled(MantineDatePicker)<DatePickerProps>`
}
`;
export const DatePicker = ({ width, maxWidth, ...props }: DatePickerProps) => {
export const DatePicker = ({ maxWidth, width, ...props }: DatePickerProps) => {
return (
<StyledDatePicker
{...props}

View file

@ -1,4 +1,5 @@
import type { DialogProps as MantineDialogProps } from '@mantine/core';
import { Dialog as MantineDialog } from '@mantine/core';
import styled from 'styled-components';

View file

@ -1,24 +1,25 @@
import { ReactNode } from 'react';
import type {
MenuProps as MantineMenuProps,
MenuItemProps as MantineMenuItemProps,
MenuLabelProps as MantineMenuLabelProps,
MenuDividerProps as MantineMenuDividerProps,
MenuDropdownProps as MantineMenuDropdownProps,
MenuItemProps as MantineMenuItemProps,
MenuLabelProps as MantineMenuLabelProps,
MenuProps as MantineMenuProps,
} from '@mantine/core';
import { Menu as MantineMenu, createPolymorphicComponent } from '@mantine/core';
import { createPolymorphicComponent, Menu as MantineMenu } from '@mantine/core';
import { ReactNode } from 'react';
import { RiArrowLeftSFill } from 'react-icons/ri';
import styled from 'styled-components';
type MenuProps = MantineMenuProps;
type MenuLabelProps = MantineMenuLabelProps;
type MenuDividerProps = MantineMenuDividerProps;
type MenuDropdownProps = MantineMenuDropdownProps;
interface MenuItemProps extends MantineMenuItemProps {
$danger?: boolean;
$isActive?: boolean;
children: ReactNode;
}
type MenuDividerProps = MantineMenuDividerProps;
type MenuDropdownProps = MantineMenuDropdownProps;
type MenuLabelProps = MantineMenuLabelProps;
type MenuProps = MantineMenuProps;
const StyledMenu = styled(MantineMenu)<MenuProps>``;
@ -81,7 +82,6 @@ const StyledMenuDivider = styled(MantineMenu.Divider)`
export const DropdownMenu = ({ children, ...props }: MenuProps) => {
return (
<StyledMenu
withinPortal
styles={{
dropdown: {
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 50%))',
@ -90,6 +90,7 @@ export const DropdownMenu = ({ children, ...props }: MenuProps) => {
transitionProps={{
transition: 'fade',
}}
withinPortal
{...props}
>
{children}
@ -101,7 +102,7 @@ const MenuLabel = ({ children, ...props }: MenuLabelProps) => {
return <StyledMenuLabel {...props}>{children}</StyledMenuLabel>;
};
const pMenuItem = ({ $isActive, $danger, children, ...props }: MenuItemProps) => {
const pMenuItem = ({ $danger, $isActive, children, ...props }: MenuItemProps) => {
return (
<StyledMenuItem
$danger={$danger}

View file

@ -1,20 +1,22 @@
import type { MouseEvent } from 'react';
import { useState } from 'react';
import { Group, Image, Stack } from '@mantine/core';
import type { Variants } from 'framer-motion';
import type { MouseEvent } from 'react';
import { Group, Image, Stack } from '@mantine/core';
import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
import { Link, generatePath } from 'react-router-dom';
import { generatePath, Link } from 'react-router-dom';
import styled from 'styled-components';
import { Album, LibraryItem } from '/@/renderer/api/types';
import { Badge } from '/@/renderer/components/badge';
import { Button } from '/@/renderer/components/button';
import { TextTitle } from '/@/renderer/components/text-title';
import { Badge } from '/@/renderer/components/badge';
import { AppRoute } from '/@/renderer/router/routes';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import { Play } from '/@/renderer/types';
import { AppRoute } from '/@/renderer/router/routes';
import { usePlayButtonBehavior } from '/@/renderer/store';
import { Play } from '/@/renderer/types';
const Carousel = styled(motion.div)`
position: relative;
@ -152,11 +154,11 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
>
{data && (
<Carousel
key={`image-${itemIndex}`}
animate="animate"
custom={direction}
exit="exit"
initial="initial"
key={`image-${itemIndex}`}
variants={variants}
>
<Grid>
@ -218,9 +220,6 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
</Group>
<Group position="apart">
<Button
size="lg"
style={{ borderRadius: '5rem' }}
variant="outline"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@ -234,6 +233,9 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
playType,
});
}}
size="lg"
style={{ borderRadius: '5rem' }}
variant="outline"
>
{t(
playType === Play.NOW
@ -246,18 +248,18 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
</Button>
<Group spacing="sm">
<Button
onClick={handlePrevious}
radius="lg"
size="sm"
variant="outline"
onClick={handlePrevious}
>
<RiArrowLeftSLine size="2rem" />
</Button>
<Button
onClick={handleNext}
radius="lg"
size="sm"
variant="outline"
onClick={handleNext}
>
<RiArrowRightSLine size="2rem" />
</Button>

View file

@ -1,3 +1,5 @@
import { Group, Stack } from '@mantine/core';
import throttle from 'lodash/throttle';
import {
isValidElement,
memo,
@ -9,14 +11,13 @@ import {
useRef,
useState,
} from 'react';
import { Group, Stack } from '@mantine/core';
import throttle from 'lodash/throttle';
import { RiArrowLeftSLine, RiArrowRightSLine } from 'react-icons/ri';
import styled from 'styled-components';
import { SwiperOptions, Virtual } from 'swiper';
import 'swiper/css';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Swiper as SwiperCore } from 'swiper/types';
import { Album, AlbumArtist, Artist, LibraryItem, RelatedArtist } from '/@/renderer/api/types';
import { Button } from '/@/renderer/components/button';
import { PosterCard } from '/@/renderer/components/card/poster-card';
@ -44,14 +45,14 @@ const CarouselContainer = styled(Stack)`
interface TitleProps {
handleNext?: () => void;
handlePrev?: () => void;
label?: string | ReactNode;
label?: ReactNode | string;
pagination: {
hasNextPage: boolean;
hasPreviousPage: boolean;
};
}
const Title = ({ label, handleNext, handlePrev, pagination }: TitleProps) => {
const Title = ({ handleNext, handlePrev, label, pagination }: TitleProps) => {
return (
<Group position="apart">
{isValidElement(label) ? (
@ -69,18 +70,18 @@ const Title = ({ label, handleNext, handlePrev, pagination }: TitleProps) => {
<Button
compact
disabled={!pagination.hasPreviousPage}
onClick={handlePrev}
size="lg"
variant="default"
onClick={handlePrev}
>
<RiArrowLeftSLine />
</Button>
<Button
compact
disabled={!pagination.hasNextPage}
onClick={handleNext}
size="lg"
variant="default"
onClick={handleNext}
>
<RiArrowRightSLine />
</Button>
@ -90,7 +91,7 @@ const Title = ({ label, handleNext, handlePrev, pagination }: TitleProps) => {
};
export interface SwiperGridCarouselProps {
cardRows: CardRow<Album>[] | CardRow<Artist>[] | CardRow<AlbumArtist>[];
cardRows: CardRow<Album>[] | CardRow<AlbumArtist>[] | CardRow<Artist>[];
data: Album[] | AlbumArtist[] | Artist[] | RelatedArtist[] | undefined;
isLoading?: boolean;
itemType: LibraryItem;
@ -100,7 +101,7 @@ export interface SwiperGridCarouselProps {
children?: ReactNode;
hasPagination?: boolean;
icon?: ReactNode;
label: string | ReactNode;
label: ReactNode | string;
};
uniqueId: string;
}
@ -108,15 +109,15 @@ export interface SwiperGridCarouselProps {
export const SwiperGridCarousel = ({
cardRows,
data,
isLoading,
itemType,
route,
swiperProps,
title,
isLoading,
uniqueId,
}: SwiperGridCarouselProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const swiperRef = useRef<SwiperCore | any>(null);
const swiperRef = useRef<any | SwiperCore>(null);
const playButtonBehavior = usePlayButtonBehavior();
const handlePlayQueueAdd = usePlayQueueAdd();
const [slideCount, setSlideCount] = useState(4);
@ -140,7 +141,7 @@ export const SwiperGridCarousel = ({
itemType: LibraryItem;
serverId: string;
}) => {
const { id, itemType, isFavorite, serverId } = options;
const { id, isFavorite, itemType, serverId } = options;
if (isFavorite) {
deleteFavoriteMutation.mutate({
query: {
@ -205,7 +206,7 @@ export const SwiperGridCarousel = ({
}, [slideCount, swiperProps?.slidesPerView]);
const handleOnSlideChange = useCallback((e: SwiperCore) => {
const { slides, isEnd, isBeginning, params } = e;
const { isBeginning, isEnd, params, slides } = e;
if (isEnd || isBeginning) return;
const slideCount = (params.slidesPerView as number | undefined) || 4;
@ -216,7 +217,7 @@ export const SwiperGridCarousel = ({
}, []);
const handleOnZoomChange = useCallback((e: SwiperCore) => {
const { slides, isEnd, isBeginning, params } = e;
const { isBeginning, isEnd, params, slides } = e;
if (isEnd || isBeginning) return;
const slideCount = (params.slidesPerView as number | undefined) || 4;
@ -227,7 +228,7 @@ export const SwiperGridCarousel = ({
}, []);
const handleOnReachEnd = useCallback((e: SwiperCore) => {
const { slides, params } = e;
const { params, slides } = e;
const slideCount = (params.slidesPerView as number | undefined) || 4;
setPagination({
@ -237,7 +238,7 @@ export const SwiperGridCarousel = ({
}, []);
const handleOnReachBeginning = useCallback((e: SwiperCore) => {
const { slides, params } = e;
const { params, slides } = e;
const slideCount = (params.slidesPerView as number | undefined) || 4;
setPagination({
@ -279,8 +280,8 @@ export const SwiperGridCarousel = ({
return (
<CarouselContainer
ref={containerRef}
className="grid-carousel"
ref={containerRef}
spacing="md"
>
{title ? (
@ -292,12 +293,7 @@ export const SwiperGridCarousel = ({
/>
) : null}
<Swiper
ref={swiperRef}
resizeObserver
modules={[Virtual]}
slidesPerView={slideCount}
spaceBetween={20}
style={{ height: '100%', width: '100%' }}
onBeforeInit={(swiper) => {
swiperRef.current = swiper;
}}
@ -305,6 +301,11 @@ export const SwiperGridCarousel = ({
onReachEnd={handleOnReachEnd}
onSlideChange={handleOnSlideChange}
onZoomChange={handleOnZoomChange}
ref={swiperRef}
resizeObserver
slidesPerView={slideCount}
spaceBetween={20}
style={{ height: '100%', width: '100%' }}
{...swiperProps}
>
{slides.map((slideContent, index) => {

View file

@ -1,4 +1,4 @@
import { HoverCard as MantineHoverCard, HoverCardProps } from '@mantine/core';
import { HoverCardProps, HoverCard as MantineHoverCard } from '@mantine/core';
export const HoverCard = ({ children, ...props }: HoverCardProps) => {
return (

View file

@ -1,23 +1,30 @@
import React, { forwardRef } from 'react';
import type {
TextInputProps as MantineTextInputProps,
NumberInputProps as MantineNumberInputProps,
PasswordInputProps as MantinePasswordInputProps,
FileInputProps as MantineFileInputProps,
JsonInputProps as MantineJsonInputProps,
NumberInputProps as MantineNumberInputProps,
PasswordInputProps as MantinePasswordInputProps,
TextareaProps as MantineTextareaProps,
TextInputProps as MantineTextInputProps,
} from '@mantine/core';
import {
TextInput as MantineTextInput,
NumberInput as MantineNumberInput,
PasswordInput as MantinePasswordInput,
FileInput as MantineFileInput,
JsonInput as MantineJsonInput,
NumberInput as MantineNumberInput,
PasswordInput as MantinePasswordInput,
Textarea as MantineTextarea,
TextInput as MantineTextInput,
} from '@mantine/core';
import React, { forwardRef } from 'react';
import styled from 'styled-components';
interface TextInputProps extends MantineTextInputProps {
interface FileInputProps extends MantineFileInputProps {
children?: React.ReactNode;
maxWidth?: number | string;
width?: number | string;
}
interface JsonInputProps extends MantineJsonInputProps {
children?: React.ReactNode;
maxWidth?: number | string;
width?: number | string;
@ -35,24 +42,18 @@ interface PasswordInputProps extends MantinePasswordInputProps {
width?: number | string;
}
interface FileInputProps extends MantineFileInputProps {
children?: React.ReactNode;
maxWidth?: number | string;
width?: number | string;
}
interface JsonInputProps extends MantineJsonInputProps {
children?: React.ReactNode;
maxWidth?: number | string;
width?: number | string;
}
interface TextareaProps extends MantineTextareaProps {
children?: React.ReactNode;
maxWidth?: number | string;
width?: number | string;
}
interface TextInputProps extends MantineTextInputProps {
children?: React.ReactNode;
maxWidth?: number | string;
width?: number | string;
}
const StyledTextInput = styled(MantineTextInput)<TextInputProps>`
& .mantine-TextInput-wrapper {
border-color: var(--primary-color);
@ -276,7 +277,7 @@ const StyledTextarea = styled(MantineTextarea)<TextareaProps>`
`;
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ children, width, maxWidth, ...props }: TextInputProps, ref) => {
({ children, maxWidth, width, ...props }: TextInputProps, ref) => {
return (
<StyledTextInput
ref={ref}
@ -291,11 +292,11 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
);
export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
({ children, width, maxWidth, ...props }: NumberInputProps, ref) => {
({ children, maxWidth, width, ...props }: NumberInputProps, ref) => {
return (
<StyledNumberInput
ref={ref}
hideControls
ref={ref}
spellCheck={false}
{...props}
sx={{ maxWidth, width }}
@ -307,7 +308,7 @@ export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
);
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
({ children, width, maxWidth, ...props }: PasswordInputProps, ref) => {
({ children, maxWidth, width, ...props }: PasswordInputProps, ref) => {
return (
<StyledPasswordInput
ref={ref}
@ -321,7 +322,7 @@ export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
);
export const FileInput = forwardRef<HTMLButtonElement, FileInputProps>(
({ children, width, maxWidth, ...props }: FileInputProps, ref) => {
({ children, maxWidth, width, ...props }: FileInputProps, ref) => {
return (
<StyledFileInput
ref={ref}
@ -340,7 +341,7 @@ export const FileInput = forwardRef<HTMLButtonElement, FileInputProps>(
);
export const JsonInput = forwardRef<HTMLTextAreaElement, JsonInputProps>(
({ children, width, maxWidth, ...props }: JsonInputProps, ref) => {
({ children, maxWidth, width, ...props }: JsonInputProps, ref) => {
return (
<StyledJsonInput
ref={ref}
@ -354,7 +355,7 @@ export const JsonInput = forwardRef<HTMLTextAreaElement, JsonInputProps>(
);
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
({ children, width, maxWidth, ...props }: TextareaProps, ref) => {
({ children, maxWidth, width, ...props }: TextareaProps, ref) => {
return (
<StyledTextarea
ref={ref}

View file

@ -1,12 +1,13 @@
import React, { ReactNode } from 'react';
import {
ModalProps as MantineModalProps,
Stack,
Modal as MantineModal,
Flex,
Group,
Modal as MantineModal,
ModalProps as MantineModalProps,
Stack,
} from '@mantine/core';
import { closeAllModals, ContextModalProps } from '@mantine/modals';
import React, { ReactNode } from 'react';
import { Button } from '/@/renderer/components/button';
export interface ModalProps extends Omit<MantineModalProps, 'onClose'> {
@ -59,12 +60,12 @@ interface ConfirmModalProps {
}
export const ConfirmModal = ({
loading,
children,
disabled,
labels,
loading,
onCancel,
onConfirm,
children,
}: ConfirmModalProps) => {
const handleCancel = () => {
if (onCancel) {
@ -80,16 +81,16 @@ export const ConfirmModal = ({
<Group position="right">
<Button
data-focus
variant="default"
onClick={handleCancel}
variant="default"
>
{labels?.cancel ? labels.cancel : 'Cancel'}
</Button>
<Button
disabled={disabled}
loading={loading}
variant="filled"
onClick={onConfirm}
variant="filled"
>
{labels?.confirm ? labels.confirm : 'Confirm'}
</Button>

View file

@ -1,5 +1,5 @@
import { ReactNode } from 'react';
import { Flex, Group } from '@mantine/core';
import { ReactNode } from 'react';
export const Option = ({ children }: any) => {
return (

View file

@ -2,6 +2,7 @@ import { Flex, FlexProps } from '@mantine/core';
import { AnimatePresence, motion, Variants } from 'framer-motion';
import { ReactNode, useRef } from 'react';
import styled from 'styled-components';
import { useShouldPadTitlebar, useTheme } from '/@/renderer/hooks';
import { useWindowSettings } from '/@/renderer/store/settings.store';
import { Platform } from '/@/renderer/types';
@ -49,7 +50,7 @@ const BackgroundImage = styled.div<{ $background: string }>`
background: ${(props) => props.$background || 'var(--titlebar-bg)'};
`;
const BackgroundImageOverlay = styled.div<{ theme: 'light' | 'dark' }>`
const BackgroundImageOverlay = styled.div<{ theme: 'dark' | 'light' }>`
position: absolute;
top: 0;
left: 0;
@ -63,7 +64,7 @@ const BackgroundImageOverlay = styled.div<{ theme: 'light' | 'dark' }>`
`;
export interface PageHeaderProps
extends Omit<FlexProps, 'onAnimationStart' | 'onDragStart' | 'onDragEnd' | 'onDrag'> {
extends Omit<FlexProps, 'onAnimationStart' | 'onDrag' | 'onDragEnd' | 'onDragStart'> {
animated?: boolean;
backgroundColor?: string;
children?: ReactNode;
@ -93,11 +94,11 @@ const variants: Variants = {
export const PageHeader = ({
animated,
position,
height,
backgroundColor,
isHidden,
children,
height,
isHidden,
position,
...props
}: PageHeaderProps) => {
const ref = useRef(null);
@ -108,9 +109,9 @@ export const PageHeader = ({
return (
<>
<Container
ref={ref}
$height={height}
$position={position}
ref={ref}
{...props}
>
<Header
@ -132,7 +133,7 @@ export const PageHeader = ({
{backgroundColor && (
<>
<BackgroundImage $background={backgroundColor || 'var(--titlebar-bg)'} />
<BackgroundImageOverlay theme={theme as 'light' | 'dark'} />
<BackgroundImageOverlay theme={theme as 'dark' | 'light'} />
</>
)}
</Container>

View file

@ -1,6 +1,7 @@
import { ReactNode } from 'react';
import type { PaperProps as MantinePaperProps } from '@mantine/core';
import { Paper as MantinePaper } from '@mantine/core';
import { ReactNode } from 'react';
import styled from 'styled-components';
export interface PaperProps extends MantinePaperProps {

View file

@ -1,12 +1,13 @@
import type {
PopoverProps as MantinePopoverProps,
PopoverDropdownProps as MantinePopoverDropdownProps,
PopoverProps as MantinePopoverProps,
} from '@mantine/core';
import { Popover as MantinePopover } from '@mantine/core';
import styled from 'styled-components';
type PopoverProps = MantinePopoverProps;
type PopoverDropdownProps = MantinePopoverDropdownProps;
type PopoverProps = MantinePopoverProps;
const StyledPopover = styled(MantinePopover)``;
@ -21,13 +22,13 @@ const StyledDropdown = styled(MantinePopover.Dropdown)<PopoverDropdownProps>`
export const Popover = ({ children, ...props }: PopoverProps) => {
return (
<StyledPopover
withinPortal
styles={{
dropdown: {
filter: 'drop-shadow(0 0 5px rgb(0, 0, 0, 50%))',
},
}}
transitionProps={{ transition: 'fade' }}
withinPortal
{...props}
>
{children}

View file

@ -1,12 +1,13 @@
import { Group, Stack } from '@mantine/core';
import { Select } from '/@/renderer/components/select';
import { AnimatePresence, motion } from 'framer-motion';
import { RiAddFill, RiAddLine, RiDeleteBinFill, RiMore2Line, RiRestartLine } from 'react-icons/ri';
import i18n from '/@/i18n/i18n';
import { Button } from '/@/renderer/components/button';
import { DropdownMenu } from '/@/renderer/components/dropdown-menu';
import { QueryBuilderOption } from '/@/renderer/components/query-builder/query-builder-option';
import { Select } from '/@/renderer/components/select';
import { QueryBuilderGroup, QueryBuilderRule } from '/@/renderer/types';
import i18n from '/@/i18n/i18n';
const FILTER_GROUP_OPTIONS_DATA = [
{
@ -63,22 +64,22 @@ interface QueryBuilderProps {
export const QueryBuilder = ({
data,
filters,
groupIndex,
level,
onAddRule,
onDeleteRuleGroup,
onDeleteRule,
onAddRuleGroup,
onChangeType,
onChangeField,
operators,
onChangeOperator,
onChangeType,
onChangeValue,
onClearFilters,
onDeleteRule,
onDeleteRuleGroup,
onResetFilters,
operators,
playlists,
groupIndex,
uniqueId,
filters,
}: QueryBuilderProps) => {
const handleAddRule = () => {
onAddRule({ groupIndex, level });
@ -92,7 +93,7 @@ export const QueryBuilder = ({
onDeleteRuleGroup({ groupIndex, level, uniqueId });
};
const handleChangeType = (value: string | null) => {
const handleChangeType = (value: null | string) => {
onChangeType({ groupIndex, level, value });
};
@ -105,17 +106,17 @@ export const QueryBuilder = ({
<Select
data={FILTER_GROUP_OPTIONS_DATA}
maxWidth={175}
onChange={handleChangeType}
size="sm"
value={data.type}
width="20%"
onChange={handleChangeType}
/>
<Button
onClick={handleAddRule}
px={5}
size="sm"
tooltip={{ label: 'Add rule' }}
variant="default"
onClick={handleAddRule}
>
<RiAddLine size={20} />
</Button>
@ -170,10 +171,10 @@ export const QueryBuilder = ({
<AnimatePresence initial={false}>
{data?.rules?.map((rule: QueryBuilderRule) => (
<motion.div
key={rule.uniqueId}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -25 }}
initial={{ opacity: 0, x: -25 }}
key={rule.uniqueId}
transition={{ duration: 0.2, ease: 'easeInOut' }}
>
<QueryBuilderOption
@ -182,12 +183,12 @@ export const QueryBuilder = ({
groupIndex={groupIndex || []}
level={level}
noRemove={data?.rules?.length === 1}
operators={operators}
selectData={playlists}
onChangeField={onChangeField}
onChangeOperator={onChangeOperator}
onChangeValue={onChangeValue}
onDeleteRule={onDeleteRule}
operators={operators}
selectData={playlists}
/>
</motion.div>
))}
@ -196,10 +197,10 @@ export const QueryBuilder = ({
<AnimatePresence initial={false}>
{data.group?.map((group: QueryBuilderGroup, index: number) => (
<motion.div
key={group.uniqueId}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -25 }}
initial={{ opacity: 0, x: -25 }}
key={group.uniqueId}
transition={{ duration: 0.2, ease: 'easeInOut' }}
>
<QueryBuilder
@ -207,9 +208,6 @@ export const QueryBuilder = ({
filters={filters}
groupIndex={[...(groupIndex || []), index]}
level={level + 1}
operators={operators}
playlists={playlists}
uniqueId={group.uniqueId}
onAddRule={onAddRule}
onAddRuleGroup={onAddRuleGroup}
onChangeField={onChangeField}
@ -220,6 +218,9 @@ export const QueryBuilder = ({
onDeleteRule={onDeleteRule}
onDeleteRuleGroup={onDeleteRuleGroup}
onResetFilters={onResetFilters}
operators={operators}
playlists={playlists}
uniqueId={group.uniqueId}
/>
</motion.div>
))}

View file

@ -1,6 +1,7 @@
import { Group } from '@mantine/core';
import { useState } from 'react';
import { RiSubtractLine } from 'react-icons/ri';
import { Button } from '/@/renderer/components/button';
import { NumberInput, TextInput } from '/@/renderer/components/input';
import { Select } from '/@/renderer/components/select';
@ -31,62 +32,10 @@ interface QueryOptionProps {
selectData?: { label: string; value: string }[];
}
const QueryValueInput = ({ onChange, type, data, ...props }: any) => {
const QueryValueInput = ({ data, onChange, type, ...props }: any) => {
const [numberRange, setNumberRange] = useState([0, 0]);
switch (type) {
case 'string':
return (
<TextInput
size="sm"
onChange={onChange}
{...props}
/>
);
case 'number':
return (
<NumberInput
size="sm"
onChange={onChange}
{...props}
defaultValue={props.defaultValue && Number(props.defaultValue)}
/>
);
case 'date':
return (
<TextInput
size="sm"
onChange={onChange}
{...props}
/>
);
case 'dateRange':
return (
<>
<NumberInput
{...props}
defaultValue={props.defaultValue && Number(props.defaultValue?.[0])}
maxWidth={81}
width="10%"
onChange={(e) => {
const newRange = [e || 0, numberRange[1]];
setNumberRange(newRange);
onChange(newRange);
}}
/>
<NumberInput
{...props}
defaultValue={props.defaultValue && Number(props.defaultValue?.[1])}
maxWidth={81}
width="10%"
onChange={(e) => {
const newRange = [numberRange[0], e || 0];
setNumberRange(newRange);
onChange(newRange);
}}
/>
</>
);
case 'boolean':
return (
<Select
@ -98,6 +47,50 @@ const QueryValueInput = ({ onChange, type, data, ...props }: any) => {
{...props}
/>
);
case 'date':
return (
<TextInput
onChange={onChange}
size="sm"
{...props}
/>
);
case 'dateRange':
return (
<>
<NumberInput
{...props}
defaultValue={props.defaultValue && Number(props.defaultValue?.[0])}
maxWidth={81}
onChange={(e) => {
const newRange = [e || 0, numberRange[1]];
setNumberRange(newRange);
onChange(newRange);
}}
width="10%"
/>
<NumberInput
{...props}
defaultValue={props.defaultValue && Number(props.defaultValue?.[1])}
maxWidth={81}
onChange={(e) => {
const newRange = [numberRange[0], e || 0];
setNumberRange(newRange);
onChange(newRange);
}}
width="10%"
/>
</>
);
case 'number':
return (
<NumberInput
onChange={onChange}
size="sm"
{...props}
defaultValue={props.defaultValue && Number(props.defaultValue)}
/>
);
case 'playlist':
return (
<Select
@ -106,6 +99,14 @@ const QueryValueInput = ({ onChange, type, data, ...props }: any) => {
{...props}
/>
);
case 'string':
return (
<TextInput
onChange={onChange}
size="sm"
{...props}
/>
);
default:
return <></>;
@ -115,14 +116,14 @@ const QueryValueInput = ({ onChange, type, data, ...props }: any) => {
export const QueryBuilderOption = ({
data,
filters,
level,
onDeleteRule,
operators,
groupIndex,
level,
noRemove,
onChangeField,
onChangeOperator,
onChangeValue,
onDeleteRule,
operators,
selectData,
}: QueryOptionProps) => {
const { field, operator, uniqueId, value } = data;
@ -192,51 +193,51 @@ export const QueryBuilderOption = ({
spacing="sm"
>
<Select
searchable
data={filters}
maxWidth={170}
onChange={handleChangeField}
searchable
size="sm"
value={field}
width="25%"
onChange={handleChangeField}
/>
<Select
searchable
data={operatorsByFieldType || []}
disabled={!field}
maxWidth={170}
onChange={handleChangeOperator}
searchable
size="sm"
value={operator}
width="25%"
onChange={handleChangeOperator}
/>
{field ? (
<QueryValueInput
data={selectData || []}
defaultValue={value}
maxWidth={170}
onChange={handleChangeValue}
size="sm"
type={operator === 'inTheRange' ? 'dateRange' : fieldType}
width="25%"
onChange={handleChangeValue}
/>
) : (
<TextInput
disabled
defaultValue={value}
disabled
maxWidth={170}
onChange={handleChangeValue}
size="sm"
width="25%"
onChange={handleChangeValue}
/>
)}
<Button
disabled={noRemove}
onClick={handleDeleteRule}
px={5}
size="sm"
tooltip={{ label: 'Remove rule' }}
variant="default"
onClick={handleDeleteRule}
>
<RiSubtractLine size={20} />
</Button>

View file

@ -1,7 +1,7 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
import { useCallback } from 'react';
import { Rating as MantineRating, RatingProps } from '@mantine/core';
import debounce from 'lodash/debounce';
import { useCallback } from 'react';
import styled from 'styled-components';
const StyledRating = styled(MantineRating)`

View file

@ -1,10 +1,13 @@
/* eslint-disable react/display-name */
import type { ScrollAreaProps as MantineScrollAreaProps } from '@mantine/core';
import { ScrollArea as MantineScrollArea } from '@mantine/core';
import { useMergedRef } from '@mantine/hooks';
import { useInView } from 'framer-motion';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import { CSSProperties, forwardRef, ReactNode, Ref, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import { PageHeader, PageHeaderProps } from '/@/renderer/components/page-header';
import { useWindowSettings } from '/@/renderer/store/settings.store';
import { Platform } from '/@/renderer/types';
@ -51,7 +54,7 @@ const StyledNativeScrollArea = styled.div<{
$scrollBarOffset?: string;
$windowBarStyle?: Platform;
}>`
height: 100%;
height: calc(100vh - 90px);
`;
export const ScrollArea = forwardRef(({ children, ...props }: ScrollAreaProps, ref: Ref<any>) => {
@ -80,10 +83,10 @@ export const NativeScrollArea = forwardRef(
(
{
children,
noHeader,
pageHeaderProps,
scrollBarOffset,
scrollHideDelay,
noHeader,
...props
}: NativeScrollAreaProps,
ref: Ref<HTMLDivElement>,
@ -156,9 +159,9 @@ export const NativeScrollArea = forwardRef(
/>
)}
<StyledNativeScrollArea
ref={mergedRef}
$scrollBarOffset={scrollBarOffset}
$windowBarStyle={windowBarStyle}
ref={mergedRef}
{...props}
>
{children}

View file

@ -1,10 +1,11 @@
import { ChangeEvent, KeyboardEvent } from 'react';
import { ActionIcon, TextInputProps } from '@mantine/core';
import { useFocusWithin, useHotkeys, useMergedRef } from '@mantine/hooks';
import { ChangeEvent, KeyboardEvent } from 'react';
import { RiCloseFill, RiSearchLine } from 'react-icons/ri';
import { shallow } from 'zustand/shallow';
import { TextInput } from '/@/renderer/components/input';
import { useSettingsStore } from '/@/renderer/store';
import { shallow } from 'zustand/shallow';
interface SearchInputProps extends TextInputProps {
initialWidth?: number;
@ -18,7 +19,7 @@ export const SearchInput = ({
openedWidth,
...props
}: SearchInputProps) => {
const { ref, focused } = useFocusWithin();
const { focused, ref } = useFocusWithin();
const mergedRef = useMergedRef<HTMLInputElement>(ref);
const binding = useSettingsStore((state) => state.hotkeys.bindings.localSearch, shallow);
@ -40,6 +41,8 @@ export const SearchInput = ({
ref={mergedRef}
{...props}
icon={showIcon && <RiSearchLine />}
onChange={onChange}
onKeyDown={handleEscape}
rightSection={
isOpened ? (
<ActionIcon
@ -64,8 +67,6 @@ export const SearchInput = ({
},
}}
width={isOpened ? openedWidth || 150 : initialWidth || 35}
onChange={onChange}
onKeyDown={handleEscape}
/>
);
};

View file

@ -1,6 +1,7 @@
import { forwardRef } from 'react';
import type { SegmentedControlProps as MantineSegmentedControlProps } from '@mantine/core';
import { SegmentedControl as MantineSegmentedControl } from '@mantine/core';
import { forwardRef } from 'react';
import styled from 'styled-components';
type SegmentedControlProps = MantineSegmentedControlProps;

View file

@ -1,6 +1,7 @@
import { MultiSelect, MultiSelectProps, Select, SelectProps } from '/@/renderer/components/select';
import { useTranslation } from 'react-i18next';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { MultiSelect, MultiSelectProps, Select, SelectProps } from '/@/renderer/components/select';
export const SelectWithInvalidData = ({ data, defaultValue, ...props }: SelectProps) => {
const { t } = useTranslation();

View file

@ -1,16 +1,17 @@
import type {
SelectProps as MantineSelectProps,
MultiSelectProps as MantineMultiSelectProps,
SelectProps as MantineSelectProps,
} from '@mantine/core';
import { Select as MantineSelect, MultiSelect as MantineMultiSelect } from '@mantine/core';
import { MultiSelect as MantineMultiSelect, Select as MantineSelect } from '@mantine/core';
import styled from 'styled-components';
export interface SelectProps extends MantineSelectProps {
export interface MultiSelectProps extends MantineMultiSelectProps {
maxWidth?: number | string;
width?: number | string;
}
export interface MultiSelectProps extends MantineMultiSelectProps {
export interface SelectProps extends MantineSelectProps {
maxWidth?: number | string;
width?: number | string;
}
@ -37,10 +38,9 @@ const StyledSelect = styled(MantineSelect)`
}
`;
export const Select = ({ width, maxWidth, ...props }: SelectProps) => {
export const Select = ({ maxWidth, width, ...props }: SelectProps) => {
return (
<StyledSelect
withinPortal
styles={{
dropdown: {
background: 'var(--dropdown-menu-bg)',
@ -70,6 +70,7 @@ export const Select = ({ width, maxWidth, ...props }: SelectProps) => {
}}
sx={{ maxWidth, width }}
transitionProps={{ duration: 100, transition: 'fade' }}
withinPortal
{...props}
/>
);
@ -92,10 +93,9 @@ const StyledMultiSelect = styled(MantineMultiSelect)`
}
`;
export const MultiSelect = ({ width, maxWidth, ...props }: MultiSelectProps) => {
export const MultiSelect = ({ maxWidth, width, ...props }: MultiSelectProps) => {
return (
<StyledMultiSelect
withinPortal
styles={{
dropdown: {
background: 'var(--dropdown-menu-bg)',
@ -131,6 +131,7 @@ export const MultiSelect = ({ width, maxWidth, ...props }: MultiSelectProps) =>
}}
sx={{ maxWidth, width }}
transitionProps={{ duration: 100, transition: 'fade' }}
withinPortal
{...props}
/>
);

View file

@ -1,4 +1,5 @@
import type { SkeletonProps as MantineSkeletonProps } from '@mantine/core';
import { Skeleton as MantineSkeleton } from '@mantine/core';
import styled from 'styled-components';

View file

@ -1,4 +1,5 @@
import type { SliderProps as MantineSliderProps } from '@mantine/core';
import { Slider as MantineSlider } from '@mantine/core';
import styled from 'styled-components';

View file

@ -1,7 +1,9 @@
import { Center } from '@mantine/core';
import type { IconType } from 'react-icons';
import { Center } from '@mantine/core';
import { RiLoader5Fill } from 'react-icons/ri';
import styled from 'styled-components';
import { rotating } from '/@/renderer/styles';
interface SpinnerProps extends IconType {

View file

@ -1,6 +1,8 @@
import clsx from 'clsx';
import { HTMLAttributes, ReactNode, useRef, useState } from 'react';
import styles from './spoiler.module.scss';
import { useIsOverflow } from '/@/renderer/hooks';
interface SpoilerProps extends HTMLAttributes<HTMLDivElement> {
@ -9,7 +11,7 @@ interface SpoilerProps extends HTMLAttributes<HTMLDivElement> {
maxHeight?: number;
}
export const Spoiler = ({ maxHeight, defaultOpened, children, ...props }: SpoilerProps) => {
export const Spoiler = ({ children, defaultOpened, maxHeight, ...props }: SpoilerProps) => {
const ref = useRef(null);
const isOverflow = useIsOverflow(ref);
const [isExpanded, setIsExpanded] = useState(!!defaultOpened);
@ -25,12 +27,12 @@ export const Spoiler = ({ maxHeight, defaultOpened, children, ...props }: Spoile
return (
<div
ref={ref}
className={spoilerClassNames}
onClick={handleToggleExpand}
ref={ref}
role="button"
style={{ maxHeight: maxHeight ?? '100px', whiteSpace: 'pre-wrap' }}
tabIndex={-1}
onClick={handleToggleExpand}
{...props}
>
{children}

View file

@ -1,4 +1,5 @@
import type { SwitchProps as MantineSwitchProps } from '@mantine/core';
import { Switch as MantineSwitch } from '@mantine/core';
import styled from 'styled-components';

View file

@ -1,5 +1,5 @@
import { Tabs as MantineTabs, TabsProps as MantineTabsProps, TabsPanelProps } from '@mantine/core';
import { Suspense } from 'react';
import { TabsPanelProps, TabsProps as MantineTabsProps, Tabs as MantineTabs } from '@mantine/core';
import styled from 'styled-components';
type TabsProps = MantineTabsProps;

View file

@ -1,10 +1,12 @@
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
import type { TitleProps as MantineTitleProps } from '@mantine/core';
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
import { createPolymorphicComponent, Title as MantineHeader } from '@mantine/core';
import styled from 'styled-components';
import { textEllipsis } from '/@/renderer/styles';
type MantineTextTitleDivProps = MantineTitleProps & ComponentPropsWithoutRef<'div'>;
type MantineTextTitleDivProps = ComponentPropsWithoutRef<'div'> & MantineTitleProps;
interface TextTitleProps extends MantineTextTitleDivProps {
$link?: boolean;
@ -30,7 +32,7 @@ const StyledTextTitle = styled(MantineHeader)<TextTitleProps>`
}
`;
const _TextTitle = ({ children, $secondary, overflow, $noSelect, ...rest }: TextTitleProps) => {
const _TextTitle = ({ $noSelect, $secondary, children, overflow, ...rest }: TextTitleProps) => {
return (
<StyledTextTitle
$noSelect={$noSelect}

View file

@ -1,11 +1,13 @@
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
import type { Font } from '/@/renderer/styles';
import type { TextProps as MantineTextProps } from '@mantine/core';
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
import { createPolymorphicComponent, Text as MantineText } from '@mantine/core';
import styled from 'styled-components';
import type { Font } from '/@/renderer/styles';
import { textEllipsis } from '/@/renderer/styles';
type MantineTextDivProps = MantineTextProps & ComponentPropsWithoutRef<'div'>;
type MantineTextDivProps = ComponentPropsWithoutRef<'div'> & MantineTextProps;
interface TextProps extends MantineTextDivProps {
$link?: boolean;
@ -32,7 +34,7 @@ const StyledText = styled(MantineText)<TextProps>`
}
`;
export const _Text = ({ children, $secondary, overflow, font, $noSelect, ...rest }: TextProps) => {
export const _Text = ({ $noSelect, $secondary, children, font, overflow, ...rest }: TextProps) => {
return (
<StyledText
$noSelect={$noSelect}

View file

@ -1,14 +1,15 @@
import type { NotificationProps as MantineNotificationProps } from '@mantine/notifications';
import {
showNotification,
updateNotification,
hideNotification,
cleanNotifications,
cleanNotificationsQueue,
hideNotification,
showNotification,
updateNotification,
} from '@mantine/notifications';
interface NotificationProps extends MantineNotificationProps {
type?: 'success' | 'error' | 'warning' | 'info';
type?: 'error' | 'info' | 'success' | 'warning';
}
const showToast = ({ type, ...props }: NotificationProps) => {

View file

@ -1,4 +1,5 @@
import type { TooltipProps } from '@mantine/core';
import { Tooltip as MantineTooltip } from '@mantine/core';
import styled from 'styled-components';
@ -12,7 +13,6 @@ export const Tooltip = ({ children, ...rest }: TooltipProps) => {
return (
<StyledTooltip
multiline
withinPortal
pl={10}
pr={10}
py={5}
@ -31,6 +31,7 @@ export const Tooltip = ({ children, ...rest }: TooltipProps) => {
duration: 250,
transition: 'fade',
}}
withinPortal
{...rest}
>
{children}

View file

@ -1,14 +1,15 @@
import { Center, Stack } from '@mantine/core';
import { RiAlbumFill, RiUserVoiceFill, RiPlayListFill } from 'react-icons/ri';
import { RiAlbumFill, RiPlayListFill, RiUserVoiceFill } from 'react-icons/ri';
import { generatePath, useNavigate } from 'react-router-dom';
import { SimpleImg } from 'react-simple-img';
import { ListChildComponentProps } from 'react-window';
import styled from 'styled-components';
import { Album, AlbumArtist, Artist, LibraryItem, Playlist, Song } from '/@/renderer/api/types';
import { CardRows } from '/@/renderer/components/card';
import { Skeleton } from '/@/renderer/components/skeleton';
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types';
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/renderer/types';
interface BaseGridCardProps {
columnIndex: number;
@ -131,11 +132,11 @@ const DetailContainer = styled.div`
`;
export const DefaultCard = ({
listChildProps,
data,
columnIndex,
controls,
data,
isHidden,
listChildProps,
}: BaseGridCardProps) => {
const navigate = useNavigate();
@ -156,10 +157,10 @@ export const DefaultCard = ({
case LibraryItem.ALBUM:
Placeholder = RiAlbumFill;
break;
case LibraryItem.ARTIST:
case LibraryItem.ALBUM_ARTIST:
Placeholder = RiUserVoiceFill;
break;
case LibraryItem.ALBUM_ARTIST:
case LibraryItem.ARTIST:
Placeholder = RiUserVoiceFill;
break;
case LibraryItem.PLAYLIST:
@ -172,8 +173,8 @@ export const DefaultCard = ({
return (
<DefaultCardContainer
key={`card-${columnIndex}-${listChildProps.index}`}
$itemGap={controls.itemGap}
key={`card-${columnIndex}-${listChildProps.index}`}
onClick={() => navigate(path)}
>
<InnerCardContainer>
@ -221,25 +222,25 @@ export const DefaultCard = ({
return (
<DefaultCardContainer
key={`card-${columnIndex}-${listChildProps.index}`}
$isHidden={isHidden}
$itemGap={controls.itemGap}
key={`card-${columnIndex}-${listChildProps.index}`}
>
<InnerCardContainer>
<ImageContainer>
<Skeleton
visible
radius="sm"
visible
/>
</ImageContainer>
<DetailContainer>
<Stack spacing="sm">
{(controls?.cardRows || []).map((row, index) => (
<Skeleton
key={`${index}-${columnIndex}-${row.arrayProperty}`}
visible
height={14}
key={`${index}-${columnIndex}-${row.arrayProperty}`}
radius="sm"
visible
/>
))}
</Stack>

View file

@ -1,20 +1,23 @@
import React, { MouseEvent, useState } from 'react';
import type { UnstyledButtonProps } from '@mantine/core';
import { RiPlayFill, RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
import styled from 'styled-components';
import { _Button } from '/@/renderer/components/button';
import type { PlayQueueAddOptions } from '/@/renderer/types';
import { Play } from '/@/renderer/types';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { LibraryItem } from '/@/renderer/api/types';
import { useHandleGridContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
import type { UnstyledButtonProps } from '@mantine/core';
import React, { MouseEvent, useState } from 'react';
import { RiHeartFill, RiHeartLine, RiMoreFill, RiPlayFill } from 'react-icons/ri';
import styled from 'styled-components';
import {
PLAYLIST_CONTEXT_MENU_ITEMS,
ALBUM_CONTEXT_MENU_ITEMS,
ARTIST_CONTEXT_MENU_ITEMS,
PLAYLIST_CONTEXT_MENU_ITEMS,
} from '../../../features/context-menu/context-menu-items';
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
import { LibraryItem } from '/@/renderer/api/types';
import { _Button } from '/@/renderer/components/button';
import { useHandleGridContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { Play } from '/@/renderer/types';
type PlayButtonType = React.ComponentPropsWithoutRef<'button'> & UnstyledButtonProps;
const PlayButton = styled.button<PlayButtonType>`
position: absolute;
@ -108,10 +111,10 @@ const FavoriteWrapper = styled.span<{ isFavorite: boolean }>`
`;
export const GridCardControls = ({
handleFavorite,
handlePlayQueueAdd,
itemData,
itemType,
handlePlayQueueAdd,
handleFavorite,
resetInfiniteLoaderCache,
}: {
handleFavorite: (options: {
@ -178,9 +181,9 @@ export const GridCardControls = ({
<BottomControls>
{itemType !== LibraryItem.PLAYLIST && (
<SecondaryButton
onClick={(e) => handleFavorites(e, itemData?.serverId)}
p={5}
variant="subtle"
onClick={(e) => handleFavorites(e, itemData?.serverId)}
>
<FavoriteWrapper isFavorite={itemData?.isFavorite}>
{isFavorite ? (
@ -196,13 +199,13 @@ export const GridCardControls = ({
)}
<SecondaryButton
p={5}
variant="subtle"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleContextMenu(e, [itemData]);
}}
p={5}
variant="subtle"
>
<RiMoreFill
color="white"

View file

@ -1,24 +1,26 @@
import { memo } from 'react';
import type { ListChildComponentProps } from 'react-window';
import { memo } from 'react';
import { areEqual } from 'react-window';
import { DefaultCard } from '/@/renderer/components/virtual-grid/grid-card/default-card';
import { PosterCard } from '/@/renderer/components/virtual-grid/grid-card/poster-card';
import { GridCardData, ListDisplayType } from '/@/renderer/types';
export const GridCard = memo(({ data, index, style }: ListChildComponentProps) => {
const {
columnCount,
itemCount,
cardRows,
itemData,
itemType,
itemGap,
playButtonBehavior,
handlePlayQueueAdd,
handleFavorite,
route,
columnCount,
display,
handleFavorite,
handlePlayQueueAdd,
itemCount,
itemData,
itemGap,
itemType,
playButtonBehavior,
resetInfiniteLoaderCache,
route,
} = data as GridCardData;
const cards = [];
@ -35,7 +37,6 @@ export const GridCard = memo(({ data, index, style }: ListChildComponentProps) =
for (let i = startIndex; i <= stopIndex + columnCountToAdd; i += 1) {
cards.push(
<View
key={`card-${i}-${index}`}
columnIndex={i}
controls={{
cardRows,
@ -49,6 +50,7 @@ export const GridCard = memo(({ data, index, style }: ListChildComponentProps) =
}}
data={itemData[i]}
isHidden={i > stopIndex}
key={`card-${i}-${index}`}
listChildProps={{ index }}
/>,
);

View file

@ -4,11 +4,12 @@ import { generatePath, useNavigate } from 'react-router-dom';
import { SimpleImg } from 'react-simple-img';
import { ListChildComponentProps } from 'react-window';
import styled from 'styled-components';
import { Album, AlbumArtist, Artist, LibraryItem, Playlist, Song } from '/@/renderer/api/types';
import { CardRows } from '/@/renderer/components/card';
import { Skeleton } from '/@/renderer/components/skeleton';
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
import { CardRow, PlayQueueAddOptions, Play, CardRoute } from '/@/renderer/types';
import { CardRoute, CardRow, Play, PlayQueueAddOptions } from '/@/renderer/types';
interface BaseGridCardProps {
columnIndex: number;
@ -119,11 +120,11 @@ const DetailContainer = styled.div`
`;
export const PosterCard = ({
listChildProps,
data,
columnIndex,
controls,
data,
isHidden,
listChildProps,
}: BaseGridCardProps) => {
const navigate = useNavigate();
@ -144,10 +145,10 @@ export const PosterCard = ({
case LibraryItem.ALBUM:
Placeholder = RiAlbumFill;
break;
case LibraryItem.ARTIST:
case LibraryItem.ALBUM_ARTIST:
Placeholder = RiUserVoiceFill;
break;
case LibraryItem.ALBUM_ARTIST:
case LibraryItem.ARTIST:
Placeholder = RiUserVoiceFill;
break;
case LibraryItem.PLAYLIST:
@ -160,8 +161,8 @@ export const PosterCard = ({
return (
<PosterCardContainer
key={`card-${columnIndex}-${listChildProps.index}`}
$itemGap={controls.itemGap}
key={`card-${columnIndex}-${listChildProps.index}`}
>
<LinkContainer onClick={() => navigate(path)}>
<ImageContainer $isFavorite={data?.userFavorite}>
@ -207,13 +208,13 @@ export const PosterCard = ({
return (
<PosterCardContainer
key={`card-${columnIndex}-${listChildProps.index}`}
$isHidden={isHidden}
$itemGap={controls.itemGap}
key={`card-${columnIndex}-${listChildProps.index}`}
>
<Skeleton
visible
radius="sm"
visible
>
<ImageContainer />
</Skeleton>
@ -221,10 +222,10 @@ export const PosterCard = ({
<Stack spacing="sm">
{(controls?.cardRows || []).map((row, index) => (
<Skeleton
key={`${index}-${columnIndex}-${row.arrayProperty}`}
visible
height={14}
key={`${index}-${columnIndex}-${row.arrayProperty}`}
radius="sm"
visible
/>
))}
</Stack>

View file

@ -1,12 +1,14 @@
import type { CardRoute, CardRow, ListDisplayType, PlayQueueAddOptions } from '/@/renderer/types';
import type { Ref } from 'react';
import type { FixedSizeListProps } from 'react-window';
import debounce from 'lodash/debounce';
import memoize from 'memoize-one';
import type { FixedSizeListProps } from 'react-window';
import { FixedSizeList } from 'react-window';
import styled from 'styled-components';
import { GridCard } from '/@/renderer/components/virtual-grid/grid-card';
import type { CardRow, ListDisplayType, CardRoute, PlayQueueAddOptions } from '/@/renderer/types';
import { Album, AlbumArtist, Artist, LibraryItem } from '/@/renderer/api/types';
import { GridCard } from '/@/renderer/components/virtual-grid/grid-card';
const createItemData = memoize(
(
@ -43,27 +45,27 @@ const createItemData = memoize(
const createScrollHandler = memoize((onScroll) => debounce(onScroll, 250));
export const VirtualGridWrapper = ({
refInstance,
cardRows,
itemGap,
itemType,
itemWidth,
display,
itemHeight,
itemCount,
columnCount,
rowCount,
initialScrollOffset,
display,
handleFavorite,
handlePlayQueueAdd,
itemData,
route,
onScroll,
height,
width,
initialScrollOffset,
itemCount,
itemData,
itemGap,
itemHeight,
itemType,
itemWidth,
onScroll,
refInstance,
resetInfiniteLoaderCache,
route,
rowCount,
width,
...rest
}: Omit<FixedSizeListProps, 'ref' | 'itemSize' | 'children' | 'height' | 'width'> & {
}: Omit<FixedSizeListProps, 'children' | 'height' | 'itemSize' | 'ref' | 'width'> & {
cardRows: CardRow<Album | AlbumArtist | Artist>[];
columnCount: number;
display: ListDisplayType;
@ -112,9 +114,9 @@ export const VirtualGridWrapper = ({
itemCount={rowCount}
itemData={memoizedItemData}
itemSize={itemHeight}
onScroll={memoizedOnScroll}
overscanCount={5}
width={(width && Number(width)) || 0}
onScroll={memoizedOnScroll}
>
{GridCard}
</FixedSizeList>

View file

@ -1,21 +1,21 @@
import type { CardRoute, CardRow, PlayQueueAddOptions } from '/@/renderer/types';
import type { FixedSizeListProps } from 'react-window';
import debounce from 'lodash/debounce';
import {
useState,
useRef,
useMemo,
useCallback,
forwardRef,
Ref,
useCallback,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import debounce from 'lodash/debounce';
import type { FixedSizeListProps } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { VirtualGridWrapper } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import type { CardRoute, CardRow, PlayQueueAddOptions } from '/@/renderer/types';
import { ListDisplayType } from '/@/renderer/types';
import { AnyLibraryItem, Genre, LibraryItem } from '/@/renderer/api/types';
type LibraryItemOrGenre = AnyLibraryItem | Genre;
import { AnyLibraryItem, Genre, LibraryItem } from '/@/renderer/api/types';
import { VirtualGridWrapper } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { ListDisplayType } from '/@/renderer/types';
export type VirtualInfiniteGridRef = {
resetLoadMoreItemsCache: () => void;
@ -24,8 +24,10 @@ export type VirtualInfiniteGridRef = {
updateItemData: (rule: (item: LibraryItemOrGenre) => LibraryItemOrGenre) => void;
};
type LibraryItemOrGenre = AnyLibraryItem | Genre;
interface VirtualGridProps
extends Omit<FixedSizeListProps, 'children' | 'itemSize' | 'height' | 'width'> {
extends Omit<FixedSizeListProps, 'children' | 'height' | 'itemSize' | 'width'> {
cardRows: CardRow<any>[];
display?: ListDisplayType;
fetchFn: (options: { columnCount: number; skip: number; take: number }) => Promise<any>;
@ -49,22 +51,22 @@ interface VirtualGridProps
export const VirtualInfiniteGrid = forwardRef(
(
{
cardRows,
display,
fetchFn,
fetchInitialData,
handleFavorite,
handlePlayQueueAdd,
height,
initialScrollOffset,
itemCount,
itemGap,
itemSize,
itemType,
cardRows,
route,
onScroll,
display,
handlePlayQueueAdd,
minimumBatchSize,
fetchFn,
fetchInitialData,
loading,
initialScrollOffset,
handleFavorite,
height,
minimumBatchSize,
onScroll,
route,
width,
}: VirtualGridProps,
ref: Ref<VirtualInfiniteGridRef>,
@ -78,7 +80,7 @@ export const VirtualInfiniteGrid = forwardRef(
fetchInitialData?.() || [],
);
const { itemHeight, rowCount, columnCount } = useMemo(() => {
const { columnCount, itemHeight, rowCount } = useMemo(() => {
const itemsPerRow = width ? Math.floor(width / (itemSize + itemGap * 2)) : 5;
const widthPerItem = Number(width) / itemsPerRow;
const itemHeight = widthPerItem + cardRows.length * 26;
@ -165,11 +167,11 @@ export const VirtualInfiniteGrid = forwardRef(
return (
<>
<InfiniteLoader
ref={loader}
isItemLoaded={(index) => isItemLoaded(index)}
itemCount={itemCount || 0}
loadMoreItems={debouncedLoadMoreItems}
minimumBatchSize={minimumBatchSize}
ref={loader}
threshold={30}
>
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
@ -187,6 +189,8 @@ export const VirtualInfiniteGrid = forwardRef(
itemHeight={itemHeight}
itemType={itemType}
itemWidth={itemSize}
onItemsRendered={onItemsRendered}
onScroll={onScroll}
refInstance={(list) => {
infiniteLoaderRef(list);
listRef.current = list;
@ -200,8 +204,6 @@ export const VirtualInfiniteGrid = forwardRef(
route={route}
rowCount={rowCount}
width={width}
onItemsRendered={onItemsRendered}
onScroll={onScroll}
/>
)}
</InfiniteLoader>

View file

@ -1,19 +1,21 @@
import type { ICellRendererParams } from '@ag-grid-community/core';
import { RiMoreFill } from 'react-icons/ri';
import { Button } from '/@/renderer/components/button';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
export const ActionsCell = ({ context, api }: ICellRendererParams) => {
export const ActionsCell = ({ api, context }: ICellRendererParams) => {
return (
<CellContainer $position="center">
<Button
compact
variant="subtle"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
context.onCellContextMenu(undefined, api, e);
}}
variant="subtle"
>
<RiMoreFill />
</Button>

View file

@ -1,15 +1,17 @@
import React from 'react';
import type { AlbumArtist, Artist } from '/@/renderer/api/types';
import type { ICellRendererParams } from '@ag-grid-community/core';
import React from 'react';
import { generatePath } from 'react-router';
import { Link } from 'react-router-dom';
import type { AlbumArtist, Artist } from '/@/renderer/api/types';
import { Separator } from '/@/renderer/components/separator';
import { Skeleton } from '/@/renderer/components/skeleton';
import { Text } from '/@/renderer/components/text';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
import { AppRoute } from '/@/renderer/router/routes';
import { Skeleton } from '/@/renderer/components/skeleton';
import { Separator } from '/@/renderer/components/separator';
export const AlbumArtistCell = ({ value, data }: ICellRendererParams) => {
export const AlbumArtistCell = ({ data, value }: ICellRendererParams) => {
if (value === undefined) {
return (
<CellContainer $position="left">
@ -28,7 +30,7 @@ export const AlbumArtistCell = ({ value, data }: ICellRendererParams) => {
overflow="hidden"
size="md"
>
{value?.map((item: Artist | AlbumArtist, index: number) => (
{value?.map((item: AlbumArtist | Artist, index: number) => (
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
{index > 0 && <Separator />}
{item.id ? (

View file

@ -1,15 +1,17 @@
import React from 'react';
import type { AlbumArtist, Artist } from '/@/renderer/api/types';
import type { ICellRendererParams } from '@ag-grid-community/core';
import React from 'react';
import { generatePath } from 'react-router';
import { Link } from 'react-router-dom';
import type { AlbumArtist, Artist } from '/@/renderer/api/types';
import { Separator } from '/@/renderer/components/separator';
import { Skeleton } from '/@/renderer/components/skeleton';
import { Text } from '/@/renderer/components/text';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
import { AppRoute } from '/@/renderer/router/routes';
import { Skeleton } from '/@/renderer/components/skeleton';
import { Separator } from '/@/renderer/components/separator';
export const ArtistCell = ({ value, data }: ICellRendererParams) => {
export const ArtistCell = ({ data, value }: ICellRendererParams) => {
if (value === undefined) {
return (
<CellContainer $position="left">
@ -28,7 +30,7 @@ export const ArtistCell = ({ value, data }: ICellRendererParams) => {
overflow="hidden"
size="md"
>
{value?.map((item: Artist | AlbumArtist, index: number) => (
{value?.map((item: AlbumArtist | Artist, index: number) => (
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
{index > 0 && <Separator />}
{item.id ? (

View file

@ -1,13 +1,15 @@
import React, { MouseEvent } from 'react';
import type { UnstyledButtonProps } from '@mantine/core';
import React, { MouseEvent } from 'react';
import { RiPlayFill } from 'react-icons/ri';
import styled from 'styled-components';
import { Play } from '/@/renderer/types';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { LibraryItem } from '/@/renderer/api/types';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { Play } from '/@/renderer/types';
type PlayButtonType = UnstyledButtonProps & React.ComponentPropsWithoutRef<'button'>;
type PlayButtonType = React.ComponentPropsWithoutRef<'button'> & UnstyledButtonProps;
const PlayButton = styled.button<PlayButtonType>`
position: absolute;
@ -50,9 +52,9 @@ const ListConverControlsContainer = styled.div`
`;
export const ListCoverControls = ({
context,
itemData,
itemType,
context,
uniqueId,
}: {
context: Record<string, any>;

View file

@ -1,18 +1,20 @@
import React, { useMemo } from 'react';
import type { ICellRendererParams } from '@ag-grid-community/core';
import { Center } from '@mantine/core';
import { motion } from 'framer-motion';
import React, { useMemo } from 'react';
import { RiAlbumFill } from 'react-icons/ri';
import { generatePath } from 'react-router';
import { Link } from 'react-router-dom';
import { SimpleImg } from 'react-simple-img';
import styled from 'styled-components';
import { AlbumArtist, Artist } from '/@/renderer/api/types';
import { Text } from '/@/renderer/components/text';
import { AppRoute } from '/@/renderer/router/routes';
import { Skeleton } from '/@/renderer/components/skeleton';
import { SEPARATOR_STRING } from '/@/renderer/api/utils';
import { Skeleton } from '/@/renderer/components/skeleton';
import { Text } from '/@/renderer/components/text';
import { ListCoverControls } from '/@/renderer/components/virtual-table/cells/combined-title-cell-controls';
import { AppRoute } from '/@/renderer/router/routes';
const CellContainer = styled(motion.div)<{ height: number }>`
display: grid;
@ -61,11 +63,11 @@ const StyledImage = styled(SimpleImg)`
`;
export const CombinedTitleCell = ({
value,
rowIndex,
node,
context,
data,
node,
rowIndex,
value,
}: ICellRendererParams) => {
const artists = useMemo(() => {
if (!value) return null;
@ -141,7 +143,7 @@ export const CombinedTitleCell = ({
size="md"
>
{artists?.length ? (
artists.map((artist: Artist | AlbumArtist, index: number) => (
artists.map((artist: AlbumArtist | Artist, index: number) => (
<React.Fragment key={`queue-${rowIndex}-artist-${artist.id}`}>
{index > 0 ? SEPARATOR_STRING : null}
{artist.id ? (

View file

@ -1,11 +1,13 @@
/* eslint-disable import/no-cycle */
import type { ICellRendererParams } from '@ag-grid-community/core';
import { RiHeartFill, RiHeartLine } from 'react-icons/ri';
import { Button } from '/@/renderer/components/button';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
export const FavoriteCell = ({ value, data, node }: ICellRendererParams) => {
export const FavoriteCell = ({ data, node, value }: ICellRendererParams) => {
const createMutation = useCreateFavorite({});
const deleteMutation = useDeleteFavorite({});
@ -49,6 +51,7 @@ export const FavoriteCell = ({ value, data, node }: ICellRendererParams) => {
<CellContainer $position="center">
<Button
compact
onClick={handleToggleFavorite}
sx={{
svg: {
fill: !value
@ -57,7 +60,6 @@ export const FavoriteCell = ({ value, data, node }: ICellRendererParams) => {
},
}}
variant="subtle"
onClick={handleToggleFavorite}
>
{!value ? <RiHeartLine size="1.3em" /> : <RiHeartFill size="1.3em" />}
</Button>

View file

@ -1,11 +1,13 @@
import { useState } from 'react';
import { ICellRendererParams } from '@ag-grid-community/core';
import { Group } from '@mantine/core';
import { useState } from 'react';
import { RiCheckboxBlankLine, RiCheckboxLine } from 'react-icons/ri';
import styled from 'styled-components';
import { getNodesByDiscNumber, setNodeSelection } from '../utils';
import { Button } from '/@/renderer/components/button';
import { Paper } from '/@/renderer/components/paper';
import { getNodesByDiscNumber, setNodeSelection } from '../utils';
const Container = styled(Paper)`
display: flex;
@ -14,7 +16,7 @@ const Container = styled(Paper)`
border: 1px solid transparent;
`;
export const FullWidthDiscCell = ({ node, data, api }: ICellRendererParams) => {
export const FullWidthDiscCell = ({ api, data, node }: ICellRendererParams) => {
const [isSelected, setIsSelected] = useState(false);
const handleToggleDiscNodes = () => {
@ -38,9 +40,9 @@ export const FullWidthDiscCell = ({ node, data, api }: ICellRendererParams) => {
<Button
compact
leftIcon={isSelected ? <RiCheckboxLine /> : <RiCheckboxBlankLine />}
onClick={handleToggleDiscNodes}
size="md"
variant="subtle"
onClick={handleToggleDiscNodes}
>
{data.name}
</Button>

View file

@ -1,10 +1,12 @@
import type { ICellRendererParams } from '@ag-grid-community/core';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { Skeleton } from '/@/renderer/components/skeleton';
import { Text } from '/@/renderer/components/text';
export const CellContainer = styled.div<{ $position?: 'left' | 'center' | 'right' }>`
export const CellContainer = styled.div<{ $position?: 'center' | 'left' | 'right' }>`
display: flex;
align-items: center;
justify-content: ${(props) =>
@ -22,13 +24,13 @@ type Options = {
array?: boolean;
isArray?: boolean;
isLink?: boolean;
position?: 'left' | 'center' | 'right';
position?: 'center' | 'left' | 'right';
primary?: boolean;
};
export const GenericCell = (
{ value, valueFormatted }: ICellRendererParams,
{ position, primary, isLink }: Options,
{ isLink, position, primary }: Options,
) => {
const displayedValue = valueFormatted || value;

View file

@ -1,13 +1,15 @@
import React from 'react';
import type { ICellRendererParams } from '@ag-grid-community/core';
import { generatePath, Link } from 'react-router-dom';
import type { AlbumArtist, Artist } from '/@/renderer/api/types';
import type { ICellRendererParams } from '@ag-grid-community/core';
import React from 'react';
import { generatePath, Link } from 'react-router-dom';
import { Separator } from '/@/renderer/components/separator';
import { Text } from '/@/renderer/components/text';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
import { Separator } from '/@/renderer/components/separator';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
export const GenreCell = ({ value, data }: ICellRendererParams) => {
export const GenreCell = ({ data, value }: ICellRendererParams) => {
const genrePath = useGenreRoute();
return (
<CellContainer $position="left">
@ -16,7 +18,7 @@ export const GenreCell = ({ value, data }: ICellRendererParams) => {
overflow="hidden"
size="md"
>
{value?.map((item: Artist | AlbumArtist, index: number) => (
{value?.map((item: AlbumArtist | Artist, index: number) => (
<React.Fragment key={`row-${item.id}-${data.uniqueId}`}>
{index > 0 && <Separator />}
<Text

View file

@ -1,8 +1,10 @@
import type { ICellRendererParams } from '@ag-grid-community/core';
import { Skeleton } from '/@/renderer/components/skeleton';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
import { useMemo } from 'react';
import { Skeleton } from '/@/renderer/components/skeleton';
import { Text } from '/@/renderer/components/text';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
export const NoteCell = ({ value }: ICellRendererParams) => {

View file

@ -1,10 +1,11 @@
/* eslint-disable import/no-cycle */
import type { ICellRendererParams } from '@ag-grid-community/core';
import { Rating } from '/@/renderer/components/rating';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
import { useSetRating } from '/@/renderer/features/shared';
export const RatingCell = ({ value, node }: ICellRendererParams) => {
export const RatingCell = ({ node, value }: ICellRendererParams) => {
const updateRatingMutation = useSetRating({});
const handleUpdateRating = (rating: number) => {
@ -27,9 +28,9 @@ export const RatingCell = ({ value, node }: ICellRendererParams) => {
return (
<CellContainer $position="center">
<Rating
onChange={handleUpdateRating}
size="xs"
value={value?.userRating}
onChange={handleUpdateRating}
/>
</CellContainer>
);

View file

@ -1,4 +1,5 @@
import type { ICellRendererParams } from '@ag-grid-community/core';
import { Text } from '/@/renderer/components/text';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
@ -132,7 +133,7 @@ const StaticSvg = () => {
);
};
export const RowIndexCell = ({ value, eGridCell }: ICellRendererParams) => {
export const RowIndexCell = ({ eGridCell, value }: ICellRendererParams) => {
const classList = eGridCell.classList;
// const isFocused = classList.contains('focused');
const isPlaying = classList.contains('playing');

View file

@ -1,4 +1,5 @@
import type { ICellRendererParams } from '@ag-grid-community/core';
import { Skeleton } from '/@/renderer/components/skeleton';
import { Text } from '/@/renderer/components/text';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';

View file

@ -1,4 +1,5 @@
import type { IHeaderParams } from '@ag-grid-community/core';
import { FiClock } from 'react-icons/fi';
export interface ICustomHeaderParams extends IHeaderParams {

View file

@ -1,19 +1,21 @@
import type { ReactNode } from 'react';
import type { IHeaderParams } from '@ag-grid-community/core';
import type { ReactNode } from 'react';
import { AiOutlineNumber } from 'react-icons/ai';
import { FiClock } from 'react-icons/fi';
import { RiHeartLine, RiMoreFill, RiStarLine } from 'react-icons/ri';
import styled from 'styled-components';
import { _Text } from '/@/renderer/components/text';
type Presets = 'duration' | 'rowIndex' | 'userFavorite' | 'userRating' | 'actions';
import { _Text } from '/@/renderer/components/text';
type Options = {
children?: ReactNode;
position?: 'left' | 'center' | 'right';
position?: 'center' | 'left' | 'right';
preset?: Presets;
};
type Presets = 'actions' | 'duration' | 'rowIndex' | 'userFavorite' | 'userRating';
export const HeaderWrapper = styled.div<{ $position: Options['position'] }>`
display: flex;
justify-content: ${(props) =>
@ -77,7 +79,7 @@ const headerPresets = {
export const GenericTableHeader = (
{ displayName }: IHeaderParams,
{ preset, children, position }: Options,
{ children, position, preset }: Options,
) => {
if (preset) {
return <HeaderWrapper $position={position}>{headerPresets[preset]}</HeaderWrapper>;

View file

@ -1,6 +1,7 @@
import { MutableRefObject } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useClickOutside } from '@mantine/hooks';
import { MutableRefObject } from 'react';
export const useClickOutsideDeselect = (tableRef: MutableRefObject<AgGridReactType | null>) => {
const handleDeselect = () => {

View file

@ -1,6 +1,8 @@
import { MutableRefObject, useEffect, useMemo, useRef } from 'react';
import { RowClassRules, RowNode } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { RowClassRules, RowNode } from '@ag-grid-community/core';
import { MutableRefObject, useEffect, useMemo, useRef } from 'react';
import { Song } from '/@/renderer/api/types';
import { useAppFocus } from '/@/renderer/hooks';
import { useCurrentSong, usePlayerStore } from '/@/renderer/store';

View file

@ -1,5 +1,6 @@
import { useInView } from 'framer-motion';
import { useEffect, useRef } from 'react';
import { useWindowSettings } from '/@/renderer/store/settings.store';
import { Platform } from '/@/renderer/types';

Some files were not shown because too many files have changed in this diff Show more