Lint all files
This commit is contained in:
parent
22af76b4d6
commit
30e52ebb54
334 changed files with 76519 additions and 75932 deletions
6
.github/stale.yml
vendored
6
.github/stale.yml
vendored
|
@ -10,8 +10,8 @@ exemptLabels:
|
||||||
staleLabel: wontfix
|
staleLabel: wontfix
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
markComment: >
|
markComment: >
|
||||||
This issue has been automatically marked as stale because it has not had
|
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
|
||||||
recent activity. It will be closed if no further activity occurs. Thank you
|
|
||||||
for your contributions.
|
|
||||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
closeComment: false
|
closeComment: false
|
||||||
|
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
npm run package
|
|
||||||
npm run lint
|
npm run lint
|
||||||
|
npm run package
|
||||||
npm exec tsc
|
npm exec tsc
|
||||||
npm test
|
npm test
|
||||||
|
|
|
@ -21,10 +21,7 @@
|
||||||
"declaration-block-no-redundant-longhand-properties": null,
|
"declaration-block-no-redundant-longhand-properties": null,
|
||||||
"selector-class-pattern": null,
|
"selector-class-pattern": null,
|
||||||
"selector-type-case": ["lower", { "ignoreTypes": ["/^\\$\\w+/"] }],
|
"selector-type-case": ["lower", { "ignoreTypes": ["/^\\$\\w+/"] }],
|
||||||
"selector-type-no-unknown": [
|
"selector-type-no-unknown": [true, { "ignoreTypes": ["/-styled-mixin/", "/^\\$\\w+/"] }],
|
||||||
true,
|
|
||||||
{ "ignoreTypes": ["/-styled-mixin/", "/^\\$\\w+/"] }
|
|
||||||
],
|
|
||||||
"value-keyword-case": ["lower", { "ignoreKeywords": ["dummyValue"] }],
|
"value-keyword-case": ["lower", { "ignoreKeywords": ["dummyValue"] }],
|
||||||
"declaration-colon-newline-after": null
|
"declaration-colon-newline-after": null
|
||||||
}
|
}
|
||||||
|
|
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
|
@ -7,9 +7,7 @@
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"protocol": "inspector",
|
"protocol": "inspector",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": ["run start:main --inspect=5858 --remote-debugging-port=9223"],
|
||||||
"run start:main --inspect=5858 --remote-debugging-port=9223"
|
|
||||||
],
|
|
||||||
"preLaunchTask": "Start Webpack Dev"
|
"preLaunchTask": "Start Webpack Dev"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -155,7 +155,9 @@ ipcMain.on(
|
||||||
? song.artists?.map((artist: RelatedArtist) => artist.name)
|
? song.artists?.map((artist: RelatedArtist) => artist.name)
|
||||||
: null,
|
: null,
|
||||||
'xesam:discNumber': song.discNumber ? song.discNumber : null,
|
'xesam:discNumber': song.discNumber ? song.discNumber : null,
|
||||||
'xesam:genre': song.genres?.length ? song.genres.map((genre: any) => genre.name) : null,
|
'xesam:genre': song.genres?.length
|
||||||
|
? song.genres.map((genre: any) => genre.name)
|
||||||
|
: null,
|
||||||
'xesam:title': song.name || null,
|
'xesam:title': song.name || null,
|
||||||
'xesam:trackNumber': song.trackNumber ? song.trackNumber : null,
|
'xesam:trackNumber': song.trackNumber ? song.trackNumber : null,
|
||||||
'xesam:useCount':
|
'xesam:useCount':
|
||||||
|
|
|
@ -85,7 +85,7 @@ const singleInstance = app.requestSingleInstanceLock();
|
||||||
if (!singleInstance) {
|
if (!singleInstance) {
|
||||||
app.quit();
|
app.quit();
|
||||||
} else {
|
} else {
|
||||||
app.on('second-instance', (_event, _argv, _workingDirectory) => {
|
app.on('second-instance', () => {
|
||||||
mainWindow?.show();
|
mainWindow?.show();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -579,7 +579,8 @@ const HOTKEY_ACTIONS: Record<BindingActions, () => void> = {
|
||||||
[BindingActions.STOP]: () => getMainWindow()?.webContents.send('renderer-player-stop'),
|
[BindingActions.STOP]: () => getMainWindow()?.webContents.send('renderer-player-stop'),
|
||||||
[BindingActions.TOGGLE_REPEAT]: () =>
|
[BindingActions.TOGGLE_REPEAT]: () =>
|
||||||
getMainWindow()?.webContents.send('renderer-player-toggle-repeat'),
|
getMainWindow()?.webContents.send('renderer-player-toggle-repeat'),
|
||||||
[BindingActions.VOLUME_UP]: () => getMainWindow()?.webContents.send('renderer-player-volume-up'),
|
[BindingActions.VOLUME_UP]: () =>
|
||||||
|
getMainWindow()?.webContents.send('renderer-player-volume-up'),
|
||||||
[BindingActions.VOLUME_DOWN]: () =>
|
[BindingActions.VOLUME_DOWN]: () =>
|
||||||
getMainWindow()?.webContents.send('renderer-player-volume-down'),
|
getMainWindow()?.webContents.send('renderer-player-volume-down'),
|
||||||
[BindingActions.GLOBAL_SEARCH]: () => {},
|
[BindingActions.GLOBAL_SEARCH]: () => {},
|
||||||
|
@ -600,10 +601,13 @@ ipcMain.on(
|
||||||
for (const shortcut of Object.keys(data)) {
|
for (const shortcut of Object.keys(data)) {
|
||||||
const isGlobalHotkey = data[shortcut as BindingActions].isGlobal;
|
const isGlobalHotkey = data[shortcut as BindingActions].isGlobal;
|
||||||
const isValidHotkey =
|
const isValidHotkey =
|
||||||
data[shortcut as BindingActions].hotkey && data[shortcut as BindingActions].hotkey !== '';
|
data[shortcut as BindingActions].hotkey &&
|
||||||
|
data[shortcut as BindingActions].hotkey !== '';
|
||||||
|
|
||||||
if (isGlobalHotkey && isValidHotkey) {
|
if (isGlobalHotkey && isValidHotkey) {
|
||||||
const accelerator = hotkeyToElectronAccelerator(data[shortcut as BindingActions].hotkey);
|
const accelerator = hotkeyToElectronAccelerator(
|
||||||
|
data[shortcut as BindingActions].hotkey,
|
||||||
|
);
|
||||||
|
|
||||||
globalShortcut.register(accelerator, () => {
|
globalShortcut.register(accelerator, () => {
|
||||||
HOTKEY_ACTIONS[shortcut as BindingActions]();
|
HOTKEY_ACTIONS[shortcut as BindingActions]();
|
||||||
|
@ -636,8 +640,7 @@ app.on('window-all-closed', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app
|
app.whenReady()
|
||||||
.whenReady()
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
createWindow();
|
createWindow();
|
||||||
createTray();
|
createTray();
|
||||||
|
|
|
@ -18,7 +18,9 @@ export default class MenuBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
const template =
|
const template =
|
||||||
process.platform === 'darwin' ? this.buildDarwinTemplate() : this.buildDefaultTemplate();
|
process.platform === 'darwin'
|
||||||
|
? this.buildDarwinTemplate()
|
||||||
|
: this.buildDefaultTemplate();
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate(template);
|
const menu = Menu.buildFromTemplate(template);
|
||||||
Menu.setApplicationMenu(menu);
|
Menu.setApplicationMenu(menu);
|
||||||
|
@ -151,7 +153,9 @@ export default class MenuBuilder {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
click() {
|
click() {
|
||||||
shell.openExternal('https://github.com/electron/electron/tree/main/docs#readme');
|
shell.openExternal(
|
||||||
|
'https://github.com/electron/electron/tree/main/docs#readme',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
label: 'Documentation',
|
label: 'Documentation',
|
||||||
},
|
},
|
||||||
|
@ -211,7 +215,9 @@ export default class MenuBuilder {
|
||||||
{
|
{
|
||||||
accelerator: 'F11',
|
accelerator: 'F11',
|
||||||
click: () => {
|
click: () => {
|
||||||
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
|
this.mainWindow.setFullScreen(
|
||||||
|
!this.mainWindow.isFullScreen(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
label: 'Toggle &Full Screen',
|
label: 'Toggle &Full Screen',
|
||||||
},
|
},
|
||||||
|
@ -227,7 +233,9 @@ export default class MenuBuilder {
|
||||||
{
|
{
|
||||||
accelerator: 'F11',
|
accelerator: 'F11',
|
||||||
click: () => {
|
click: () => {
|
||||||
this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
|
this.mainWindow.setFullScreen(
|
||||||
|
!this.mainWindow.isFullScreen(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
label: 'Toggle &Full Screen',
|
label: 'Toggle &Full Screen',
|
||||||
},
|
},
|
||||||
|
@ -244,7 +252,9 @@ export default class MenuBuilder {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
click() {
|
click() {
|
||||||
shell.openExternal('https://github.com/electron/electron/tree/main/docs#readme');
|
shell.openExternal(
|
||||||
|
'https://github.com/electron/electron/tree/main/docs#readme',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
label: 'Documentation',
|
label: 'Documentation',
|
||||||
},
|
},
|
||||||
|
|
|
@ -420,7 +420,10 @@ const deleteFavorite = async (args: FavoriteArgs) => {
|
||||||
|
|
||||||
const updateRating = async (args: SetRatingArgs) => {
|
const updateRating = async (args: SetRatingArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController('setRating', args.apiClientProps.server?.type) as ControllerEndpoint['setRating']
|
apiController(
|
||||||
|
'setRating',
|
||||||
|
args.apiClientProps.server?.type,
|
||||||
|
) as ControllerEndpoint['setRating']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -435,7 +438,10 @@ const getTopSongList = async (args: TopSongListArgs) => {
|
||||||
|
|
||||||
const scrobble = async (args: ScrobbleArgs) => {
|
const scrobble = async (args: ScrobbleArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController('scrobble', args.apiClientProps.server?.type) as ControllerEndpoint['scrobble']
|
apiController(
|
||||||
|
'scrobble',
|
||||||
|
args.apiClientProps.server?.type,
|
||||||
|
) as ControllerEndpoint['scrobble']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -456,7 +462,10 @@ const getRandomSongList = async (args: RandomSongListArgs) => {
|
||||||
|
|
||||||
const getLyrics = async (args: LyricsArgs) => {
|
const getLyrics = async (args: LyricsArgs) => {
|
||||||
return (
|
return (
|
||||||
apiController('getLyrics', args.apiClientProps.server?.type) as ControllerEndpoint['getLyrics']
|
apiController(
|
||||||
|
'getLyrics',
|
||||||
|
args.apiClientProps.server?.type,
|
||||||
|
) as ControllerEndpoint['getLyrics']
|
||||||
)?.(args);
|
)?.(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -284,7 +284,9 @@ const getAlbumList = async (args: AlbumListArgs): Promise<AlbumListResponse> =>
|
||||||
userId: apiClientProps.server?.userId,
|
userId: apiClientProps.server?.userId,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
AlbumArtistIds: query.artistIds ? formatCommaDelimitedString(query.artistIds) : undefined,
|
AlbumArtistIds: query.artistIds
|
||||||
|
? formatCommaDelimitedString(query.artistIds)
|
||||||
|
: undefined,
|
||||||
IncludeItemTypes: 'MusicAlbum',
|
IncludeItemTypes: 'MusicAlbum',
|
||||||
Limit: query.limit,
|
Limit: query.limit,
|
||||||
ParentId: query.musicFolderId,
|
ParentId: query.musicFolderId,
|
||||||
|
@ -363,7 +365,9 @@ const getSongList = async (args: SongListArgs): Promise<SongListResponse> => {
|
||||||
|
|
||||||
const yearsFilter = yearsGroup.length ? formatCommaDelimitedString(yearsGroup) : undefined;
|
const yearsFilter = yearsGroup.length ? formatCommaDelimitedString(yearsGroup) : undefined;
|
||||||
const albumIdsFilter = query.albumIds ? formatCommaDelimitedString(query.albumIds) : undefined;
|
const albumIdsFilter = query.albumIds ? formatCommaDelimitedString(query.albumIds) : undefined;
|
||||||
const artistIdsFilter = query.artistIds ? formatCommaDelimitedString(query.artistIds) : undefined;
|
const artistIdsFilter = query.artistIds
|
||||||
|
? formatCommaDelimitedString(query.artistIds)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).getSongList({
|
const res = await jfApiClient(apiClientProps).getSongList({
|
||||||
params: {
|
params: {
|
||||||
|
@ -798,7 +802,9 @@ const search = async (args: SearchArgs): Promise<SearchResponse> => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumArtists: albumArtists.map((item) => jfNormalize.albumArtist(item, apiClientProps.server)),
|
albumArtists: albumArtists.map((item) =>
|
||||||
|
jfNormalize.albumArtist(item, apiClientProps.server),
|
||||||
|
),
|
||||||
albums: albums.map((item) => jfNormalize.album(item, apiClientProps.server)),
|
albums: albums.map((item) => jfNormalize.album(item, apiClientProps.server)),
|
||||||
songs: songs.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
|
songs: songs.map((item) => jfNormalize.song(item, apiClientProps.server, '')),
|
||||||
};
|
};
|
||||||
|
|
|
@ -194,7 +194,11 @@ const normalizeAlbum = (
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
name: entry.Name,
|
name: entry.Name,
|
||||||
})) || [],
|
})) || [],
|
||||||
artists: item.ArtistItems?.map((entry) => ({ id: entry.Id, imageUrl: null, name: entry.Name })),
|
artists: item.ArtistItems?.map((entry) => ({
|
||||||
|
id: entry.Id,
|
||||||
|
imageUrl: null,
|
||||||
|
name: entry.Name,
|
||||||
|
})),
|
||||||
backdropImageUrl: null,
|
backdropImageUrl: null,
|
||||||
createdAt: item.DateCreated,
|
createdAt: item.DateCreated,
|
||||||
duration: item.RunTimeTicks / 10000,
|
duration: item.RunTimeTicks / 10000,
|
||||||
|
|
|
@ -181,7 +181,8 @@ const parsePath = (fullPath: string) => {
|
||||||
const newParams: Record<string, any> = {};
|
const newParams: Record<string, any> = {};
|
||||||
Object.keys(parsedParams).forEach((key) => {
|
Object.keys(parsedParams).forEach((key) => {
|
||||||
const isIndexedArrayObject =
|
const isIndexedArrayObject =
|
||||||
typeof parsedParams[key] === 'object' && Object.keys(parsedParams[key] || {}).includes('0');
|
typeof parsedParams[key] === 'object' &&
|
||||||
|
Object.keys(parsedParams[key] || {}).includes('0');
|
||||||
|
|
||||||
if (!isIndexedArrayObject) {
|
if (!isIndexedArrayObject) {
|
||||||
newParams[key] = parsedParams[key];
|
newParams[key] = parsedParams[key];
|
||||||
|
@ -280,7 +281,9 @@ axiosClient.interceptors.response.use(
|
||||||
});
|
});
|
||||||
|
|
||||||
const serverId = currentServer.id;
|
const serverId = currentServer.id;
|
||||||
useAuthStore.getState().actions.updateServer(serverId, { ndCredential: undefined });
|
useAuthStore
|
||||||
|
.getState()
|
||||||
|
.actions.updateServer(serverId, { ndCredential: undefined });
|
||||||
useAuthStore.getState().actions.setCurrentServer(null);
|
useAuthStore.getState().actions.setCurrentServer(null);
|
||||||
|
|
||||||
// special error to prevent sending a second message, and stop other messages that could be enqueued
|
// special error to prevent sending a second message, and stop other messages that could be enqueued
|
||||||
|
|
|
@ -394,7 +394,9 @@ const getPlaylistSongList = async (
|
||||||
query: {
|
query: {
|
||||||
_end: query.startIndex + (query.limit || 0),
|
_end: query.startIndex + (query.limit || 0),
|
||||||
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : 'ASC',
|
_order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : 'ASC',
|
||||||
_sort: query.sortBy ? songListSortMap.navidrome[query.sortBy] : ndType._enum.songList.ID,
|
_sort: query.sortBy
|
||||||
|
? songListSortMap.navidrome[query.sortBy]
|
||||||
|
: ndType._enum.songList.ID,
|
||||||
_start: query.startIndex,
|
_start: query.startIndex,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -161,7 +161,9 @@ export const ssApiClient = (args: {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await axiosClient.request<z.infer<typeof ssType._response.baseResponse>>({
|
const result = await axiosClient.request<
|
||||||
|
z.infer<typeof ssType._response.baseResponse>
|
||||||
|
>({
|
||||||
data: body,
|
data: body,
|
||||||
headers,
|
headers,
|
||||||
method: method as Method,
|
method: method as Method,
|
||||||
|
|
|
@ -266,8 +266,9 @@ const getTopSongList = async (args: TopSongListArgs): Promise<SongListResponse>
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items:
|
items:
|
||||||
res.body.topSongs?.song?.map((song) => ssNormalize.song(song, apiClientProps.server, '')) ||
|
res.body.topSongs?.song?.map((song) =>
|
||||||
[],
|
ssNormalize.song(song, apiClientProps.server, ''),
|
||||||
|
) || [],
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
totalRecordCount: res.body.topSongs?.song?.length || 0,
|
||||||
};
|
};
|
||||||
|
|
|
@ -145,7 +145,9 @@ const normalizeAlbum = (
|
||||||
}) || null;
|
}) || null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumArtists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [],
|
albumArtists: item.artistId
|
||||||
|
? [{ id: item.artistId, imageUrl: null, name: item.artist }]
|
||||||
|
: [],
|
||||||
artists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [],
|
artists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [],
|
||||||
backdropImageUrl: null,
|
backdropImageUrl: null,
|
||||||
createdAt: item.created,
|
createdAt: item.created,
|
||||||
|
|
|
@ -171,7 +171,9 @@ export const AudioPlayer = forwardRef(
|
||||||
volume={volume}
|
volume={volume}
|
||||||
width={0}
|
width={0}
|
||||||
onEnded={handleOnEnded}
|
onEnded={handleOnEnded}
|
||||||
onProgress={playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1}
|
onProgress={
|
||||||
|
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ReactPlayer
|
<ReactPlayer
|
||||||
ref={player2Ref}
|
ref={player2Ref}
|
||||||
|
@ -183,7 +185,9 @@ export const AudioPlayer = forwardRef(
|
||||||
volume={volume}
|
volume={volume}
|
||||||
width={0}
|
width={0}
|
||||||
onEnded={handleOnEnded}
|
onEnded={handleOnEnded}
|
||||||
onProgress={playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2}
|
onProgress={
|
||||||
|
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -101,9 +101,11 @@ export const crossfadeHandler = (args: {
|
||||||
|
|
||||||
percentageOfFadeLeft = timeLeft / fadeDuration;
|
percentageOfFadeLeft = timeLeft / fadeDuration;
|
||||||
currentPlayerVolumeCalculation =
|
currentPlayerVolumeCalculation =
|
||||||
Math.cos((Math.PI / 4) * ((2 * percentageOfFadeLeft - 1) ** (2 * n + 1) - 1)) * volume;
|
Math.cos((Math.PI / 4) * ((2 * percentageOfFadeLeft - 1) ** (2 * n + 1) - 1)) *
|
||||||
|
volume;
|
||||||
nextPlayerVolumeCalculation =
|
nextPlayerVolumeCalculation =
|
||||||
Math.cos((Math.PI / 4) * ((2 * percentageOfFadeLeft - 1) ** (2 * n + 1) + 1)) * volume;
|
Math.cos((Math.PI / 4) * ((2 * percentageOfFadeLeft - 1) ** (2 * n + 1) + 1)) *
|
||||||
|
volume;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -60,7 +60,10 @@ export const CardRows = ({ data, rows }: CardRowsProps) => {
|
||||||
row.route!.slugs?.reduce((acc, slug) => {
|
row.route!.slugs?.reduce((acc, slug) => {
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
[slug.slugProperty]: data[row.property][itemIndex][slug.idProperty],
|
[slug.slugProperty]:
|
||||||
|
data[row.property][itemIndex][
|
||||||
|
slug.idProperty
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}, {}),
|
}, {}),
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -138,7 +138,9 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: currentItem?.id || '' })}>
|
<Wrapper
|
||||||
|
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: currentItem?.id || '' })}
|
||||||
|
>
|
||||||
<AnimatePresence
|
<AnimatePresence
|
||||||
custom={direction}
|
custom={direction}
|
||||||
initial={false}
|
initial={false}
|
||||||
|
|
|
@ -118,7 +118,12 @@ export const SwiperGridCarousel = ({
|
||||||
const deleteFavoriteMutation = useDeleteFavorite({});
|
const deleteFavoriteMutation = useDeleteFavorite({});
|
||||||
|
|
||||||
const handleFavorite = useCallback(
|
const handleFavorite = useCallback(
|
||||||
(options: { id: string[]; isFavorite: boolean; itemType: LibraryItem; serverId: string }) => {
|
(options: {
|
||||||
|
id: string[];
|
||||||
|
isFavorite: boolean;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
serverId: string;
|
||||||
|
}) => {
|
||||||
const { id, itemType, isFavorite, serverId } = options;
|
const { id, itemType, isFavorite, serverId } = options;
|
||||||
if (isFavorite) {
|
if (isFavorite) {
|
||||||
deleteFavoriteMutation.mutate({
|
deleteFavoriteMutation.mutate({
|
||||||
|
|
|
@ -14,7 +14,11 @@ interface BaseGridCardProps {
|
||||||
columnIndex: number;
|
columnIndex: number;
|
||||||
controls: {
|
controls: {
|
||||||
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
||||||
handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void;
|
handleFavorite: (options: {
|
||||||
|
id: string[];
|
||||||
|
isFavorite: boolean;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
}) => void;
|
||||||
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
playButtonBehavior: Play;
|
playButtonBehavior: Play;
|
||||||
|
|
|
@ -14,7 +14,11 @@ interface BaseGridCardProps {
|
||||||
columnIndex: number;
|
columnIndex: number;
|
||||||
controls: {
|
controls: {
|
||||||
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
||||||
handleFavorite: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void;
|
handleFavorite: (options: {
|
||||||
|
id: string[];
|
||||||
|
isFavorite: boolean;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
}) => void;
|
||||||
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
handlePlayQueueAdd: (options: PlayQueueAddOptions) => void;
|
||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
playButtonBehavior: Play;
|
playButtonBehavior: Play;
|
||||||
|
|
|
@ -64,7 +64,11 @@ export const VirtualGridWrapper = ({
|
||||||
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
cardRows: CardRow<Album | AlbumArtist | Artist>[];
|
||||||
columnCount: number;
|
columnCount: number;
|
||||||
display: ListDisplayType;
|
display: ListDisplayType;
|
||||||
handleFavorite?: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void;
|
handleFavorite?: (options: {
|
||||||
|
id: string[];
|
||||||
|
isFavorite: boolean;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
}) => void;
|
||||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||||
height?: number;
|
height?: number;
|
||||||
itemData: any[];
|
itemData: any[];
|
||||||
|
|
|
@ -26,7 +26,11 @@ interface VirtualGridProps
|
||||||
cardRows: CardRow<any>[];
|
cardRows: CardRow<any>[];
|
||||||
display?: ListDisplayType;
|
display?: ListDisplayType;
|
||||||
fetchFn: (options: { columnCount: number; skip: number; take: number }) => Promise<any>;
|
fetchFn: (options: { columnCount: number; skip: number; take: number }) => Promise<any>;
|
||||||
handleFavorite?: (options: { id: string[]; isFavorite: boolean; itemType: LibraryItem }) => void;
|
handleFavorite?: (options: {
|
||||||
|
id: string[];
|
||||||
|
isFavorite: boolean;
|
||||||
|
itemType: LibraryItem;
|
||||||
|
}) => void;
|
||||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||||
height?: number;
|
height?: number;
|
||||||
itemGap: number;
|
itemGap: number;
|
||||||
|
|
|
@ -60,7 +60,8 @@ export const useUpdateRating = () => {
|
||||||
onSuccess: (_data, variables) => {
|
onSuccess: (_data, variables) => {
|
||||||
// We only need to set if we're already on the album detail page
|
// We only need to set if we're already on the album detail page
|
||||||
const isAlbumDetailPage =
|
const isAlbumDetailPage =
|
||||||
variables.query.item.length === 1 && variables.query.item[0].itemType === LibraryItem.ALBUM;
|
variables.query.item.length === 1 &&
|
||||||
|
variables.query.item[0].itemType === LibraryItem.ALBUM;
|
||||||
|
|
||||||
if (isAlbumDetailPage) {
|
if (isAlbumDetailPage) {
|
||||||
const { serverType, id: albumId, serverId } = variables.query.item[0] as Album;
|
const { serverType, id: albumId, serverId } = variables.query.item[0] as Album;
|
||||||
|
@ -94,7 +95,11 @@ export const useUpdateRating = () => {
|
||||||
variables.query.item[0].itemType === LibraryItem.ALBUM_ARTIST;
|
variables.query.item[0].itemType === LibraryItem.ALBUM_ARTIST;
|
||||||
|
|
||||||
if (isAlbumArtistDetailPage) {
|
if (isAlbumArtistDetailPage) {
|
||||||
const { serverType, id: albumArtistId, serverId } = variables.query.item[0] as AlbumArtist;
|
const {
|
||||||
|
serverType,
|
||||||
|
id: albumArtistId,
|
||||||
|
serverId,
|
||||||
|
} = variables.query.item[0] as AlbumArtist;
|
||||||
|
|
||||||
const queryKey = queryKeys.albumArtists.detail(serverId || '', {
|
const queryKey = queryKeys.albumArtists.detail(serverId || '', {
|
||||||
id: albumArtistId,
|
id: albumArtistId,
|
||||||
|
|
|
@ -77,10 +77,12 @@ const tableColumns: { [key: string]: ColDef } = {
|
||||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||||
colId: TableColumn.ALBUM_COUNT,
|
colId: TableColumn.ALBUM_COUNT,
|
||||||
field: 'albumCount',
|
field: 'albumCount',
|
||||||
headerComponent: (params: IHeaderParams) => GenericTableHeader(params, { position: 'center' }),
|
headerComponent: (params: IHeaderParams) =>
|
||||||
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
headerName: 'Albums',
|
headerName: 'Albums',
|
||||||
suppressSizeToFit: true,
|
suppressSizeToFit: true,
|
||||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.albumCount : undefined),
|
valueGetter: (params: ValueGetterParams) =>
|
||||||
|
params.data ? params.data.albumCount : undefined,
|
||||||
width: 80,
|
width: 80,
|
||||||
},
|
},
|
||||||
artist: {
|
artist: {
|
||||||
|
@ -102,7 +104,8 @@ const tableColumns: { [key: string]: ColDef } = {
|
||||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||||
colId: TableColumn.BIT_RATE,
|
colId: TableColumn.BIT_RATE,
|
||||||
field: 'bitRate',
|
field: 'bitRate',
|
||||||
headerComponent: (params: IHeaderParams) => GenericTableHeader(params, { position: 'center' }),
|
headerComponent: (params: IHeaderParams) =>
|
||||||
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
suppressSizeToFit: true,
|
suppressSizeToFit: true,
|
||||||
valueFormatter: (params: ValueFormatterParams) => `${params.value} kbps`,
|
valueFormatter: (params: ValueFormatterParams) => `${params.value} kbps`,
|
||||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.bitRate : undefined),
|
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.bitRate : undefined),
|
||||||
|
@ -111,7 +114,8 @@ const tableColumns: { [key: string]: ColDef } = {
|
||||||
bpm: {
|
bpm: {
|
||||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||||
colId: TableColumn.BPM,
|
colId: TableColumn.BPM,
|
||||||
headerComponent: (params: IHeaderParams) => GenericTableHeader(params, { position: 'center' }),
|
headerComponent: (params: IHeaderParams) =>
|
||||||
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
headerName: 'BPM',
|
headerName: 'BPM',
|
||||||
suppressSizeToFit: true,
|
suppressSizeToFit: true,
|
||||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.bpm : undefined),
|
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.bpm : undefined),
|
||||||
|
@ -121,8 +125,10 @@ const tableColumns: { [key: string]: ColDef } = {
|
||||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||||
colId: TableColumn.CHANNELS,
|
colId: TableColumn.CHANNELS,
|
||||||
field: 'channels',
|
field: 'channels',
|
||||||
headerComponent: (params: IHeaderParams) => GenericTableHeader(params, { position: 'center' }),
|
headerComponent: (params: IHeaderParams) =>
|
||||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.channels : undefined),
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
|
valueGetter: (params: ValueGetterParams) =>
|
||||||
|
params.data ? params.data.channels : undefined,
|
||||||
width: 100,
|
width: 100,
|
||||||
},
|
},
|
||||||
comment: {
|
comment: {
|
||||||
|
@ -136,22 +142,26 @@ const tableColumns: { [key: string]: ColDef } = {
|
||||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||||
colId: TableColumn.DATE_ADDED,
|
colId: TableColumn.DATE_ADDED,
|
||||||
field: 'createdAt',
|
field: 'createdAt',
|
||||||
headerComponent: (params: IHeaderParams) => GenericTableHeader(params, { position: 'center' }),
|
headerComponent: (params: IHeaderParams) =>
|
||||||
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
headerName: 'Date Added',
|
headerName: 'Date Added',
|
||||||
suppressSizeToFit: true,
|
suppressSizeToFit: true,
|
||||||
valueFormatter: (params: ValueFormatterParams) =>
|
valueFormatter: (params: ValueFormatterParams) =>
|
||||||
params.value ? dayjs(params.value).format('MMM D, YYYY') : '',
|
params.value ? dayjs(params.value).format('MMM D, YYYY') : '',
|
||||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.createdAt : undefined),
|
valueGetter: (params: ValueGetterParams) =>
|
||||||
|
params.data ? params.data.createdAt : undefined,
|
||||||
width: 130,
|
width: 130,
|
||||||
},
|
},
|
||||||
discNumber: {
|
discNumber: {
|
||||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||||
colId: TableColumn.DISC_NUMBER,
|
colId: TableColumn.DISC_NUMBER,
|
||||||
field: 'discNumber',
|
field: 'discNumber',
|
||||||
headerComponent: (params: IHeaderParams) => GenericTableHeader(params, { position: 'center' }),
|
headerComponent: (params: IHeaderParams) =>
|
||||||
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
headerName: 'Disc',
|
headerName: 'Disc',
|
||||||
suppressSizeToFit: true,
|
suppressSizeToFit: true,
|
||||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.discNumber : undefined),
|
valueGetter: (params: ValueGetterParams) =>
|
||||||
|
params.data ? params.data.discNumber : undefined,
|
||||||
width: 60,
|
width: 60,
|
||||||
},
|
},
|
||||||
duration: {
|
duration: {
|
||||||
|
@ -162,7 +172,8 @@ const tableColumns: { [key: string]: ColDef } = {
|
||||||
GenericTableHeader(params, { position: 'center', preset: 'duration' }),
|
GenericTableHeader(params, { position: 'center', preset: 'duration' }),
|
||||||
suppressSizeToFit: true,
|
suppressSizeToFit: true,
|
||||||
valueFormatter: (params: ValueFormatterParams) => formatDuration(params.value * 1000),
|
valueFormatter: (params: ValueFormatterParams) => formatDuration(params.value * 1000),
|
||||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.duration : undefined),
|
valueGetter: (params: ValueGetterParams) =>
|
||||||
|
params.data ? params.data.duration : undefined,
|
||||||
width: 70,
|
width: 70,
|
||||||
},
|
},
|
||||||
genre: {
|
genre: {
|
||||||
|
@ -175,7 +186,8 @@ const tableColumns: { [key: string]: ColDef } = {
|
||||||
lastPlayedAt: {
|
lastPlayedAt: {
|
||||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||||
colId: TableColumn.LAST_PLAYED,
|
colId: TableColumn.LAST_PLAYED,
|
||||||
headerComponent: (params: IHeaderParams) => GenericTableHeader(params, { position: 'center' }),
|
headerComponent: (params: IHeaderParams) =>
|
||||||
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
headerName: 'Last Played',
|
headerName: 'Last Played',
|
||||||
valueFormatter: (params: ValueFormatterParams) =>
|
valueFormatter: (params: ValueFormatterParams) =>
|
||||||
params.value ? dayjs(params.value).fromNow() : '',
|
params.value ? dayjs(params.value).fromNow() : '',
|
||||||
|
@ -194,32 +206,38 @@ const tableColumns: { [key: string]: ColDef } = {
|
||||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||||
colId: TableColumn.PLAY_COUNT,
|
colId: TableColumn.PLAY_COUNT,
|
||||||
field: 'playCount',
|
field: 'playCount',
|
||||||
headerComponent: (params: IHeaderParams) => GenericTableHeader(params, { position: 'center' }),
|
headerComponent: (params: IHeaderParams) =>
|
||||||
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
headerName: 'Plays',
|
headerName: 'Plays',
|
||||||
suppressSizeToFit: true,
|
suppressSizeToFit: true,
|
||||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.playCount : undefined),
|
valueGetter: (params: ValueGetterParams) =>
|
||||||
|
params.data ? params.data.playCount : undefined,
|
||||||
width: 90,
|
width: 90,
|
||||||
},
|
},
|
||||||
releaseDate: {
|
releaseDate: {
|
||||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||||
colId: TableColumn.RELEASE_DATE,
|
colId: TableColumn.RELEASE_DATE,
|
||||||
field: 'releaseDate',
|
field: 'releaseDate',
|
||||||
headerComponent: (params: IHeaderParams) => GenericTableHeader(params, { position: 'center' }),
|
headerComponent: (params: IHeaderParams) =>
|
||||||
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
headerName: 'Release Date',
|
headerName: 'Release Date',
|
||||||
suppressSizeToFit: true,
|
suppressSizeToFit: true,
|
||||||
valueFormatter: (params: ValueFormatterParams) =>
|
valueFormatter: (params: ValueFormatterParams) =>
|
||||||
params.value ? dayjs(params.value).format('MMM D, YYYY') : '',
|
params.value ? dayjs(params.value).format('MMM D, YYYY') : '',
|
||||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.releaseDate : undefined),
|
valueGetter: (params: ValueGetterParams) =>
|
||||||
|
params.data ? params.data.releaseDate : undefined,
|
||||||
width: 130,
|
width: 130,
|
||||||
},
|
},
|
||||||
releaseYear: {
|
releaseYear: {
|
||||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||||
colId: TableColumn.YEAR,
|
colId: TableColumn.YEAR,
|
||||||
field: 'releaseYear',
|
field: 'releaseYear',
|
||||||
headerComponent: (params: IHeaderParams) => GenericTableHeader(params, { position: 'center' }),
|
headerComponent: (params: IHeaderParams) =>
|
||||||
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
headerName: 'Year',
|
headerName: 'Year',
|
||||||
suppressSizeToFit: true,
|
suppressSizeToFit: true,
|
||||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.releaseYear : undefined),
|
valueGetter: (params: ValueGetterParams) =>
|
||||||
|
params.data ? params.data.releaseYear : undefined,
|
||||||
width: 80,
|
width: 80,
|
||||||
},
|
},
|
||||||
rowIndex: {
|
rowIndex: {
|
||||||
|
@ -237,10 +255,12 @@ const tableColumns: { [key: string]: ColDef } = {
|
||||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||||
colId: TableColumn.SONG_COUNT,
|
colId: TableColumn.SONG_COUNT,
|
||||||
field: 'songCount',
|
field: 'songCount',
|
||||||
headerComponent: (params: IHeaderParams) => GenericTableHeader(params, { position: 'center' }),
|
headerComponent: (params: IHeaderParams) =>
|
||||||
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
headerName: 'Songs',
|
headerName: 'Songs',
|
||||||
suppressSizeToFit: true,
|
suppressSizeToFit: true,
|
||||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.songCount : undefined),
|
valueGetter: (params: ValueGetterParams) =>
|
||||||
|
params.data ? params.data.songCount : undefined,
|
||||||
width: 80,
|
width: 80,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
|
@ -276,10 +296,12 @@ const tableColumns: { [key: string]: ColDef } = {
|
||||||
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
cellRenderer: (params: ICellRendererParams) => GenericCell(params, { position: 'center' }),
|
||||||
colId: TableColumn.TRACK_NUMBER,
|
colId: TableColumn.TRACK_NUMBER,
|
||||||
field: 'trackNumber',
|
field: 'trackNumber',
|
||||||
headerComponent: (params: IHeaderParams) => GenericTableHeader(params, { position: 'center' }),
|
headerComponent: (params: IHeaderParams) =>
|
||||||
|
GenericTableHeader(params, { position: 'center' }),
|
||||||
headerName: 'Track',
|
headerName: 'Track',
|
||||||
suppressSizeToFit: true,
|
suppressSizeToFit: true,
|
||||||
valueGetter: (params: ValueGetterParams) => (params.data ? params.data.trackNumber : undefined),
|
valueGetter: (params: ValueGetterParams) =>
|
||||||
|
params.data ? params.data.trackNumber : undefined,
|
||||||
width: 80,
|
width: 80,
|
||||||
},
|
},
|
||||||
userFavorite: {
|
userFavorite: {
|
||||||
|
@ -296,7 +318,8 @@ const tableColumns: { [key: string]: ColDef } = {
|
||||||
width: 50,
|
width: 50,
|
||||||
},
|
},
|
||||||
userRating: {
|
userRating: {
|
||||||
cellClass: (params) => (params.value?.userRating ? 'visible ag-cell-rating' : 'ag-cell-rating'),
|
cellClass: (params) =>
|
||||||
|
params.value?.userRating ? 'visible ag-cell-rating' : 'ag-cell-rating',
|
||||||
cellRenderer: RatingCell,
|
cellRenderer: RatingCell,
|
||||||
colId: TableColumn.USER_RATING,
|
colId: TableColumn.USER_RATING,
|
||||||
field: 'userRating',
|
field: 'userRating',
|
||||||
|
@ -433,7 +456,9 @@ export const VirtualTable = forwardRef(
|
||||||
<TableWrapper
|
<TableWrapper
|
||||||
ref={deselectRef}
|
ref={deselectRef}
|
||||||
className={
|
className={
|
||||||
transparentHeader ? 'ag-header-transparent ag-theme-alpine-dark' : 'ag-theme-alpine-dark'
|
transparentHeader
|
||||||
|
? 'ag-header-transparent ag-theme-alpine-dark'
|
||||||
|
: 'ag-theme-alpine-dark'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AgGridReact
|
<AgGridReact
|
||||||
|
|
|
@ -47,7 +47,11 @@ export const TablePagination = ({
|
||||||
|
|
||||||
const handleGoSubmit = goToForm.onSubmit((values) => {
|
const handleGoSubmit = goToForm.onSubmit((values) => {
|
||||||
handlers.close();
|
handlers.close();
|
||||||
if (!values.pageNumber || values.pageNumber < 1 || values.pageNumber > pagination.totalPages) {
|
if (
|
||||||
|
!values.pageNumber ||
|
||||||
|
values.pageNumber < 1 ||
|
||||||
|
values.pageNumber > pagination.totalPages
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,12 @@ export const ServerCredentialRequired = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text>
|
<Text>
|
||||||
The selected server '{currentServer?.name}' requires an additional login to
|
The selected server '{currentServer?.name}' requires an additional login
|
||||||
access.
|
to access.
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
Add your credentials in the 'manage servers' menu or switch to a different server.
|
Add your credentials in the 'manage servers' menu or switch to a different
|
||||||
|
server.
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -129,8 +129,13 @@ export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => {
|
||||||
const rowData: (QueueSong | { id: string; name: string })[] = [];
|
const rowData: (QueueSong | { id: string; name: string })[] = [];
|
||||||
|
|
||||||
for (const discNumber of uniqueDiscNumbers.values()) {
|
for (const discNumber of uniqueDiscNumbers.values()) {
|
||||||
const songsByDiscNumber = detailQuery.data?.songs.filter((s) => s.discNumber === discNumber);
|
const songsByDiscNumber = detailQuery.data?.songs.filter(
|
||||||
rowData.push({ id: `disc-${discNumber}`, name: `Disc ${discNumber}`.toLocaleUpperCase() });
|
(s) => s.discNumber === discNumber,
|
||||||
|
);
|
||||||
|
rowData.push({
|
||||||
|
id: `disc-${discNumber}`,
|
||||||
|
name: `Disc ${discNumber}`.toLocaleUpperCase(),
|
||||||
|
});
|
||||||
rowData.push(...songsByDiscNumber);
|
rowData.push(...songsByDiscNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,7 +280,9 @@ export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => {
|
||||||
<Group spacing="xs">
|
<Group spacing="xs">
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
loading={createFavoriteMutation.isLoading || deleteFavoriteMutation.isLoading}
|
loading={
|
||||||
|
createFavoriteMutation.isLoading || deleteFavoriteMutation.isLoading
|
||||||
|
}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={handleFavorite}
|
onClick={handleFavorite}
|
||||||
>
|
>
|
||||||
|
@ -315,7 +322,9 @@ export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => {
|
||||||
component={Link}
|
component={Link}
|
||||||
radius={0}
|
radius={0}
|
||||||
size="md"
|
size="md"
|
||||||
to={generatePath(`${AppRoute.LIBRARY_ALBUMS}?genre=${genre.id}`, { albumId })}
|
to={generatePath(`${AppRoute.LIBRARY_ALBUMS}?genre=${genre.id}`, {
|
||||||
|
albumId,
|
||||||
|
})}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{genre.name}
|
{genre.name}
|
||||||
|
@ -369,7 +378,9 @@ export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => {
|
||||||
property: 'name',
|
property: 'name',
|
||||||
route: {
|
route: {
|
||||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
slugs: [
|
||||||
|
{ idProperty: 'id', slugProperty: 'albumId' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -377,7 +388,12 @@ export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => {
|
||||||
property: 'albumArtists',
|
property: 'albumArtists',
|
||||||
route: {
|
route: {
|
||||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
slugs: [
|
||||||
|
{
|
||||||
|
idProperty: 'id',
|
||||||
|
slugProperty: 'albumArtistId',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|
|
@ -36,7 +36,8 @@ export const AlbumDetailHeader = forwardRef(
|
||||||
{
|
{
|
||||||
id: 'duration',
|
id: 'duration',
|
||||||
secondary: true,
|
secondary: true,
|
||||||
value: detailQuery?.data?.duration && formatDurationString(detailQuery.data.duration),
|
value:
|
||||||
|
detailQuery?.data?.duration && formatDurationString(detailQuery.data.duration),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -89,7 +90,10 @@ export const AlbumDetailHeader = forwardRef(
|
||||||
<>
|
<>
|
||||||
<Text $noSelect>•</Text>
|
<Text $noSelect>•</Text>
|
||||||
<Rating
|
<Rating
|
||||||
readOnly={detailQuery?.isFetching || updateRatingMutation.isLoading}
|
readOnly={
|
||||||
|
detailQuery?.isFetching ||
|
||||||
|
updateRatingMutation.isLoading
|
||||||
|
}
|
||||||
value={detailQuery?.data?.userRating || 0}
|
value={detailQuery?.data?.userRating || 0}
|
||||||
onChange={handleUpdateRating}
|
onChange={handleUpdateRating}
|
||||||
onClick={handleClearRating}
|
onClick={handleClearRating}
|
||||||
|
|
|
@ -48,7 +48,11 @@ const FILTERS = {
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Critic Rating', value: AlbumListSort.CRITIC_RATING },
|
{ defaultOrder: SortOrder.DESC, name: 'Critic Rating', value: AlbumListSort.CRITIC_RATING },
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumListSort.NAME },
|
{ defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumListSort.NAME },
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Random', value: AlbumListSort.RANDOM },
|
{ defaultOrder: SortOrder.ASC, name: 'Random', value: AlbumListSort.RANDOM },
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Recently Added', value: AlbumListSort.RECENTLY_ADDED },
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: 'Recently Added',
|
||||||
|
value: AlbumListSort.RECENTLY_ADDED,
|
||||||
|
},
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Release Date', value: AlbumListSort.RELEASE_DATE },
|
{ defaultOrder: SortOrder.DESC, name: 'Release Date', value: AlbumListSort.RELEASE_DATE },
|
||||||
],
|
],
|
||||||
navidrome: [
|
navidrome: [
|
||||||
|
@ -59,8 +63,16 @@ const FILTERS = {
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumListSort.NAME },
|
{ defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumListSort.NAME },
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Random', value: AlbumListSort.RANDOM },
|
{ defaultOrder: SortOrder.ASC, name: 'Random', value: AlbumListSort.RANDOM },
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Rating', value: AlbumListSort.RATING },
|
{ defaultOrder: SortOrder.DESC, name: 'Rating', value: AlbumListSort.RATING },
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Recently Added', value: AlbumListSort.RECENTLY_ADDED },
|
{
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Recently Played', value: AlbumListSort.RECENTLY_PLAYED },
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: 'Recently Added',
|
||||||
|
value: AlbumListSort.RECENTLY_ADDED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: 'Recently Played',
|
||||||
|
value: AlbumListSort.RECENTLY_PLAYED,
|
||||||
|
},
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Song Count', value: AlbumListSort.SONG_COUNT },
|
{ defaultOrder: SortOrder.DESC, name: 'Song Count', value: AlbumListSort.SONG_COUNT },
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Favorited', value: AlbumListSort.FAVORITED },
|
{ defaultOrder: SortOrder.DESC, name: 'Favorited', value: AlbumListSort.FAVORITED },
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Year', value: AlbumListSort.YEAR },
|
{ defaultOrder: SortOrder.DESC, name: 'Year', value: AlbumListSort.YEAR },
|
||||||
|
@ -97,7 +109,8 @@ export const AlbumListHeaderFilters = ({
|
||||||
|
|
||||||
const sortByLabel =
|
const sortByLabel =
|
||||||
(server?.type &&
|
(server?.type &&
|
||||||
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filter.sortBy)?.name) ||
|
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filter.sortBy)
|
||||||
|
?.name) ||
|
||||||
'Unknown';
|
'Unknown';
|
||||||
|
|
||||||
const sortOrderLabel = ORDER.find((o) => o.value === filter.sortOrder)?.name || 'Unknown';
|
const sortOrderLabel = ORDER.find((o) => o.value === filter.sortOrder)?.name || 'Unknown';
|
||||||
|
@ -181,7 +194,10 @@ export const AlbumListHeaderFilters = ({
|
||||||
{ cacheTime: 1000 * 60 * 1 },
|
{ cacheTime: 1000 * 60 * 1 },
|
||||||
);
|
);
|
||||||
|
|
||||||
return params.successCallback(albumsRes?.items || [], albumsRes?.totalRecordCount || 0);
|
return params.successCallback(
|
||||||
|
albumsRes?.items || [],
|
||||||
|
albumsRes?.totalRecordCount || 0,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
rowCount: undefined,
|
rowCount: undefined,
|
||||||
};
|
};
|
||||||
|
@ -523,7 +539,11 @@ export const AlbumListHeaderFilters = ({
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
size="md"
|
size="md"
|
||||||
sx={{ svg: { fill: isFilterApplied ? 'var(--primary-color) !important' : undefined } }}
|
sx={{
|
||||||
|
svg: {
|
||||||
|
fill: isFilterApplied ? 'var(--primary-color) !important' : undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
tooltip={{ label: 'Filters' }}
|
tooltip={{ label: 'Filters' }}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={handleOpenFiltersModal}
|
onClick={handleOpenFiltersModal}
|
||||||
|
@ -580,7 +600,8 @@ export const AlbumListHeaderFilters = ({
|
||||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||||
<Slider
|
<Slider
|
||||||
defaultValue={
|
defaultValue={
|
||||||
display === ListDisplayType.CARD || display === ListDisplayType.POSTER
|
display === ListDisplayType.CARD ||
|
||||||
|
display === ListDisplayType.POSTER
|
||||||
? grid?.itemsPerRow || 0
|
? grid?.itemsPerRow || 0
|
||||||
: table.rowHeight
|
: table.rowHeight
|
||||||
}
|
}
|
||||||
|
@ -590,7 +611,8 @@ export const AlbumListHeaderFilters = ({
|
||||||
onChange={debouncedHandleItemSize}
|
onChange={debouncedHandleItemSize}
|
||||||
/>
|
/>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{(display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) && (
|
{(display === ListDisplayType.TABLE ||
|
||||||
|
display === ListDisplayType.TABLE_PAGINATED) && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu.Label>Table Columns</DropdownMenu.Label>
|
<DropdownMenu.Label>Table Columns</DropdownMenu.Label>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
|
@ -602,7 +624,9 @@ export const AlbumListHeaderFilters = ({
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
clearable
|
clearable
|
||||||
data={ALBUM_TABLE_COLUMNS}
|
data={ALBUM_TABLE_COLUMNS}
|
||||||
defaultValue={table?.columns.map((column) => column.column)}
|
defaultValue={table?.columns.map(
|
||||||
|
(column) => column.column,
|
||||||
|
)}
|
||||||
width={300}
|
width={300}
|
||||||
onChange={handleTableColumns}
|
onChange={handleTableColumns}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -128,7 +128,10 @@ export const AlbumListHeader = ({
|
||||||
{ cacheTime: 1000 * 60 * 1 },
|
{ cacheTime: 1000 * 60 * 1 },
|
||||||
);
|
);
|
||||||
|
|
||||||
params.successCallback(albumsRes?.items || [], albumsRes?.totalRecordCount || 0);
|
params.successCallback(
|
||||||
|
albumsRes?.items || [],
|
||||||
|
albumsRes?.totalRecordCount || 0,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
rowCount: undefined,
|
rowCount: undefined,
|
||||||
};
|
};
|
||||||
|
@ -217,9 +220,13 @@ export const AlbumListHeader = ({
|
||||||
w="100%"
|
w="100%"
|
||||||
>
|
>
|
||||||
<LibraryHeaderBar>
|
<LibraryHeaderBar>
|
||||||
<LibraryHeaderBar.PlayButton onClick={() => handlePlay(playButtonBehavior)} />
|
<LibraryHeaderBar.PlayButton
|
||||||
|
onClick={() => handlePlay(playButtonBehavior)}
|
||||||
|
/>
|
||||||
<LibraryHeaderBar.Title>{title || 'Albums'}</LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title>{title || 'Albums'}</LibraryHeaderBar.Title>
|
||||||
<LibraryHeaderBar.Badge isLoading={itemCount === null || itemCount === undefined}>
|
<LibraryHeaderBar.Badge
|
||||||
|
isLoading={itemCount === null || itemCount === undefined}
|
||||||
|
>
|
||||||
{itemCount}
|
{itemCount}
|
||||||
</LibraryHeaderBar.Badge>
|
</LibraryHeaderBar.Badge>
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
|
|
|
@ -79,7 +79,10 @@ export const AlbumListTableView = ({ tableRef, itemCount }: any) => {
|
||||||
{ cacheTime: 1000 * 60 * 1 },
|
{ cacheTime: 1000 * 60 * 1 },
|
||||||
);
|
);
|
||||||
|
|
||||||
return params.successCallback(albumsRes?.items || [], albumsRes?.totalRecordCount || 0);
|
return params.successCallback(
|
||||||
|
albumsRes?.items || [],
|
||||||
|
albumsRes?.totalRecordCount || 0,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
rowCount: undefined,
|
rowCount: undefined,
|
||||||
};
|
};
|
||||||
|
@ -95,7 +98,8 @@ export const AlbumListTableView = ({ tableRef, itemCount }: any) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Scroll to top of page on pagination change
|
// Scroll to top of page on pagination change
|
||||||
const currentPageStartIndex = table.pagination.currentPage * table.pagination.itemsPerPage;
|
const currentPageStartIndex =
|
||||||
|
table.pagination.currentPage * table.pagination.itemsPerPage;
|
||||||
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
|
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
@ -128,7 +132,9 @@ export const AlbumListTableView = ({ tableRef, itemCount }: any) => {
|
||||||
const columnsInSettings = table.columns;
|
const columnsInSettings = table.columns;
|
||||||
const updatedColumns = [];
|
const updatedColumns = [];
|
||||||
for (const column of columnsOrder) {
|
for (const column of columnsOrder) {
|
||||||
const columnInSettings = columnsInSettings.find((c) => c.column === column.getColDef().colId);
|
const columnInSettings = columnsInSettings.find(
|
||||||
|
(c) => c.column === column.getColDef().colId,
|
||||||
|
);
|
||||||
|
|
||||||
if (columnInSettings) {
|
if (columnInSettings) {
|
||||||
updatedColumns.push({
|
updatedColumns.push({
|
||||||
|
@ -150,7 +156,10 @@ export const AlbumListTableView = ({ tableRef, itemCount }: any) => {
|
||||||
setTable({ data: { scrollOffset }, key: pageKey });
|
setTable({ data: { scrollOffset }, key: pageKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContextMenu = useHandleTableContextMenu(LibraryItem.ALBUM, ALBUM_CONTEXT_MENU_ITEMS);
|
const handleContextMenu = useHandleTableContextMenu(
|
||||||
|
LibraryItem.ALBUM,
|
||||||
|
ALBUM_CONTEXT_MENU_ITEMS,
|
||||||
|
);
|
||||||
|
|
||||||
const handleRowDoubleClick = (e: RowDoubleClickedEvent) => {
|
const handleRowDoubleClick = (e: RowDoubleClickedEvent) => {
|
||||||
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id }));
|
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id }));
|
||||||
|
|
|
@ -45,7 +45,9 @@ const AlbumDetailRoute = () => {
|
||||||
children: (
|
children: (
|
||||||
<LibraryHeaderBar>
|
<LibraryHeaderBar>
|
||||||
<LibraryHeaderBar.PlayButton onClick={handlePlay} />
|
<LibraryHeaderBar.PlayButton onClick={handlePlay} />
|
||||||
<LibraryHeaderBar.Title>{detailQuery?.data?.name}</LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title>
|
||||||
|
{detailQuery?.data?.name}
|
||||||
|
</LibraryHeaderBar.Title>
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
),
|
),
|
||||||
target: headerRef,
|
target: headerRef,
|
||||||
|
|
|
@ -59,11 +59,17 @@ export const AlbumArtistDetailContent = () => {
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
|
||||||
const detailQuery = useAlbumArtistDetail({ query: { id: albumArtistId }, serverId: server?.id });
|
const detailQuery = useAlbumArtistDetail({
|
||||||
|
query: { id: albumArtistId },
|
||||||
|
serverId: server?.id,
|
||||||
|
});
|
||||||
|
|
||||||
const artistDiscographyLink = `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY, {
|
const artistDiscographyLink = `${generatePath(
|
||||||
|
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY,
|
||||||
|
{
|
||||||
albumArtistId,
|
albumArtistId,
|
||||||
})}?${createSearchParams({
|
},
|
||||||
|
)}?${createSearchParams({
|
||||||
artistId: albumArtistId,
|
artistId: albumArtistId,
|
||||||
artistName: detailQuery?.data?.name || '',
|
artistName: detailQuery?.data?.name || '',
|
||||||
})}`;
|
})}`;
|
||||||
|
@ -79,7 +85,9 @@ export const AlbumArtistDetailContent = () => {
|
||||||
query: {
|
query: {
|
||||||
_custom: {
|
_custom: {
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
...(server?.type === ServerType.JELLYFIN ? { ArtistIds: albumArtistId } : undefined),
|
...(server?.type === ServerType.JELLYFIN
|
||||||
|
? { ArtistIds: albumArtistId }
|
||||||
|
: undefined),
|
||||||
},
|
},
|
||||||
navidrome: {
|
navidrome: {
|
||||||
...(server?.type === ServerType.NAVIDROME
|
...(server?.type === ServerType.NAVIDROME
|
||||||
|
@ -303,7 +311,8 @@ export const AlbumArtistDetailContent = () => {
|
||||||
const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false;
|
const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false;
|
||||||
|
|
||||||
const isLoading =
|
const isLoading =
|
||||||
detailQuery?.isLoading || (server?.type === ServerType.NAVIDROME && topSongsQuery?.isLoading);
|
detailQuery?.isLoading ||
|
||||||
|
(server?.type === ServerType.NAVIDROME && topSongsQuery?.isLoading);
|
||||||
|
|
||||||
if (isLoading) return <ContentContainer ref={cq.ref} />;
|
if (isLoading) return <ContentContainer ref={cq.ref} />;
|
||||||
|
|
||||||
|
@ -315,7 +324,9 @@ export const AlbumArtistDetailContent = () => {
|
||||||
<Group spacing="xs">
|
<Group spacing="xs">
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
loading={createFavoriteMutation.isLoading || deleteFavoriteMutation.isLoading}
|
loading={
|
||||||
|
createFavoriteMutation.isLoading || deleteFavoriteMutation.isLoading
|
||||||
|
}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={handleFavorite}
|
onClick={handleFavorite}
|
||||||
>
|
>
|
||||||
|
@ -369,9 +380,12 @@ export const AlbumArtistDetailContent = () => {
|
||||||
component={Link}
|
component={Link}
|
||||||
radius="md"
|
radius="md"
|
||||||
size="md"
|
size="md"
|
||||||
to={generatePath(`${AppRoute.LIBRARY_ALBUM_ARTISTS}?genre=${genre.id}`, {
|
to={generatePath(
|
||||||
|
`${AppRoute.LIBRARY_ALBUM_ARTISTS}?genre=${genre.id}`,
|
||||||
|
{
|
||||||
albumArtistId,
|
albumArtistId,
|
||||||
})}
|
},
|
||||||
|
)}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
{genre.name}
|
{genre.name}
|
||||||
|
|
|
@ -38,7 +38,8 @@ export const AlbumArtistDetailHeader = forwardRef(
|
||||||
{
|
{
|
||||||
id: 'duration',
|
id: 'duration',
|
||||||
secondary: true,
|
secondary: true,
|
||||||
value: detailQuery?.data?.duration && formatDurationString(detailQuery.data.duration),
|
value:
|
||||||
|
detailQuery?.data?.duration && formatDurationString(detailQuery.data.duration),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -96,7 +97,10 @@ export const AlbumArtistDetailHeader = forwardRef(
|
||||||
<>
|
<>
|
||||||
<Text $noSelect>•</Text>
|
<Text $noSelect>•</Text>
|
||||||
<Rating
|
<Rating
|
||||||
readOnly={detailQuery?.isFetching || updateRatingMutation.isLoading}
|
readOnly={
|
||||||
|
detailQuery?.isFetching ||
|
||||||
|
updateRatingMutation.isLoading
|
||||||
|
}
|
||||||
value={detailQuery?.data?.userRating || 0}
|
value={detailQuery?.data?.userRating || 0}
|
||||||
onChange={handleUpdateRating}
|
onChange={handleUpdateRating}
|
||||||
onClick={handleClearRating}
|
onClick={handleClearRating}
|
||||||
|
|
|
@ -117,7 +117,8 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Scroll to top of page on pagination change
|
// Scroll to top of page on pagination change
|
||||||
const currentPageStartIndex = table.pagination.currentPage * table.pagination.itemsPerPage;
|
const currentPageStartIndex =
|
||||||
|
table.pagination.currentPage * table.pagination.itemsPerPage;
|
||||||
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
|
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
@ -150,7 +151,9 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon
|
||||||
const columnsInSettings = table.columns;
|
const columnsInSettings = table.columns;
|
||||||
const updatedColumns = [];
|
const updatedColumns = [];
|
||||||
for (const column of columnsOrder) {
|
for (const column of columnsOrder) {
|
||||||
const columnInSettings = columnsInSettings.find((c) => c.column === column.getColDef().colId);
|
const columnInSettings = columnsInSettings.find(
|
||||||
|
(c) => c.column === column.getColDef().colId,
|
||||||
|
);
|
||||||
|
|
||||||
if (columnInSettings) {
|
if (columnInSettings) {
|
||||||
updatedColumns.push({
|
updatedColumns.push({
|
||||||
|
@ -281,7 +284,9 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon
|
||||||
minimumBatchSize={40}
|
minimumBatchSize={40}
|
||||||
route={{
|
route={{
|
||||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
slugs: [
|
||||||
|
{ idProperty: 'id', slugProperty: 'albumArtistId' },
|
||||||
|
],
|
||||||
}}
|
}}
|
||||||
width={width}
|
width={width}
|
||||||
onScroll={handleGridScroll}
|
onScroll={handleGridScroll}
|
||||||
|
|
|
@ -44,9 +44,17 @@ const FILTERS = {
|
||||||
// { defaultOrder: SortOrder.DESC, name: 'Release Date', value: AlbumArtistListSort.RELEASE_DATE },
|
// { defaultOrder: SortOrder.DESC, name: 'Release Date', value: AlbumArtistListSort.RELEASE_DATE },
|
||||||
],
|
],
|
||||||
navidrome: [
|
navidrome: [
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Album Count', value: AlbumArtistListSort.ALBUM_COUNT },
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: 'Album Count',
|
||||||
|
value: AlbumArtistListSort.ALBUM_COUNT,
|
||||||
|
},
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Favorited', value: AlbumArtistListSort.FAVORITED },
|
{ defaultOrder: SortOrder.DESC, name: 'Favorited', value: AlbumArtistListSort.FAVORITED },
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Most Played', value: AlbumArtistListSort.PLAY_COUNT },
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: 'Most Played',
|
||||||
|
value: AlbumArtistListSort.PLAY_COUNT,
|
||||||
|
},
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumArtistListSort.NAME },
|
{ defaultOrder: SortOrder.ASC, name: 'Name', value: AlbumArtistListSort.NAME },
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Rating', value: AlbumArtistListSort.RATING },
|
{ defaultOrder: SortOrder.DESC, name: 'Rating', value: AlbumArtistListSort.RATING },
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Song Count', value: AlbumArtistListSort.SONG_COUNT },
|
{ defaultOrder: SortOrder.DESC, name: 'Song Count', value: AlbumArtistListSort.SONG_COUNT },
|
||||||
|
@ -80,7 +88,8 @@ export const AlbumArtistListHeaderFilters = ({
|
||||||
|
|
||||||
const sortByLabel =
|
const sortByLabel =
|
||||||
(server?.type &&
|
(server?.type &&
|
||||||
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filter.sortBy)?.name) ||
|
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filter.sortBy)
|
||||||
|
?.name) ||
|
||||||
'Unknown';
|
'Unknown';
|
||||||
|
|
||||||
const sortOrderLabel = ORDER.find((o) => o.value === filter.sortOrder)?.name || 'Unknown';
|
const sortOrderLabel = ORDER.find((o) => o.value === filter.sortOrder)?.name || 'Unknown';
|
||||||
|
@ -432,7 +441,8 @@ export const AlbumArtistListHeaderFilters = ({
|
||||||
<DropdownMenu.Divider />
|
<DropdownMenu.Divider />
|
||||||
<DropdownMenu.Label>Item size</DropdownMenu.Label>
|
<DropdownMenu.Label>Item size</DropdownMenu.Label>
|
||||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||||
{display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? (
|
{display === ListDisplayType.CARD ||
|
||||||
|
display === ListDisplayType.POSTER ? (
|
||||||
<Slider
|
<Slider
|
||||||
defaultValue={grid?.itemsPerRow}
|
defaultValue={grid?.itemsPerRow}
|
||||||
label={null}
|
label={null}
|
||||||
|
@ -450,7 +460,8 @@ export const AlbumArtistListHeaderFilters = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{(display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) && (
|
{(display === ListDisplayType.TABLE ||
|
||||||
|
display === ListDisplayType.TABLE_PAGINATED) && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu.Label>Table Columns</DropdownMenu.Label>
|
<DropdownMenu.Label>Table Columns</DropdownMenu.Label>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
|
@ -462,7 +473,9 @@ export const AlbumArtistListHeaderFilters = ({
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
clearable
|
clearable
|
||||||
data={ALBUMARTIST_TABLE_COLUMNS}
|
data={ALBUMARTIST_TABLE_COLUMNS}
|
||||||
defaultValue={table?.columns.map((column) => column.column)}
|
defaultValue={table?.columns.map(
|
||||||
|
(column) => column.column,
|
||||||
|
)}
|
||||||
width={300}
|
width={300}
|
||||||
onChange={handleTableColumns}
|
onChange={handleTableColumns}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -155,7 +155,9 @@ export const AlbumArtistListHeader = ({
|
||||||
>
|
>
|
||||||
<LibraryHeaderBar>
|
<LibraryHeaderBar>
|
||||||
<LibraryHeaderBar.Title>Album Artists</LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title>Album Artists</LibraryHeaderBar.Title>
|
||||||
<LibraryHeaderBar.Badge isLoading={itemCount === null || itemCount === undefined}>
|
<LibraryHeaderBar.Badge
|
||||||
|
isLoading={itemCount === null || itemCount === undefined}
|
||||||
|
>
|
||||||
{itemCount}
|
{itemCount}
|
||||||
</LibraryHeaderBar.Badge>
|
</LibraryHeaderBar.Badge>
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
|
|
|
@ -13,7 +13,10 @@ export const useAlbumArtistDetail = (args: QueryHookArgs<AlbumArtistDetailQuery>
|
||||||
enabled: !!server?.id && !!query.id,
|
enabled: !!server?.id && !!query.id,
|
||||||
queryFn: ({ signal }) => {
|
queryFn: ({ signal }) => {
|
||||||
if (!server) throw new Error('Server not found');
|
if (!server) throw new Error('Server not found');
|
||||||
return api.controller.getAlbumArtistDetail({ apiClientProps: { server, signal }, query });
|
return api.controller.getAlbumArtistDetail({
|
||||||
|
apiClientProps: { server, signal },
|
||||||
|
query,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
queryKey: queryKeys.albumArtists.detail(server?.id || '', query),
|
queryKey: queryKeys.albumArtists.detail(server?.id || '', query),
|
||||||
...options,
|
...options,
|
||||||
|
|
|
@ -13,7 +13,10 @@ export const useAlbumArtistInfo = (args: QueryHookArgs<AlbumArtistDetailQuery>)
|
||||||
enabled: !!server?.id && !!query.id,
|
enabled: !!server?.id && !!query.id,
|
||||||
queryFn: ({ signal }) => {
|
queryFn: ({ signal }) => {
|
||||||
if (!server) throw new Error('Server not found');
|
if (!server) throw new Error('Server not found');
|
||||||
return api.controller.getAlbumArtistDetail({ apiClientProps: { server, signal }, query });
|
return api.controller.getAlbumArtistDetail({
|
||||||
|
apiClientProps: { server, signal },
|
||||||
|
query,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
queryKey: queryKeys.albumArtists.detail(server?.id || '', query),
|
queryKey: queryKeys.albumArtists.detail(server?.id || '', query),
|
||||||
...options,
|
...options,
|
||||||
|
|
|
@ -19,7 +19,10 @@ const AlbumArtistDetailRoute = () => {
|
||||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
const { albumArtistId } = useParams() as { albumArtistId: string };
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
const detailQuery = useAlbumArtistDetail({ query: { id: albumArtistId }, serverId: server?.id });
|
const detailQuery = useAlbumArtistDetail({
|
||||||
|
query: { id: albumArtistId },
|
||||||
|
serverId: server?.id,
|
||||||
|
});
|
||||||
const background = useFastAverageColor(detailQuery.data?.imageUrl, !detailQuery.isLoading);
|
const background = useFastAverageColor(detailQuery.data?.imageUrl, !detailQuery.isLoading);
|
||||||
|
|
||||||
const handlePlay = () => {
|
const handlePlay = () => {
|
||||||
|
@ -43,7 +46,9 @@ const AlbumArtistDetailRoute = () => {
|
||||||
children: (
|
children: (
|
||||||
<LibraryHeaderBar>
|
<LibraryHeaderBar>
|
||||||
<LibraryHeaderBar.PlayButton onClick={handlePlay} />
|
<LibraryHeaderBar.PlayButton onClick={handlePlay} />
|
||||||
<LibraryHeaderBar.Title>{detailQuery?.data?.name}</LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title>
|
||||||
|
{detailQuery?.data?.name}
|
||||||
|
</LibraryHeaderBar.Title>
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
),
|
),
|
||||||
target: headerRef,
|
target: headerRef,
|
||||||
|
|
|
@ -13,7 +13,10 @@ const AlbumArtistDetailTopSongsListRoute = () => {
|
||||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
const { albumArtistId } = useParams() as { albumArtistId: string };
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
|
||||||
const detailQuery = useAlbumArtistDetail({ query: { id: albumArtistId }, serverId: server?.id });
|
const detailQuery = useAlbumArtistDetail({
|
||||||
|
query: { id: albumArtistId },
|
||||||
|
serverId: server?.id,
|
||||||
|
});
|
||||||
|
|
||||||
const topSongsQuery = useTopSongsList({
|
const topSongsQuery = useTopSongsList({
|
||||||
options: { enabled: !!detailQuery?.data?.name },
|
options: { enabled: !!detailQuery?.data?.name },
|
||||||
|
|
|
@ -112,7 +112,9 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
let validMenuItems = menuItems;
|
let validMenuItems = menuItems;
|
||||||
|
|
||||||
if (serverType === ServerType.JELLYFIN) {
|
if (serverType === ServerType.JELLYFIN) {
|
||||||
validMenuItems = menuItems.filter((item) => !JELLYFIN_IGNORED_MENU_ITEMS.includes(item.id));
|
validMenuItems = menuItems.filter(
|
||||||
|
(item) => !JELLYFIN_IGNORED_MENU_ITEMS.includes(item.id),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the context menu dimension can't be automatically calculated, calculate it manually
|
// If the context menu dimension can't be automatically calculated, calculate it manually
|
||||||
|
@ -361,7 +363,9 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
}, {} as Record<string, AnyLibraryItems>);
|
}, {} as Record<string, AnyLibraryItems>);
|
||||||
|
|
||||||
for (const serverId of Object.keys(itemsByServerId)) {
|
for (const serverId of Object.keys(itemsByServerId)) {
|
||||||
const idsToUnfavorite = itemsByServerId[serverId].map((item: AnyLibraryItem) => item.id);
|
const idsToUnfavorite = itemsByServerId[serverId].map(
|
||||||
|
(item: AnyLibraryItem) => item.id,
|
||||||
|
);
|
||||||
deleteFavoriteMutation.mutate({
|
deleteFavoriteMutation.mutate({
|
||||||
query: {
|
query: {
|
||||||
id: idsToUnfavorite,
|
id: idsToUnfavorite,
|
||||||
|
@ -779,16 +783,27 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
<HoverCard.Target>
|
<HoverCard.Target>
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
disabled={item.disabled}
|
disabled={item.disabled}
|
||||||
leftIcon={contextMenuItems[item.id].leftIcon}
|
leftIcon={
|
||||||
rightIcon={contextMenuItems[item.id].rightIcon}
|
contextMenuItems[item.id]
|
||||||
onClick={contextMenuItems[item.id].onClick}
|
.leftIcon
|
||||||
|
}
|
||||||
|
rightIcon={
|
||||||
|
contextMenuItems[item.id]
|
||||||
|
.rightIcon
|
||||||
|
}
|
||||||
|
onClick={
|
||||||
|
contextMenuItems[item.id]
|
||||||
|
.onClick
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{contextMenuItems[item.id].label}
|
{contextMenuItems[item.id].label}
|
||||||
</ContextMenuButton>
|
</ContextMenuButton>
|
||||||
</HoverCard.Target>
|
</HoverCard.Target>
|
||||||
<HoverCard.Dropdown>
|
<HoverCard.Dropdown>
|
||||||
<Stack spacing={0}>
|
<Stack spacing={0}>
|
||||||
{contextMenuItems[item.id].children?.map((child) => (
|
{contextMenuItems[
|
||||||
|
item.id
|
||||||
|
].children?.map((child) => (
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
key={`sub-${child.id}`}
|
key={`sub-${child.id}`}
|
||||||
disabled={child.disabled}
|
disabled={child.disabled}
|
||||||
|
@ -805,8 +820,12 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
) : (
|
) : (
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
disabled={item.disabled}
|
disabled={item.disabled}
|
||||||
leftIcon={contextMenuItems[item.id].leftIcon}
|
leftIcon={
|
||||||
rightIcon={contextMenuItems[item.id].rightIcon}
|
contextMenuItems[item.id].leftIcon
|
||||||
|
}
|
||||||
|
rightIcon={
|
||||||
|
contextMenuItems[item.id].rightIcon
|
||||||
|
}
|
||||||
onClick={contextMenuItems[item.id].onClick}
|
onClick={contextMenuItems[item.id].onClick}
|
||||||
>
|
>
|
||||||
{contextMenuItems[item.id].label}
|
{contextMenuItems[item.id].label}
|
||||||
|
@ -828,7 +847,9 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
||||||
color="rgb(62, 62, 62)"
|
color="rgb(62, 62, 62)"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
<ContextMenuButton disabled>{ctx.data?.length} selected</ContextMenuButton>
|
<ContextMenuButton disabled>
|
||||||
|
{ctx.data?.length} selected
|
||||||
|
</ContextMenuButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -20,7 +20,9 @@ export const useHandleTableContextMenu = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldReplaceSelected = !selectedNodes.map((node) => node.data.id).includes(e.data.id);
|
const shouldReplaceSelected = !selectedNodes
|
||||||
|
.map((node) => node.data.id)
|
||||||
|
.includes(e.data.id);
|
||||||
|
|
||||||
if (shouldReplaceSelected) {
|
if (shouldReplaceSelected) {
|
||||||
e.api.deselectAll();
|
e.api.deselectAll();
|
||||||
|
|
|
@ -151,7 +151,10 @@ const HomeRoute = () => {
|
||||||
<FeatureCarousel data={featureItemsWithImage} />
|
<FeatureCarousel data={featureItemsWithImage} />
|
||||||
{carousels
|
{carousels
|
||||||
.filter((carousel) => {
|
.filter((carousel) => {
|
||||||
if (server?.type === ServerType.JELLYFIN && carousel.uniqueId === 'recentlyPlayed') {
|
if (
|
||||||
|
server?.type === ServerType.JELLYFIN &&
|
||||||
|
carousel.uniqueId === 'recentlyPlayed'
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,7 +176,9 @@ const HomeRoute = () => {
|
||||||
property: 'albumArtists',
|
property: 'albumArtists',
|
||||||
route: {
|
route: {
|
||||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
slugs: [
|
||||||
|
{ idProperty: 'id', slugProperty: 'albumArtistId' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|
|
@ -188,7 +188,9 @@ export const Lyrics = () => {
|
||||||
{isSynchronizedLyrics ? (
|
{isSynchronizedLyrics ? (
|
||||||
<SynchronizedLyrics {...lyricsMetadata} />
|
<SynchronizedLyrics {...lyricsMetadata} />
|
||||||
) : (
|
) : (
|
||||||
<UnsynchronizedLyrics {...(lyricsMetadata as UnsynchronizedLyricMetadata)} />
|
<UnsynchronizedLyrics
|
||||||
|
{...(lyricsMetadata as UnsynchronizedLyricMetadata)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -152,7 +152,9 @@ export const useSongLyricsByRemoteId = (
|
||||||
enabled: !!query.remoteSongId && !!query.remoteSource,
|
enabled: !!query.remoteSongId && !!query.remoteSource,
|
||||||
onError: () => {},
|
onError: () => {},
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const remoteLyricsResult: string | null = await lyricsIpc?.getRemoteLyricsByRemoteId(query);
|
const remoteLyricsResult: string | null = await lyricsIpc?.getRemoteLyricsByRemoteId(
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
|
||||||
if (remoteLyricsResult) {
|
if (remoteLyricsResult) {
|
||||||
return formatLyrics(remoteLyricsResult);
|
return formatLyrics(remoteLyricsResult);
|
||||||
|
|
|
@ -99,7 +99,9 @@ export const SynchronizedLyrics = ({
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = (playersRef.current.player1 ?? playersRef.current.player2).getInternalPlayer();
|
const player = (
|
||||||
|
playersRef.current.player1 ?? playersRef.current.player2
|
||||||
|
).getInternalPlayer();
|
||||||
|
|
||||||
// If it is null, this probably means we added a new song while the lyrics tab is open
|
// If it is null, this probably means we added a new song while the lyrics tab is open
|
||||||
// and the queue was previously empty
|
// and the queue was previously empty
|
||||||
|
@ -108,7 +110,8 @@ export const SynchronizedLyrics = ({
|
||||||
return player.currentTime;
|
return player.currentTime;
|
||||||
}, [playerType, playersRef]);
|
}, [playerType, playersRef]);
|
||||||
|
|
||||||
const setCurrentLyric = useCallback((timeInMs: number, epoch?: number, targetIndex?: number) => {
|
const setCurrentLyric = useCallback(
|
||||||
|
(timeInMs: number, epoch?: number, targetIndex?: number) => {
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
let nextEpoch: number;
|
let nextEpoch: number;
|
||||||
|
|
||||||
|
@ -139,7 +142,9 @@ export const SynchronizedLyrics = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const doc = document.getElementById('sychronized-lyrics-scroll-container') as HTMLElement;
|
const doc = document.getElementById(
|
||||||
|
'sychronized-lyrics-scroll-container',
|
||||||
|
) as HTMLElement;
|
||||||
const currentLyric = document.querySelector(`#lyric-${index}`) as HTMLElement;
|
const currentLyric = document.querySelector(`#lyric-${index}`) as HTMLElement;
|
||||||
const offsetTop = currentLyric?.offsetTop - doc?.clientHeight / 2 ?? 0;
|
const offsetTop = currentLyric?.offsetTop - doc?.clientHeight / 2 ?? 0;
|
||||||
|
|
||||||
|
@ -163,7 +168,9 @@ export const SynchronizedLyrics = ({
|
||||||
setCurrentLyric(nextTime, nextEpoch, index + 1);
|
setCurrentLyric(nextTime, nextEpoch, index + 1);
|
||||||
}, nextTime - timeInMs - elapsed);
|
}, nextTime - timeInMs - elapsed);
|
||||||
}
|
}
|
||||||
}, []);
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Copy the follow settings into a ref that can be accessed in the timeout
|
// Copy the follow settings into a ref that can be accessed in the timeout
|
||||||
|
|
|
@ -134,7 +134,9 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
||||||
|
|
||||||
const updatedColumns = [];
|
const updatedColumns = [];
|
||||||
for (const column of columnsOrder) {
|
for (const column of columnsOrder) {
|
||||||
const columnInSettings = columnsInSettings.find((c) => c.column === column.getColDef().colId);
|
const columnInSettings = columnsInSettings.find(
|
||||||
|
(c) => c.column === column.getColDef().colId,
|
||||||
|
);
|
||||||
|
|
||||||
if (columnInSettings) {
|
if (columnInSettings) {
|
||||||
updatedColumns.push({
|
updatedColumns.push({
|
||||||
|
@ -181,12 +183,16 @@ export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentNode = currentSong?.uniqueId ? api.getRowNode(currentSong.uniqueId) : undefined;
|
const currentNode = currentSong?.uniqueId
|
||||||
|
? api.getRowNode(currentSong.uniqueId)
|
||||||
|
: undefined;
|
||||||
const previousNode = previousSong?.uniqueId
|
const previousNode = previousSong?.uniqueId
|
||||||
? api.getRowNode(previousSong?.uniqueId)
|
? api.getRowNode(previousSong?.uniqueId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const rowNodes = [currentNode, previousNode].filter((e) => e !== undefined) as RowNode<any>[];
|
const rowNodes = [currentNode, previousNode].filter(
|
||||||
|
(e) => e !== undefined,
|
||||||
|
) as RowNode<any>[];
|
||||||
|
|
||||||
if (rowNodes) {
|
if (rowNodes) {
|
||||||
api.redrawRows({ rowNodes });
|
api.redrawRows({ rowNodes });
|
||||||
|
|
|
@ -211,7 +211,11 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||||
)}
|
)}
|
||||||
<PlayerButton
|
<PlayerButton
|
||||||
icon={
|
icon={
|
||||||
status === PlayerStatus.PAUSED ? <RiPlayFill size={20} /> : <IoIosPause size={20} />
|
status === PlayerStatus.PAUSED ? (
|
||||||
|
<RiPlayFill size={20} />
|
||||||
|
) : (
|
||||||
|
<IoIosPause size={20} />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: status === PlayerStatus.PAUSED ? 'Play' : 'Pause',
|
label: status === PlayerStatus.PAUSED ? 'Play' : 'Pause',
|
||||||
|
|
|
@ -266,7 +266,9 @@ export const FullScreenPlayerImage = () => {
|
||||||
{currentSong?.container} {currentSong?.bitRate}
|
{currentSong?.container} {currentSong?.bitRate}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{currentSong?.releaseYear && <Badge size="lg">{currentSong?.releaseYear}</Badge>}
|
{currentSong?.releaseYear && (
|
||||||
|
<Badge size="lg">{currentSong?.releaseYear}</Badge>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</MetadataContainer>
|
</MetadataContainer>
|
||||||
</PlayerContainer>
|
</PlayerContainer>
|
||||||
|
|
|
@ -121,7 +121,9 @@ export const LeftControls = () => {
|
||||||
|
|
||||||
useHotkeys([
|
useHotkeys([
|
||||||
[
|
[
|
||||||
bindings.toggleFullscreenPlayer.allowGlobal ? '' : bindings.toggleFullscreenPlayer.hotkey,
|
bindings.toggleFullscreenPlayer.allowGlobal
|
||||||
|
? ''
|
||||||
|
: bindings.toggleFullscreenPlayer.hotkey,
|
||||||
handleToggleFullScreenPlayer,
|
handleToggleFullScreenPlayer,
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
@ -174,7 +176,12 @@ export const LeftControls = () => {
|
||||||
opacity={0.8}
|
opacity={0.8}
|
||||||
radius={50}
|
radius={50}
|
||||||
size="md"
|
size="md"
|
||||||
sx={{ cursor: 'default', position: 'absolute', right: 2, top: 2 }}
|
sx={{
|
||||||
|
cursor: 'default',
|
||||||
|
position: 'absolute',
|
||||||
|
right: 2,
|
||||||
|
top: 2,
|
||||||
|
}}
|
||||||
tooltip={{ label: 'Expand', openDelay: 500 }}
|
tooltip={{ label: 'Expand', openDelay: 500 }}
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={handleToggleSidebarImage}
|
onClick={handleToggleSidebarImage}
|
||||||
|
|
|
@ -97,7 +97,8 @@ const StyledPlayerButton = styled(UnstyledButton)<StyledPlayerButtonProps>`
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
display: flex;
|
display: flex;
|
||||||
fill: ${({ $isActive }) => ($isActive ? 'var(--primary-color)' : 'var(--playerbar-btn-fg)')};
|
fill: ${({ $isActive }) =>
|
||||||
|
$isActive ? 'var(--primary-color)' : 'var(--playerbar-btn-fg)'};
|
||||||
stroke: var(--playerbar-btn-fg);
|
stroke: var(--playerbar-btn-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -149,7 +149,9 @@ export const RightControls = () => {
|
||||||
}
|
}
|
||||||
sx={{
|
sx={{
|
||||||
svg: {
|
svg: {
|
||||||
fill: !currentSong?.userFavorite ? undefined : 'var(--primary-color) !important',
|
fill: !currentSong?.userFavorite
|
||||||
|
? undefined
|
||||||
|
: 'var(--primary-color) !important',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
tooltip={{
|
tooltip={{
|
||||||
|
|
|
@ -227,7 +227,10 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
const playerData = autoNext();
|
const playerData = autoNext();
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
mprisUpdateSong({
|
||||||
|
song: playerData.current.song,
|
||||||
|
status: PlayerStatus.PLAYING,
|
||||||
|
});
|
||||||
mpvPlayer.autoNext(playerData);
|
mpvPlayer.autoNext(playerData);
|
||||||
play();
|
play();
|
||||||
}
|
}
|
||||||
|
@ -239,7 +242,10 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
const playerData = autoNext();
|
const playerData = autoNext();
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
mprisUpdateSong({
|
||||||
|
song: playerData.current.song,
|
||||||
|
status: PlayerStatus.PLAYING,
|
||||||
|
});
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -258,7 +264,10 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
} else {
|
} else {
|
||||||
const playerData = autoNext();
|
const playerData = autoNext();
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
mprisUpdateSong({
|
||||||
|
song: playerData.current.song,
|
||||||
|
status: PlayerStatus.PLAYING,
|
||||||
|
});
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -316,7 +325,10 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
const playerData = next();
|
const playerData = next();
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
mprisUpdateSong({
|
||||||
|
song: playerData.current.song,
|
||||||
|
status: PlayerStatus.PLAYING,
|
||||||
|
});
|
||||||
mpvPlayer.setQueue(playerData);
|
mpvPlayer.setQueue(playerData);
|
||||||
mpvPlayer.next();
|
mpvPlayer.next();
|
||||||
}
|
}
|
||||||
|
@ -324,12 +336,18 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
web: () => {
|
web: () => {
|
||||||
if (isLastTrack) {
|
if (isLastTrack) {
|
||||||
const playerData = setCurrentIndex(0);
|
const playerData = setCurrentIndex(0);
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
mprisUpdateSong({
|
||||||
|
song: playerData.current.song,
|
||||||
|
status: PlayerStatus.PLAYING,
|
||||||
|
});
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
const playerData = next();
|
const playerData = next();
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
mprisUpdateSong({
|
||||||
|
song: playerData.current.song,
|
||||||
|
status: PlayerStatus.PLAYING,
|
||||||
|
});
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -345,7 +363,10 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
web: () => {
|
web: () => {
|
||||||
if (!isLastTrack) {
|
if (!isLastTrack) {
|
||||||
const playerData = next();
|
const playerData = next();
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
mprisUpdateSong({
|
||||||
|
song: playerData.current.song,
|
||||||
|
status: PlayerStatus.PLAYING,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -399,12 +420,18 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
local: () => {
|
local: () => {
|
||||||
if (!isFirstTrack) {
|
if (!isFirstTrack) {
|
||||||
const playerData = previous();
|
const playerData = previous();
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
mprisUpdateSong({
|
||||||
|
song: playerData.current.song,
|
||||||
|
status: PlayerStatus.PLAYING,
|
||||||
|
});
|
||||||
mpvPlayer.setQueue(playerData);
|
mpvPlayer.setQueue(playerData);
|
||||||
mpvPlayer.previous();
|
mpvPlayer.previous();
|
||||||
} else {
|
} else {
|
||||||
const playerData = setCurrentIndex(queue.length - 1);
|
const playerData = setCurrentIndex(queue.length - 1);
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
mprisUpdateSong({
|
||||||
|
song: playerData.current.song,
|
||||||
|
status: PlayerStatus.PLAYING,
|
||||||
|
});
|
||||||
mpvPlayer.setQueue(playerData);
|
mpvPlayer.setQueue(playerData);
|
||||||
mpvPlayer.previous();
|
mpvPlayer.previous();
|
||||||
}
|
}
|
||||||
|
@ -412,11 +439,17 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
web: () => {
|
web: () => {
|
||||||
if (isFirstTrack) {
|
if (isFirstTrack) {
|
||||||
const playerData = setCurrentIndex(queue.length - 1);
|
const playerData = setCurrentIndex(queue.length - 1);
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
mprisUpdateSong({
|
||||||
|
song: playerData.current.song,
|
||||||
|
status: PlayerStatus.PLAYING,
|
||||||
|
});
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
} else {
|
} else {
|
||||||
const playerData = previous();
|
const playerData = previous();
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
mprisUpdateSong({
|
||||||
|
song: playerData.current.song,
|
||||||
|
status: PlayerStatus.PLAYING,
|
||||||
|
});
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -439,7 +472,10 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
pause();
|
pause();
|
||||||
} else {
|
} else {
|
||||||
const playerData = previous();
|
const playerData = previous();
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
mprisUpdateSong({
|
||||||
|
song: playerData.current.song,
|
||||||
|
status: PlayerStatus.PLAYING,
|
||||||
|
});
|
||||||
resetPlayers();
|
resetPlayers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -449,7 +485,10 @@ export const useCenterControls = (args: { playersRef: any }) => {
|
||||||
local: () => {
|
local: () => {
|
||||||
if (!isFirstTrack) {
|
if (!isFirstTrack) {
|
||||||
const playerData = previous();
|
const playerData = previous();
|
||||||
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
|
mprisUpdateSong({
|
||||||
|
song: playerData.current.song,
|
||||||
|
status: PlayerStatus.PLAYING,
|
||||||
|
});
|
||||||
mpvPlayer.setQueue(playerData);
|
mpvPlayer.setQueue(playerData);
|
||||||
mpvPlayer.previous();
|
mpvPlayer.previous();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -77,7 +77,8 @@ export const useHandlePlayQueueAdd = () => {
|
||||||
toast.info({
|
toast.info({
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
id: fetchId,
|
id: fetchId,
|
||||||
message: 'This is taking a while... close the notification to cancel the request',
|
message:
|
||||||
|
'This is taking a while... close the notification to cancel the request',
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
queryClient.cancelQueries({
|
queryClient.cancelQueries({
|
||||||
exact: false,
|
exact: false,
|
||||||
|
@ -91,11 +92,21 @@ export const useHandlePlayQueueAdd = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (itemType === LibraryItem.PLAYLIST) {
|
if (itemType === LibraryItem.PLAYLIST) {
|
||||||
songList = await getPlaylistSongsById({ id: id?.[0], query, queryClient, server });
|
songList = await getPlaylistSongsById({
|
||||||
|
id: id?.[0],
|
||||||
|
query,
|
||||||
|
queryClient,
|
||||||
|
server,
|
||||||
|
});
|
||||||
} else if (itemType === LibraryItem.ALBUM) {
|
} else if (itemType === LibraryItem.ALBUM) {
|
||||||
songList = await getAlbumSongsById({ id, query, queryClient, server });
|
songList = await getAlbumSongsById({ id, query, queryClient, server });
|
||||||
} else if (itemType === LibraryItem.ALBUM_ARTIST) {
|
} else if (itemType === LibraryItem.ALBUM_ARTIST) {
|
||||||
songList = await getAlbumArtistSongsById({ id, query, queryClient, server });
|
songList = await getAlbumArtistSongsById({
|
||||||
|
id,
|
||||||
|
query,
|
||||||
|
queryClient,
|
||||||
|
server,
|
||||||
|
});
|
||||||
} else if (itemType === LibraryItem.SONG) {
|
} else if (itemType === LibraryItem.SONG) {
|
||||||
if (id?.length === 1) {
|
if (id?.length === 1) {
|
||||||
songList = await getSongById({ id: id?.[0], queryClient, server });
|
songList = await getSongById({ id: id?.[0], queryClient, server });
|
||||||
|
@ -122,13 +133,17 @@ export const useHandlePlayQueueAdd = () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
songs = songList?.items?.map((song: Song) => ({ ...song, uniqueId: nanoid() })) || null;
|
songs =
|
||||||
|
songList?.items?.map((song: Song) => ({ ...song, uniqueId: nanoid() })) || null;
|
||||||
} else if (byData) {
|
} else if (byData) {
|
||||||
songs = byData.map((song) => ({ ...song, uniqueId: nanoid() })) || null;
|
songs = byData.map((song) => ({ ...song, uniqueId: nanoid() })) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!songs || songs?.length === 0)
|
if (!songs || songs?.length === 0)
|
||||||
return toast.warn({ message: 'The query returned no results', title: 'No tracks added' });
|
return toast.warn({
|
||||||
|
message: 'The query returned no results',
|
||||||
|
title: 'No tracks added',
|
||||||
|
});
|
||||||
|
|
||||||
if (initialIndex) {
|
if (initialIndex) {
|
||||||
initialSongIndex = initialIndex;
|
initialSongIndex = initialIndex;
|
||||||
|
|
|
@ -40,10 +40,13 @@ const checkScrobbleConditions = (args: {
|
||||||
songDuration: number;
|
songDuration: number;
|
||||||
}) => {
|
}) => {
|
||||||
const { scrobbleAtDuration, scrobbleAtPercentage, songCompletedDuration, songDuration } = args;
|
const { scrobbleAtDuration, scrobbleAtPercentage, songCompletedDuration, songDuration } = args;
|
||||||
const percentageOfSongCompleted = songDuration ? (songCompletedDuration / songDuration) * 100 : 0;
|
const percentageOfSongCompleted = songDuration
|
||||||
|
? (songCompletedDuration / songDuration) * 100
|
||||||
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
percentageOfSongCompleted >= scrobbleAtPercentage || songCompletedDuration >= scrobbleAtDuration
|
percentageOfSongCompleted >= scrobbleAtPercentage ||
|
||||||
|
songCompletedDuration >= scrobbleAtDuration
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -82,7 +85,10 @@ export const useScrobble = () => {
|
||||||
const progressIntervalId = useRef<ReturnType<typeof setInterval> | null>(null);
|
const progressIntervalId = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const songChangeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const songChangeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const handleScrobbleFromSongChange = useCallback(
|
const handleScrobbleFromSongChange = useCallback(
|
||||||
(current: (QueueSong | number | undefined)[], previous: (QueueSong | number | undefined)[]) => {
|
(
|
||||||
|
current: (QueueSong | number | undefined)[],
|
||||||
|
previous: (QueueSong | number | undefined)[],
|
||||||
|
) => {
|
||||||
if (!isScrobbleEnabled) return;
|
if (!isScrobbleEnabled) return;
|
||||||
|
|
||||||
if (progressIntervalId.current) {
|
if (progressIntervalId.current) {
|
||||||
|
@ -107,7 +113,9 @@ export const useScrobble = () => {
|
||||||
previousSong?.serverType === ServerType.JELLYFIN
|
previousSong?.serverType === ServerType.JELLYFIN
|
||||||
) {
|
) {
|
||||||
const position =
|
const position =
|
||||||
previousSong?.serverType === ServerType.JELLYFIN ? previousSongTime * 1e7 : undefined;
|
previousSong?.serverType === ServerType.JELLYFIN
|
||||||
|
? previousSongTime * 1e7
|
||||||
|
: undefined;
|
||||||
|
|
||||||
sendScrobble.mutate({
|
sendScrobble.mutate({
|
||||||
query: {
|
query: {
|
||||||
|
|
|
@ -157,7 +157,8 @@ export const AddToPlaylistContextModal = ({
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
toast.error({
|
toast.error({
|
||||||
message: `[${
|
message: `[${
|
||||||
playlistSelect.find((playlist) => playlist.value === playlistId)?.label
|
playlistSelect.find((playlist) => playlist.value === playlistId)
|
||||||
|
?.label
|
||||||
}] ${err.message}`,
|
}] ${err.message}`,
|
||||||
title: 'Failed to add songs to playlist',
|
title: 'Failed to add songs to playlist',
|
||||||
});
|
});
|
||||||
|
|
|
@ -97,7 +97,9 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
||||||
{isPublicDisplayed && (
|
{isPublicDisplayed && (
|
||||||
<Switch
|
<Switch
|
||||||
label="Is public?"
|
label="Is public?"
|
||||||
{...form.getInputProps('_custom.navidrome.public', { type: 'checkbox' })}
|
{...form.getInputProps('_custom.navidrome.public', {
|
||||||
|
type: 'checkbox',
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{server?.type === ServerType.NAVIDROME && (
|
{server?.type === ServerType.NAVIDROME && (
|
||||||
|
|
|
@ -78,7 +78,9 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
|
||||||
|
|
||||||
const columnDefs: ColDef[] = useMemo(
|
const columnDefs: ColDef[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getColumnDefs(page.table.columns).filter((c) => c.colId !== 'album' && c.colId !== 'artist'),
|
getColumnDefs(page.table.columns).filter(
|
||||||
|
(c) => c.colId !== 'album' && c.colId !== 'artist',
|
||||||
|
),
|
||||||
[page.table.columns],
|
[page.table.columns],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -182,14 +184,16 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Target>
|
</DropdownMenu.Target>
|
||||||
<DropdownMenu.Dropdown>
|
<DropdownMenu.Dropdown>
|
||||||
{PLAY_TYPES.filter((type) => type.play !== playButtonBehavior).map((type) => (
|
{PLAY_TYPES.filter((type) => type.play !== playButtonBehavior).map(
|
||||||
|
(type) => (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
key={`playtype-${type.play}`}
|
key={`playtype-${type.play}`}
|
||||||
onClick={() => handlePlay(type.play)}
|
onClick={() => handlePlay(type.play)}
|
||||||
>
|
>
|
||||||
{type.label}
|
{type.label}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
))}
|
),
|
||||||
|
)}
|
||||||
<DropdownMenu.Divider />
|
<DropdownMenu.Divider />
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -199,7 +203,9 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
|
||||||
>
|
>
|
||||||
Edit playlist
|
Edit playlist
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item onClick={openDeletePlaylist}>Delete playlist</DropdownMenu.Item>
|
<DropdownMenu.Item onClick={openDeletePlaylist}>
|
||||||
|
Delete playlist
|
||||||
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Dropdown>
|
</DropdownMenu.Dropdown>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -33,7 +33,8 @@ export const PlaylistDetailHeader = forwardRef(
|
||||||
{
|
{
|
||||||
id: 'duration',
|
id: 'duration',
|
||||||
secondary: true,
|
secondary: true,
|
||||||
value: detailQuery?.data?.duration && formatDurationString(detailQuery.data.duration),
|
value:
|
||||||
|
detailQuery?.data?.duration && formatDurationString(detailQuery.data.duration),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -175,7 +175,9 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
|
||||||
const columnsInSettings = page.table.columns;
|
const columnsInSettings = page.table.columns;
|
||||||
const updatedColumns = [];
|
const updatedColumns = [];
|
||||||
for (const column of columnsOrder) {
|
for (const column of columnsOrder) {
|
||||||
const columnInSettings = columnsInSettings.find((c) => c.column === column.getColDef().colId);
|
const columnInSettings = columnsInSettings.find(
|
||||||
|
(c) => c.column === column.getColDef().colId,
|
||||||
|
);
|
||||||
|
|
||||||
if (columnInSettings) {
|
if (columnInSettings) {
|
||||||
updatedColumns.push({
|
updatedColumns.push({
|
||||||
|
|
|
@ -57,7 +57,11 @@ const FILTERS = {
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Name', value: SongListSort.NAME },
|
{ defaultOrder: SortOrder.ASC, name: 'Name', value: SongListSort.NAME },
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Random', value: SongListSort.RANDOM },
|
{ defaultOrder: SortOrder.ASC, name: 'Random', value: SongListSort.RANDOM },
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Recently Added', value: SongListSort.RECENTLY_ADDED },
|
{ defaultOrder: SortOrder.ASC, name: 'Recently Added', value: SongListSort.RECENTLY_ADDED },
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Recently Played', value: SongListSort.RECENTLY_PLAYED },
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: 'Recently Played',
|
||||||
|
value: SongListSort.RECENTLY_PLAYED,
|
||||||
|
},
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Release Date', value: SongListSort.RELEASE_DATE },
|
{ defaultOrder: SortOrder.ASC, name: 'Release Date', value: SongListSort.RELEASE_DATE },
|
||||||
],
|
],
|
||||||
navidrome: [
|
navidrome: [
|
||||||
|
@ -74,8 +78,16 @@ const FILTERS = {
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Name', value: SongListSort.NAME },
|
{ defaultOrder: SortOrder.ASC, name: 'Name', value: SongListSort.NAME },
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Play Count', value: SongListSort.PLAY_COUNT },
|
{ defaultOrder: SortOrder.DESC, name: 'Play Count', value: SongListSort.PLAY_COUNT },
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Rating', value: SongListSort.RATING },
|
{ defaultOrder: SortOrder.DESC, name: 'Rating', value: SongListSort.RATING },
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Recently Added', value: SongListSort.RECENTLY_ADDED },
|
{
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Recently Played', value: SongListSort.RECENTLY_PLAYED },
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: 'Recently Added',
|
||||||
|
value: SongListSort.RECENTLY_ADDED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: 'Recently Played',
|
||||||
|
value: SongListSort.RECENTLY_PLAYED,
|
||||||
|
},
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Year', value: SongListSort.YEAR },
|
{ defaultOrder: SortOrder.DESC, name: 'Year', value: SongListSort.YEAR },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -117,7 +129,8 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
||||||
|
|
||||||
const sortByLabel =
|
const sortByLabel =
|
||||||
(server?.type &&
|
(server?.type &&
|
||||||
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filters.sortBy)?.name) ||
|
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filters.sortBy)
|
||||||
|
?.name) ||
|
||||||
'Unknown';
|
'Unknown';
|
||||||
|
|
||||||
const sortOrderLabel = ORDER.find((o) => o.value === filters.sortOrder)?.name || 'Unknown';
|
const sortOrderLabel = ORDER.find((o) => o.value === filters.sortOrder)?.name || 'Unknown';
|
||||||
|
@ -455,7 +468,9 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
clearable
|
clearable
|
||||||
data={SONG_TABLE_COLUMNS}
|
data={SONG_TABLE_COLUMNS}
|
||||||
defaultValue={page.table?.columns.map((column) => column.column)}
|
defaultValue={page.table?.columns.map(
|
||||||
|
(column) => column.column,
|
||||||
|
)}
|
||||||
width={300}
|
width={300}
|
||||||
onChange={handleTableColumns}
|
onChange={handleTableColumns}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -52,7 +52,11 @@ export const PlaylistDetailSongListHeader = ({
|
||||||
py="0.3rem"
|
py="0.3rem"
|
||||||
radius="sm"
|
radius="sm"
|
||||||
>
|
>
|
||||||
{itemCount === null || itemCount === undefined ? <SpinnerIcon /> : itemCount}
|
{itemCount === null || itemCount === undefined ? (
|
||||||
|
<SpinnerIcon />
|
||||||
|
) : (
|
||||||
|
itemCount
|
||||||
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
{isSmartPlaylist && <Badge size="lg">Smart playlist</Badge>}
|
{isSmartPlaylist && <Badge size="lg">Smart playlist</Badge>}
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
|
|
|
@ -90,7 +90,10 @@ export const PlaylistListContent = ({ tableRef, itemCount }: PlaylistListContent
|
||||||
{ cacheTime: 1000 * 60 * 1 },
|
{ cacheTime: 1000 * 60 * 1 },
|
||||||
);
|
);
|
||||||
|
|
||||||
params.successCallback(playlistsRes?.items || [], playlistsRes?.totalRecordCount || 0);
|
params.successCallback(
|
||||||
|
playlistsRes?.items || [],
|
||||||
|
playlistsRes?.totalRecordCount || 0,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
rowCount: undefined,
|
rowCount: undefined,
|
||||||
};
|
};
|
||||||
|
@ -138,7 +141,9 @@ export const PlaylistListContent = ({ tableRef, itemCount }: PlaylistListContent
|
||||||
const columnsInSettings = page.table.columns;
|
const columnsInSettings = page.table.columns;
|
||||||
const updatedColumns = [];
|
const updatedColumns = [];
|
||||||
for (const column of columnsOrder) {
|
for (const column of columnsOrder) {
|
||||||
const columnInSettings = columnsInSettings.find((c) => c.column === column.getColDef().colId);
|
const columnInSettings = columnsInSettings.find(
|
||||||
|
(c) => c.column === column.getColDef().colId,
|
||||||
|
);
|
||||||
|
|
||||||
if (columnInSettings) {
|
if (columnInSettings) {
|
||||||
updatedColumns.push({
|
updatedColumns.push({
|
||||||
|
|
|
@ -58,9 +58,9 @@ export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFilter
|
||||||
|
|
||||||
const sortByLabel =
|
const sortByLabel =
|
||||||
(server?.type &&
|
(server?.type &&
|
||||||
(FILTERS[server.type as keyof typeof FILTERS] as { name: string; value: string }[]).find(
|
(
|
||||||
(f) => f.value === page.filter.sortBy,
|
FILTERS[server.type as keyof typeof FILTERS] as { name: string; value: string }[]
|
||||||
)?.name) ||
|
).find((f) => f.value === page.filter.sortBy)?.name) ||
|
||||||
'Unknown';
|
'Unknown';
|
||||||
|
|
||||||
const sortOrderLabel = ORDER.find((s) => s.value === page.filter.sortOrder)?.name;
|
const sortOrderLabel = ORDER.find((s) => s.value === page.filter.sortOrder)?.name;
|
||||||
|
@ -97,7 +97,10 @@ export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFilter
|
||||||
{ cacheTime: 1000 * 60 * 1 },
|
{ cacheTime: 1000 * 60 * 1 },
|
||||||
);
|
);
|
||||||
|
|
||||||
params.successCallback(playlistsRes?.items || [], playlistsRes?.totalRecordCount || 0);
|
params.successCallback(
|
||||||
|
playlistsRes?.items || [],
|
||||||
|
playlistsRes?.totalRecordCount || 0,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
rowCount: undefined,
|
rowCount: undefined,
|
||||||
};
|
};
|
||||||
|
@ -128,7 +131,8 @@ export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFilter
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleToggleSortOrder = useCallback(() => {
|
const handleToggleSortOrder = useCallback(() => {
|
||||||
const newSortOrder = page.filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
const newSortOrder =
|
||||||
|
page.filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
||||||
const updatedFilters = setFilter({ sortOrder: newSortOrder });
|
const updatedFilters = setFilter({ sortOrder: newSortOrder });
|
||||||
handleFilterChange(updatedFilters);
|
handleFilterChange(updatedFilters);
|
||||||
}, [page.filter.sortOrder, handleFilterChange, setFilter]);
|
}, [page.filter.sortOrder, handleFilterChange, setFilter]);
|
||||||
|
@ -140,7 +144,9 @@ export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFilter
|
||||||
setPage({ list: { ...page, display: e.currentTarget.value as ListDisplayType } });
|
setPage({ list: { ...page, display: e.currentTarget.value as ListDisplayType } });
|
||||||
|
|
||||||
if (display === ListDisplayType.TABLE) {
|
if (display === ListDisplayType.TABLE) {
|
||||||
tableRef.current?.api.paginationSetPageSize(tableRef.current.props.infiniteInitialRowCount);
|
tableRef.current?.api.paginationSetPageSize(
|
||||||
|
tableRef.current.props.infiniteInitialRowCount,
|
||||||
|
);
|
||||||
setPagination({ data: { currentPage: 0 } });
|
setPagination({ data: { currentPage: 0 } });
|
||||||
} else if (display === ListDisplayType.TABLE_PAGINATED) {
|
} else if (display === ListDisplayType.TABLE_PAGINATED) {
|
||||||
setPagination({ data: { currentPage: 0 } });
|
setPagination({ data: { currentPage: 0 } });
|
||||||
|
@ -307,7 +313,9 @@ export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFilter
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
clearable
|
clearable
|
||||||
data={PLAYLIST_TABLE_COLUMNS}
|
data={PLAYLIST_TABLE_COLUMNS}
|
||||||
defaultValue={page.table?.columns.map((column) => column.column)}
|
defaultValue={page.table?.columns.map(
|
||||||
|
(column) => column.column,
|
||||||
|
)}
|
||||||
width={300}
|
width={300}
|
||||||
onChange={handleTableColumns}
|
onChange={handleTableColumns}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -49,7 +49,11 @@ export const PlaylistListHeader = ({ itemCount, tableRef }: PlaylistListHeaderPr
|
||||||
py="0.3rem"
|
py="0.3rem"
|
||||||
radius="sm"
|
radius="sm"
|
||||||
>
|
>
|
||||||
{itemCount === null || itemCount === undefined ? <SpinnerIcon /> : itemCount}
|
{itemCount === null || itemCount === undefined ? (
|
||||||
|
<SpinnerIcon />
|
||||||
|
) : (
|
||||||
|
itemCount
|
||||||
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
<Button onClick={handleCreatePlaylistModal}>Create playlist</Button>
|
<Button onClick={handleCreatePlaylistModal}>Create playlist</Button>
|
||||||
|
|
|
@ -247,7 +247,9 @@ export const PlaylistQueryBuilder = forwardRef(
|
||||||
const updatedFilters = setWith(
|
const updatedFilters = setWith(
|
||||||
filtersCopy,
|
filtersCopy,
|
||||||
path,
|
path,
|
||||||
get(filtersCopy, path).filter((rule: QueryBuilderRule) => rule.uniqueId !== uniqueId),
|
get(filtersCopy, path).filter(
|
||||||
|
(rule: QueryBuilderRule) => rule.uniqueId !== uniqueId,
|
||||||
|
),
|
||||||
clone,
|
clone,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,10 @@ export const usePlaylistSongList = (args: QueryHookArgs<PlaylistSongListQuery>)
|
||||||
enabled: !!server,
|
enabled: !!server,
|
||||||
queryFn: ({ signal }) => {
|
queryFn: ({ signal }) => {
|
||||||
if (!server) throw new Error('Server not found');
|
if (!server) throw new Error('Server not found');
|
||||||
return api.controller.getPlaylistSongList({ apiClientProps: { server, signal }, query });
|
return api.controller.getPlaylistSongList({
|
||||||
|
apiClientProps: { server, signal },
|
||||||
|
query,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
queryKey: queryKeys.playlists.songList(server?.id || '', query.id, query),
|
queryKey: queryKeys.playlists.songList(server?.id || '', query.id, query),
|
||||||
...options,
|
...options,
|
||||||
|
@ -37,7 +40,11 @@ export const usePlaylistSongListInfinite = (args: QueryHookArgs<PlaylistSongList
|
||||||
queryFn: ({ pageParam = 0, signal }) => {
|
queryFn: ({ pageParam = 0, signal }) => {
|
||||||
return api.controller.getPlaylistSongList({
|
return api.controller.getPlaylistSongList({
|
||||||
apiClientProps: { server, signal },
|
apiClientProps: { server, signal },
|
||||||
query: { ...query, limit: query.limit || 50, startIndex: pageParam * (query.limit || 50) },
|
query: {
|
||||||
|
...query,
|
||||||
|
limit: query.limit || 50,
|
||||||
|
startIndex: pageParam * (query.limit || 50),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
queryKey: queryKeys.playlists.detailSongList(server?.id || '', query.id, query),
|
queryKey: queryKeys.playlists.detailSongList(server?.id || '', query.id, query),
|
||||||
|
|
|
@ -50,7 +50,9 @@ const PlaylistDetailRoute = () => {
|
||||||
children: (
|
children: (
|
||||||
<LibraryHeaderBar>
|
<LibraryHeaderBar>
|
||||||
<LibraryHeaderBar.PlayButton onClick={handlePlay} />
|
<LibraryHeaderBar.PlayButton onClick={handlePlay} />
|
||||||
<LibraryHeaderBar.Title>{detailQuery?.data?.name}</LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title>
|
||||||
|
{detailQuery?.data?.name}
|
||||||
|
</LibraryHeaderBar.Title>
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
),
|
),
|
||||||
target: headerRef,
|
target: headerRef,
|
||||||
|
|
|
@ -62,9 +62,14 @@ const PlaylistDetailSongListRoute = () => {
|
||||||
{
|
{
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
toast.success({ message: 'Playlist has been saved' });
|
toast.success({ message: 'Playlist has been saved' });
|
||||||
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: data?.id || '' }), {
|
navigate(
|
||||||
|
generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, {
|
||||||
|
playlistId: data?.id || '',
|
||||||
|
}),
|
||||||
|
{
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
deletePlaylistMutation.mutate({
|
deletePlaylistMutation.mutate({
|
||||||
query: { id: playlistId },
|
query: { id: playlistId },
|
||||||
serverId: detailQuery?.data?.serverId,
|
serverId: detailQuery?.data?.serverId,
|
||||||
|
@ -102,7 +107,11 @@ const PlaylistDetailSongListRoute = () => {
|
||||||
serverId={detailQuery?.data?.serverId}
|
serverId={detailQuery?.data?.serverId}
|
||||||
onCancel={closeAllModals}
|
onCancel={closeAllModals}
|
||||||
onSuccess={(data) =>
|
onSuccess={(data) =>
|
||||||
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: data?.id || '' }))
|
navigate(
|
||||||
|
generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, {
|
||||||
|
playlistId: data?.id || '',
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -133,7 +142,9 @@ const PlaylistDetailSongListRoute = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSmartPlaylist =
|
const isSmartPlaylist =
|
||||||
!detailQuery?.isLoading && detailQuery?.data?.rules && server?.type === ServerType.NAVIDROME;
|
!detailQuery?.isLoading &&
|
||||||
|
detailQuery?.data?.rules &&
|
||||||
|
server?.type === ServerType.NAVIDROME;
|
||||||
|
|
||||||
const [showQueryBuilder, setShowQueryBuilder] = useState(false);
|
const [showQueryBuilder, setShowQueryBuilder] = useState(false);
|
||||||
const [isQueryBuilderExpanded, setIsQueryBuilderExpanded] = useState(false);
|
const [isQueryBuilderExpanded, setIsQueryBuilderExpanded] = useState(false);
|
||||||
|
|
|
@ -147,7 +147,11 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
||||||
key={`search-album-${album.id}`}
|
key={`search-album-${album.id}`}
|
||||||
value={`search-${album.id}`}
|
value={`search-${album.id}`}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: album.id }));
|
navigate(
|
||||||
|
generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
|
||||||
|
albumId: album.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
modalProps.handlers.close();
|
modalProps.handlers.close();
|
||||||
setQuery('');
|
setQuery('');
|
||||||
}}
|
}}
|
||||||
|
@ -157,7 +161,9 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
||||||
id={album.id}
|
id={album.id}
|
||||||
imageUrl={album.imageUrl}
|
imageUrl={album.imageUrl}
|
||||||
itemType={LibraryItem.ALBUM}
|
itemType={LibraryItem.ALBUM}
|
||||||
subtitle={album.albumArtists.map((artist) => artist.name).join(', ')}
|
subtitle={album.albumArtists
|
||||||
|
.map((artist) => artist.name)
|
||||||
|
.join(', ')}
|
||||||
title={album.name}
|
title={album.name}
|
||||||
/>
|
/>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
|
@ -186,7 +192,9 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
||||||
imageUrl={artist.imageUrl}
|
imageUrl={artist.imageUrl}
|
||||||
itemType={LibraryItem.ALBUM_ARTIST}
|
itemType={LibraryItem.ALBUM_ARTIST}
|
||||||
subtitle={
|
subtitle={
|
||||||
(artist?.albumCount || 0) > 0 ? `${artist.albumCount} albums` : undefined
|
(artist?.albumCount || 0) > 0
|
||||||
|
? `${artist.albumCount} albums`
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
title={artist.name}
|
title={artist.name}
|
||||||
/>
|
/>
|
||||||
|
@ -215,7 +223,9 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
||||||
id={song.id}
|
id={song.id}
|
||||||
imageUrl={song.imageUrl}
|
imageUrl={song.imageUrl}
|
||||||
itemType={LibraryItem.SONG}
|
itemType={LibraryItem.SONG}
|
||||||
subtitle={song.artists.map((artist) => artist.name).join(', ')}
|
subtitle={song.artists
|
||||||
|
.map((artist) => artist.name)
|
||||||
|
.join(', ')}
|
||||||
title={song.name}
|
title={song.name}
|
||||||
/>
|
/>
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
|
@ -252,7 +262,9 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
||||||
p="0.5rem"
|
p="0.5rem"
|
||||||
>
|
>
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Command.Loading>{isHome && isLoading && query !== '' && <Spinner />}</Command.Loading>
|
<Command.Loading>
|
||||||
|
{isHome && isLoading && query !== '' && <Spinner />}
|
||||||
|
</Command.Loading>
|
||||||
<Group spacing="sm">
|
<Group spacing="sm">
|
||||||
<Kbd size="md">ESC</Kbd>
|
<Kbd size="md">ESC</Kbd>
|
||||||
<Kbd size="md">↑</Kbd>
|
<Kbd size="md">↑</Kbd>
|
||||||
|
|
|
@ -69,7 +69,9 @@ export const HomeCommands = ({
|
||||||
<Command.Item onSelect={() => setPages([...pages, CommandPalettePages.GO_TO])}>
|
<Command.Item onSelect={() => setPages([...pages, CommandPalettePages.GO_TO])}>
|
||||||
Go to page...
|
Go to page...
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
<Command.Item onSelect={() => setPages([...pages, CommandPalettePages.MANAGE_SERVERS])}>
|
<Command.Item
|
||||||
|
onSelect={() => setPages([...pages, CommandPalettePages.MANAGE_SERVERS])}
|
||||||
|
>
|
||||||
Server commands...
|
Server commands...
|
||||||
</Command.Item>
|
</Command.Item>
|
||||||
</Command.Group>
|
</Command.Group>
|
||||||
|
|
|
@ -103,7 +103,11 @@ export const SearchContent = ({ tableRef, getDatasource }: SearchContentProps) =
|
||||||
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id }));
|
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id }));
|
||||||
break;
|
break;
|
||||||
case LibraryItem.ALBUM_ARTIST:
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
navigate(generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { albumArtistId: e.data.id }));
|
navigate(
|
||||||
|
generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||||
|
albumArtistId: e.data.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case LibraryItem.SONG:
|
case LibraryItem.SONG:
|
||||||
handlePlayQueueAdd?.({
|
handlePlayQueueAdd?.({
|
||||||
|
|
|
@ -77,7 +77,9 @@ export const SearchHeader = ({ tableRef, getDatasource, navigationId }: SearchHe
|
||||||
size="md"
|
size="md"
|
||||||
state={{ navigationId }}
|
state={{ navigationId }}
|
||||||
to={{
|
to={{
|
||||||
pathname: generatePath(AppRoute.SEARCH, { itemType: LibraryItem.ALBUM }),
|
pathname: generatePath(AppRoute.SEARCH, {
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
}),
|
||||||
search: searchParams.toString(),
|
search: searchParams.toString(),
|
||||||
}}
|
}}
|
||||||
variant={itemType === LibraryItem.ALBUM ? 'filled' : 'subtle'}
|
variant={itemType === LibraryItem.ALBUM ? 'filled' : 'subtle'}
|
||||||
|
@ -92,7 +94,9 @@ export const SearchHeader = ({ tableRef, getDatasource, navigationId }: SearchHe
|
||||||
size="md"
|
size="md"
|
||||||
state={{ navigationId }}
|
state={{ navigationId }}
|
||||||
to={{
|
to={{
|
||||||
pathname: generatePath(AppRoute.SEARCH, { itemType: LibraryItem.ALBUM_ARTIST }),
|
pathname: generatePath(AppRoute.SEARCH, {
|
||||||
|
itemType: LibraryItem.ALBUM_ARTIST,
|
||||||
|
}),
|
||||||
search: searchParams.toString(),
|
search: searchParams.toString(),
|
||||||
}}
|
}}
|
||||||
variant={itemType === LibraryItem.ALBUM_ARTIST ? 'filled' : 'subtle'}
|
variant={itemType === LibraryItem.ALBUM_ARTIST ? 'filled' : 'subtle'}
|
||||||
|
|
|
@ -77,7 +77,9 @@ export const ControlSettings = () => {
|
||||||
...settings,
|
...settings,
|
||||||
skipButtons: {
|
skipButtons: {
|
||||||
...settings.skipButtons,
|
...settings.skipButtons,
|
||||||
skipForwardSeconds: e.currentTarget.value ? Number(e.currentTarget.value) : 0,
|
skipForwardSeconds: e.currentTarget.value
|
||||||
|
? Number(e.currentTarget.value)
|
||||||
|
: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
@ -147,7 +149,8 @@ export const ControlSettings = () => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
description: 'Display a hover icon on the right side of the application view the play queue',
|
description:
|
||||||
|
'Display a hover icon on the right side of the application view the play queue',
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
title: 'Show floating queue hover area',
|
title: 'Show floating queue hover area',
|
||||||
},
|
},
|
||||||
|
|
|
@ -199,13 +199,22 @@ export const HotkeyManagerSettings = () => {
|
||||||
/>
|
/>
|
||||||
{isElectron() && (
|
{isElectron() && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={bindings[binding as keyof typeof BINDINGS_MAP].isGlobal}
|
checked={
|
||||||
disabled={bindings[binding as keyof typeof BINDINGS_MAP].hotkey === ''}
|
bindings[binding as keyof typeof BINDINGS_MAP].isGlobal
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
bindings[binding as keyof typeof BINDINGS_MAP].hotkey === ''
|
||||||
|
}
|
||||||
size="xl"
|
size="xl"
|
||||||
style={{
|
style={{
|
||||||
opacity: bindings[binding as keyof typeof BINDINGS_MAP].allowGlobal ? 1 : 0,
|
opacity: bindings[binding as keyof typeof BINDINGS_MAP]
|
||||||
|
.allowGlobal
|
||||||
|
? 1
|
||||||
|
: 0,
|
||||||
}}
|
}}
|
||||||
onChange={(e) => handleSetGlobalHotkey(binding as BindingActions, e)}
|
onChange={(e) =>
|
||||||
|
handleSetGlobalHotkey(binding as BindingActions, e)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -27,7 +27,9 @@ export const AudioSettings = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getAudioDevices = () => {
|
const getAudioDevices = () => {
|
||||||
getAudioDevice()
|
getAudioDevice()
|
||||||
.then((dev) => setAudioDevices(dev.map((d) => ({ label: d.label, value: d.deviceId }))))
|
.then((dev) =>
|
||||||
|
setAudioDevices(dev.map((d) => ({ label: d.label, value: d.deviceId }))),
|
||||||
|
)
|
||||||
.catch(() => toast.error({ message: 'Error fetching audio devices' }));
|
.catch(() => toast.error({ message: 'Error fetching audio devices' }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -87,7 +89,9 @@ export const AudioSettings = () => {
|
||||||
]}
|
]}
|
||||||
defaultValue={settings.style}
|
defaultValue={settings.style}
|
||||||
disabled={settings.type !== PlaybackType.WEB || status === PlayerStatus.PLAYING}
|
disabled={settings.type !== PlaybackType.WEB || status === PlayerStatus.PLAYING}
|
||||||
onChange={(e) => setSettings({ playback: { ...settings, style: e as PlaybackStyle } })}
|
onChange={(e) =>
|
||||||
|
setSettings({ playback: { ...settings, style: e as PlaybackStyle } })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
description: 'Adjust the playback style (web player only)',
|
description: 'Adjust the playback style (web player only)',
|
||||||
|
@ -107,7 +111,9 @@ export const AudioSettings = () => {
|
||||||
max={15}
|
max={15}
|
||||||
min={0}
|
min={0}
|
||||||
w={100}
|
w={100}
|
||||||
onChangeEnd={(e) => setSettings({ playback: { ...settings, crossfadeDuration: e } })}
|
onChangeEnd={(e) =>
|
||||||
|
setSettings({ playback: { ...settings, crossfadeDuration: e } })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
description: 'Adjust the crossfade duration (web player only)',
|
description: 'Adjust the crossfade duration (web player only)',
|
||||||
|
|
|
@ -43,7 +43,8 @@ export const getMpvSetting = (
|
||||||
export const getMpvProperties = (settings: SettingsState['playback']['mpvProperties']) => {
|
export const getMpvProperties = (settings: SettingsState['playback']['mpvProperties']) => {
|
||||||
const properties: Record<string, any> = {
|
const properties: Record<string, any> = {
|
||||||
'audio-exclusive': settings.audioExclusiveMode || 'no',
|
'audio-exclusive': settings.audioExclusiveMode || 'no',
|
||||||
'audio-samplerate': settings.audioSampleRateHz === 0 ? undefined : settings.audioSampleRateHz,
|
'audio-samplerate':
|
||||||
|
settings.audioSampleRateHz === 0 ? undefined : settings.audioSampleRateHz,
|
||||||
'gapless-audio': settings.gaplessAudio || 'weak',
|
'gapless-audio': settings.gaplessAudio || 'weak',
|
||||||
replaygain: settings.replayGainMode || 'no',
|
replaygain: settings.replayGainMode || 'no',
|
||||||
'replaygain-clip': settings.replayGainClip || 'no',
|
'replaygain-clip': settings.replayGainClip || 'no',
|
||||||
|
@ -127,7 +128,9 @@ export const MpvSettings = () => {
|
||||||
autosize
|
autosize
|
||||||
defaultValue={settings.mpvExtraParameters.join('\n')}
|
defaultValue={settings.mpvExtraParameters.join('\n')}
|
||||||
minRows={4}
|
minRows={4}
|
||||||
placeholder={'(Add one per line):\n--gapless-audio=weak\n--prefetch-playlist=yes'}
|
placeholder={
|
||||||
|
'(Add one per line):\n--gapless-audio=weak\n--prefetch-playlist=yes'
|
||||||
|
}
|
||||||
width={225}
|
width={225}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
handleSetExtraParameters(e.currentTarget.value.split('\n'));
|
handleSetExtraParameters(e.currentTarget.value.split('\n'));
|
||||||
|
@ -200,7 +203,10 @@ export const MpvSettings = () => {
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={settings.mpvProperties.audioExclusiveMode === 'yes'}
|
defaultChecked={settings.mpvProperties.audioExclusiveMode === 'yes'}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleSetMpvProperty('audioExclusiveMode', e.currentTarget.checked ? 'yes' : 'no')
|
handleSetMpvProperty(
|
||||||
|
'audioExclusiveMode',
|
||||||
|
e.currentTarget.checked ? 'yes' : 'no',
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -248,7 +254,9 @@ export const MpvSettings = () => {
|
||||||
control: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
defaultChecked={settings.mpvProperties.replayGainClip}
|
defaultChecked={settings.mpvProperties.replayGainClip}
|
||||||
onChange={(e) => handleSetMpvProperty('replayGainClip', e.currentTarget.checked)}
|
onChange={(e) =>
|
||||||
|
handleSetMpvProperty('replayGainClip', e.currentTarget.checked)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -52,7 +52,8 @@ export const ScrobbleSettings = () => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
description: 'The percentage of the song that must be played before submitting a scrobble',
|
description:
|
||||||
|
'The percentage of the song that must be played before submitting a scrobble',
|
||||||
isHidden: !isElectron(),
|
isHidden: !isElectron(),
|
||||||
title: 'Minimum scrobble percentage*',
|
title: 'Minimum scrobble percentage*',
|
||||||
},
|
},
|
||||||
|
|
|
@ -31,7 +31,9 @@ export const WindowSettings = () => {
|
||||||
if (!e) return;
|
if (!e) return;
|
||||||
|
|
||||||
// Platform.LINUX is used as the native frame option regardless of the actual platform
|
// Platform.LINUX is used as the native frame option regardless of the actual platform
|
||||||
const hasFrame = localSettings?.get('window_has_frame') as boolean | undefined;
|
const hasFrame = localSettings?.get('window_has_frame') as
|
||||||
|
| boolean
|
||||||
|
| undefined;
|
||||||
const isSwitchingToFrame = !hasFrame && e === Platform.LINUX;
|
const isSwitchingToFrame = !hasFrame && e === Platform.LINUX;
|
||||||
const isSwitchingToNoFrame = hasFrame && e !== Platform.LINUX;
|
const isSwitchingToNoFrame = hasFrame && e !== Platform.LINUX;
|
||||||
|
|
||||||
|
@ -41,14 +43,20 @@ export const WindowSettings = () => {
|
||||||
toast.info({
|
toast.info({
|
||||||
autoClose: false,
|
autoClose: false,
|
||||||
id: 'restart-toast',
|
id: 'restart-toast',
|
||||||
message: 'Restart to apply changes... close the notification to restart Feishin',
|
message:
|
||||||
|
'Restart to apply changes... close the notification to restart Feishin',
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
window.electron.ipc.send('app-restart');
|
window.electron.ipc.send('app-restart');
|
||||||
},
|
},
|
||||||
title: 'Restart required',
|
title: 'Restart required',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toast.update({ autoClose: 0, id: 'restart-toast', message: '', onClose: () => {} }); // clean old toasts
|
toast.update({
|
||||||
|
autoClose: 0,
|
||||||
|
id: 'restart-toast',
|
||||||
|
message: '',
|
||||||
|
onClose: () => {},
|
||||||
|
}); // clean old toasts
|
||||||
}
|
}
|
||||||
|
|
||||||
localSettings?.set('window_window_bar_style', e as Platform);
|
localSettings?.set('window_window_bar_style', e as Platform);
|
||||||
|
|
|
@ -66,7 +66,8 @@ const BackgroundImageOverlay = styled.div`
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(180deg, rgba(25, 26, 28, 5%), var(--main-bg)), var(--background-noise);
|
background: linear-gradient(180deg, rgba(25, 26, 28, 5%), var(--main-bg)),
|
||||||
|
var(--background-noise);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface LibraryHeaderProps {
|
interface LibraryHeaderProps {
|
||||||
|
|
|
@ -59,7 +59,10 @@ export const useCreateFavorite = (args: MutationHookArgs) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// We only need to set if we're already on the album detail page
|
// We only need to set if we're already on the album detail page
|
||||||
if (variables.query.type === LibraryItem.ALBUM_ARTIST && variables.query.id.length === 1) {
|
if (
|
||||||
|
variables.query.type === LibraryItem.ALBUM_ARTIST &&
|
||||||
|
variables.query.id.length === 1
|
||||||
|
) {
|
||||||
const queryKey = queryKeys.albumArtists.detail(serverId, {
|
const queryKey = queryKeys.albumArtists.detail(serverId, {
|
||||||
id: variables.query.id[0],
|
id: variables.query.id[0],
|
||||||
});
|
});
|
||||||
|
|
|
@ -59,7 +59,10 @@ export const useDeleteFavorite = (args: MutationHookArgs) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// We only need to set if we're already on the album artist detail page
|
// We only need to set if we're already on the album artist detail page
|
||||||
if (variables.query.type === LibraryItem.ALBUM_ARTIST && variables.query.id.length === 1) {
|
if (
|
||||||
|
variables.query.type === LibraryItem.ALBUM_ARTIST &&
|
||||||
|
variables.query.id.length === 1
|
||||||
|
) {
|
||||||
const queryKey = queryKeys.albumArtists.detail(serverId, {
|
const queryKey = queryKeys.albumArtists.detail(serverId, {
|
||||||
id: variables.query.id[0],
|
id: variables.query.id[0],
|
||||||
});
|
});
|
||||||
|
|
|
@ -61,7 +61,8 @@ export const useSetRating = (args: MutationHookArgs) => {
|
||||||
onSuccess: (_data, variables) => {
|
onSuccess: (_data, variables) => {
|
||||||
// We only need to set if we're already on the album detail page
|
// We only need to set if we're already on the album detail page
|
||||||
const isAlbumDetailPage =
|
const isAlbumDetailPage =
|
||||||
variables.query.item.length === 1 && variables.query.item[0].itemType === LibraryItem.ALBUM;
|
variables.query.item.length === 1 &&
|
||||||
|
variables.query.item[0].itemType === LibraryItem.ALBUM;
|
||||||
|
|
||||||
if (isAlbumDetailPage) {
|
if (isAlbumDetailPage) {
|
||||||
const { id: albumId, serverId } = variables.query.item[0] as Album;
|
const { id: albumId, serverId } = variables.query.item[0] as Album;
|
||||||
|
|
|
@ -146,7 +146,11 @@ export const SidebarPlaylistList = ({ data }: SidebarPlaylistListProps) => {
|
||||||
<AutoSizer onResize={(e) => setRect(e as { height: number; width: number })}>
|
<AutoSizer onResize={(e) => setRect(e as { height: number; width: number })}>
|
||||||
{() => (
|
{() => (
|
||||||
<FixedSizeList
|
<FixedSizeList
|
||||||
className={isScrollbarHidden ? 'hide-scrollbar overlay-scrollbar' : 'overlay-scrollbar'}
|
className={
|
||||||
|
isScrollbarHidden
|
||||||
|
? 'hide-scrollbar overlay-scrollbar'
|
||||||
|
: 'overlay-scrollbar'
|
||||||
|
}
|
||||||
height={debounced.height}
|
height={debounced.height}
|
||||||
itemCount={data?.items?.length || 0}
|
itemCount={data?.items?.length || 0}
|
||||||
itemData={memoizedItemData}
|
itemData={memoizedItemData}
|
||||||
|
|
|
@ -298,7 +298,9 @@ export const Sidebar = () => {
|
||||||
src={upsizedImageUrl}
|
src={upsizedImageUrl}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Center sx={{ background: 'var(--placeholder-bg)', height: '100%' }}>
|
<Center
|
||||||
|
sx={{ background: 'var(--placeholder-bg)', height: '100%' }}
|
||||||
|
>
|
||||||
<RiDiscLine
|
<RiDiscLine
|
||||||
color="var(--placeholder-fg)"
|
color="var(--placeholder-fg)"
|
||||||
size={50}
|
size={50}
|
||||||
|
|
|
@ -94,7 +94,8 @@ export const SongListContent = ({ itemCount, tableRef }: SongListContentProps) =
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Scroll to top of page on pagination change
|
// Scroll to top of page on pagination change
|
||||||
const currentPageStartIndex = table.pagination.currentPage * table.pagination.itemsPerPage;
|
const currentPageStartIndex =
|
||||||
|
table.pagination.currentPage * table.pagination.itemsPerPage;
|
||||||
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
|
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
@ -132,7 +133,9 @@ export const SongListContent = ({ itemCount, tableRef }: SongListContentProps) =
|
||||||
const columnsInSettings = table.columns;
|
const columnsInSettings = table.columns;
|
||||||
const updatedColumns = [];
|
const updatedColumns = [];
|
||||||
for (const column of columnsOrder) {
|
for (const column of columnsOrder) {
|
||||||
const columnInSettings = columnsInSettings.find((c) => c.column === column.getColDef().colId);
|
const columnInSettings = columnsInSettings.find(
|
||||||
|
(c) => c.column === column.getColDef().colId,
|
||||||
|
);
|
||||||
|
|
||||||
if (columnInSettings) {
|
if (columnInSettings) {
|
||||||
updatedColumns.push({
|
updatedColumns.push({
|
||||||
|
|
|
@ -45,7 +45,11 @@ const FILTERS = {
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Name', value: SongListSort.NAME },
|
{ defaultOrder: SortOrder.ASC, name: 'Name', value: SongListSort.NAME },
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Random', value: SongListSort.RANDOM },
|
{ defaultOrder: SortOrder.ASC, name: 'Random', value: SongListSort.RANDOM },
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Recently Added', value: SongListSort.RECENTLY_ADDED },
|
{ defaultOrder: SortOrder.ASC, name: 'Recently Added', value: SongListSort.RECENTLY_ADDED },
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Recently Played', value: SongListSort.RECENTLY_PLAYED },
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: 'Recently Played',
|
||||||
|
value: SongListSort.RECENTLY_PLAYED,
|
||||||
|
},
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Release Date', value: SongListSort.RELEASE_DATE },
|
{ defaultOrder: SortOrder.ASC, name: 'Release Date', value: SongListSort.RELEASE_DATE },
|
||||||
],
|
],
|
||||||
navidrome: [
|
navidrome: [
|
||||||
|
@ -61,8 +65,16 @@ const FILTERS = {
|
||||||
{ defaultOrder: SortOrder.ASC, name: 'Name', value: SongListSort.NAME },
|
{ defaultOrder: SortOrder.ASC, name: 'Name', value: SongListSort.NAME },
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Play Count', value: SongListSort.PLAY_COUNT },
|
{ defaultOrder: SortOrder.DESC, name: 'Play Count', value: SongListSort.PLAY_COUNT },
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Rating', value: SongListSort.RATING },
|
{ defaultOrder: SortOrder.DESC, name: 'Rating', value: SongListSort.RATING },
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Recently Added', value: SongListSort.RECENTLY_ADDED },
|
{
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Recently Played', value: SongListSort.RECENTLY_PLAYED },
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: 'Recently Added',
|
||||||
|
value: SongListSort.RECENTLY_ADDED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.DESC,
|
||||||
|
name: 'Recently Played',
|
||||||
|
value: SongListSort.RECENTLY_PLAYED,
|
||||||
|
},
|
||||||
{ defaultOrder: SortOrder.DESC, name: 'Year', value: SongListSort.YEAR },
|
{ defaultOrder: SortOrder.DESC, name: 'Year', value: SongListSort.YEAR },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -89,9 +101,9 @@ export const SongListHeaderFilters = ({ tableRef }: SongListHeaderFiltersProps)
|
||||||
|
|
||||||
const sortByLabel =
|
const sortByLabel =
|
||||||
(server?.type &&
|
(server?.type &&
|
||||||
(FILTERS[server.type as keyof typeof FILTERS] as { name: string; value: string }[]).find(
|
(
|
||||||
(f) => f.value === filter.sortBy,
|
FILTERS[server.type as keyof typeof FILTERS] as { name: string; value: string }[]
|
||||||
)?.name) ||
|
).find((f) => f.value === filter.sortBy)?.name) ||
|
||||||
'Unknown';
|
'Unknown';
|
||||||
|
|
||||||
const sortOrderLabel = ORDER.find((s) => s.value === filter.sortOrder)?.name;
|
const sortOrderLabel = ORDER.find((s) => s.value === filter.sortOrder)?.name;
|
||||||
|
@ -204,7 +216,9 @@ export const SongListHeaderFilters = ({ tableRef }: SongListHeaderFiltersProps)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (display === ListDisplayType.TABLE) {
|
if (display === ListDisplayType.TABLE) {
|
||||||
tableRef.current?.api.paginationSetPageSize(tableRef.current.props.infiniteInitialRowCount);
|
tableRef.current?.api.paginationSetPageSize(
|
||||||
|
tableRef.current.props.infiniteInitialRowCount,
|
||||||
|
);
|
||||||
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
|
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
|
||||||
} else if (display === ListDisplayType.TABLE_PAGINATED) {
|
} else if (display === ListDisplayType.TABLE_PAGINATED) {
|
||||||
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
|
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
|
||||||
|
@ -418,7 +432,11 @@ export const SongListHeaderFilters = ({ tableRef }: SongListHeaderFiltersProps)
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
size="md"
|
size="md"
|
||||||
sx={{ svg: { fill: isFilterApplied ? 'var(--primary-color) !important' : undefined } }}
|
sx={{
|
||||||
|
svg: {
|
||||||
|
fill: isFilterApplied ? 'var(--primary-color) !important' : undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
tooltip={{ label: 'Filters' }}
|
tooltip={{ label: 'Filters' }}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={handleOpenFiltersModal}
|
onClick={handleOpenFiltersModal}
|
||||||
|
|
|
@ -103,7 +103,9 @@ export const SongListHeader = ({ title, itemCount, tableRef }: SongListHeaderPro
|
||||||
onClick={() => handlePlay?.({ playType: playButtonBehavior })}
|
onClick={() => handlePlay?.({ playType: playButtonBehavior })}
|
||||||
/>
|
/>
|
||||||
<LibraryHeaderBar.Title>{title || 'Tracks'}</LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title>{title || 'Tracks'}</LibraryHeaderBar.Title>
|
||||||
<LibraryHeaderBar.Badge isLoading={itemCount === null || itemCount === undefined}>
|
<LibraryHeaderBar.Badge
|
||||||
|
isLoading={itemCount === null || itemCount === undefined}
|
||||||
|
>
|
||||||
{itemCount}
|
{itemCount}
|
||||||
</LibraryHeaderBar.Badge>
|
</LibraryHeaderBar.Badge>
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
|
|
|
@ -139,7 +139,8 @@ export const AppMenu = () => {
|
||||||
<DropdownMenu.Label>Select a server</DropdownMenu.Label>
|
<DropdownMenu.Label>Select a server</DropdownMenu.Label>
|
||||||
{Object.keys(serverList).map((serverId) => {
|
{Object.keys(serverList).map((serverId) => {
|
||||||
const server = serverList[serverId];
|
const server = serverList[serverId];
|
||||||
const isNavidromeExpired = server.type === ServerType.NAVIDROME && !server.ndCredential;
|
const isNavidromeExpired =
|
||||||
|
server.type === ServerType.NAVIDROME && !server.ndCredential;
|
||||||
const isJellyfinExpired = false;
|
const isJellyfinExpired = false;
|
||||||
const isSessionExpired = isNavidromeExpired || isJellyfinExpired;
|
const isSessionExpired = isNavidromeExpired || isJellyfinExpired;
|
||||||
|
|
||||||
|
@ -147,7 +148,13 @@ export const AppMenu = () => {
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
key={`server-${server.id}`}
|
key={`server-${server.id}`}
|
||||||
$isActive={server.id === currentServer?.id}
|
$isActive={server.id === currentServer?.id}
|
||||||
icon={isSessionExpired ? <RiLockLine color="var(--danger-color)" /> : <RiServerLine />}
|
icon={
|
||||||
|
isSessionExpired ? (
|
||||||
|
<RiLockLine color="var(--danger-color)" />
|
||||||
|
) : (
|
||||||
|
<RiServerLine />
|
||||||
|
)
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isSessionExpired) return handleSetCurrentServer(server);
|
if (!isSessionExpired) return handleSetCurrentServer(server);
|
||||||
return handleCredentialsModal(server);
|
return handleCredentialsModal(server);
|
||||||
|
|
|
@ -12,8 +12,7 @@ export const useFastAverageColor = (
|
||||||
const fac = new FastAverageColor();
|
const fac = new FastAverageColor();
|
||||||
|
|
||||||
if (src && srcLoaded) {
|
if (src && srcLoaded) {
|
||||||
fac
|
fac.getColorAsync(src, {
|
||||||
.getColorAsync(src, {
|
|
||||||
algorithm: aglorithm || 'dominant',
|
algorithm: aglorithm || 'dominant',
|
||||||
ignoredColor: [
|
ignoredColor: [
|
||||||
[255, 255, 255, 255, 55], // White
|
[255, 255, 255, 255, 55], // White
|
||||||
|
|
|
@ -74,7 +74,9 @@ export const MainContent = ({ shell }: { shell?: boolean }) => {
|
||||||
} else if (isResizingRight) {
|
} else if (isResizingRight) {
|
||||||
const start = Number(rightWidth.split('px')[0]);
|
const start = Number(rightWidth.split('px')[0]);
|
||||||
const { left } = rightSidebarRef!.current!.getBoundingClientRect();
|
const { left } = rightSidebarRef!.current!.getBoundingClientRect();
|
||||||
const width = `${constrainRightSidebarWidth(start + left - mouseMoveEvent.clientX)}px`;
|
const width = `${constrainRightSidebarWidth(
|
||||||
|
start + left - mouseMoveEvent.clientX,
|
||||||
|
)}px`;
|
||||||
setSideBar({ rightWidth: width });
|
setSideBar({ rightWidth: width });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -103,7 +105,9 @@ export const MainContent = ({ shell }: { shell?: boolean }) => {
|
||||||
>
|
>
|
||||||
{!shell && (
|
{!shell && (
|
||||||
<>
|
<>
|
||||||
<Suspense fallback={<></>}>{showQueueDrawerButton && <SideDrawerQueue />}</Suspense>
|
<Suspense fallback={<></>}>
|
||||||
|
{showQueueDrawerButton && <SideDrawerQueue />}
|
||||||
|
</Suspense>
|
||||||
<FullScreenOverlay />
|
<FullScreenOverlay />
|
||||||
<LeftSidebar
|
<LeftSidebar
|
||||||
isResizing={isResizing}
|
isResizing={isResizing}
|
||||||
|
|
|
@ -84,7 +84,10 @@ interface RightSidebarProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RightSidebar = forwardRef(
|
export const RightSidebar = forwardRef(
|
||||||
({ isResizing: isResizingRight, startResizing }: RightSidebarProps, ref: Ref<HTMLDivElement>) => {
|
(
|
||||||
|
{ isResizing: isResizingRight, startResizing }: RightSidebarProps,
|
||||||
|
ref: Ref<HTMLDivElement>,
|
||||||
|
) => {
|
||||||
const { windowBarStyle } = useWindowSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
const { rightWidth, rightExpanded } = useSidebarStore();
|
const { rightWidth, rightExpanded } = useSidebarStore();
|
||||||
const { sideQueueType } = useGeneralSettings();
|
const { sideQueueType } = useGeneralSettings();
|
||||||
|
|
|
@ -140,7 +140,8 @@ export const MacOsButton = styled.div<{
|
||||||
restoreButton?: boolean;
|
restoreButton?: boolean;
|
||||||
}>`
|
}>`
|
||||||
grid-row: 1 / span 1;
|
grid-row: 1 / span 1;
|
||||||
grid-column: ${(props) => (props.minButton ? 2 : props.maxButton || props.restoreButton ? 3 : 1)};
|
grid-column: ${(props) =>
|
||||||
|
props.minButton ? 2 : props.maxButton || props.restoreButton ? 3 : 1};
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
8
src/renderer/preload.d.ts
vendored
8
src/renderer/preload.d.ts
vendored
|
@ -38,13 +38,17 @@ declare global {
|
||||||
PLAYER_STOP(): void;
|
PLAYER_STOP(): void;
|
||||||
PLAYER_VOLUME(value: number): void;
|
PLAYER_VOLUME(value: number): void;
|
||||||
RENDERER_PLAYER_AUTO_NEXT(cb: (event: IpcRendererEvent, data: any) => void): void;
|
RENDERER_PLAYER_AUTO_NEXT(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||||
RENDERER_PLAYER_CURRENT_TIME(cb: (event: IpcRendererEvent, data: any) => void): void;
|
RENDERER_PLAYER_CURRENT_TIME(
|
||||||
|
cb: (event: IpcRendererEvent, data: any) => void,
|
||||||
|
): void;
|
||||||
RENDERER_PLAYER_NEXT(cb: (event: IpcRendererEvent, data: any) => void): void;
|
RENDERER_PLAYER_NEXT(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||||
RENDERER_PLAYER_PAUSE(cb: (event: IpcRendererEvent, data: any) => void): void;
|
RENDERER_PLAYER_PAUSE(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||||
RENDERER_PLAYER_PLAY(cb: (event: IpcRendererEvent, data: any) => void): void;
|
RENDERER_PLAYER_PLAY(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||||
RENDERER_PLAYER_PLAY_PAUSE(cb: (event: IpcRendererEvent, data: any) => void): void;
|
RENDERER_PLAYER_PLAY_PAUSE(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||||
RENDERER_PLAYER_PREVIOUS(cb: (event: IpcRendererEvent, data: any) => void): void;
|
RENDERER_PLAYER_PREVIOUS(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||||
RENDERER_PLAYER_RESTORE_QUEUE(cb: (event: IpcRendererEvent, data: any) => void): void;
|
RENDERER_PLAYER_RESTORE_QUEUE(
|
||||||
|
cb: (event: IpcRendererEvent, data: any) => void,
|
||||||
|
): void;
|
||||||
RENDERER_PLAYER_SAVE_QUEUE(cb: (event: IpcRendererEvent, data: any) => void): void;
|
RENDERER_PLAYER_SAVE_QUEUE(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||||
RENDERER_PLAYER_STOP(cb: (event: IpcRendererEvent, data: any) => void): void;
|
RENDERER_PLAYER_STOP(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||||
SETTINGS_GET(data: { property: string }): any;
|
SETTINGS_GET(data: { property: string }): any;
|
||||||
|
|
|
@ -58,7 +58,10 @@ export const useAlbumArtistStore = create<AlbumArtistSlice>()(
|
||||||
},
|
},
|
||||||
setTablePagination: (data) => {
|
setTablePagination: (data) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.list.table.pagination = { ...state.list.table.pagination, ...data };
|
state.list.table.pagination = {
|
||||||
|
...state.list.table.pagination,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -93,7 +93,10 @@ export const useListStore = create<ListSlice>()(
|
||||||
...state.detail[args.key]?.filter?._custom,
|
...state.detail[args.key]?.filter?._custom,
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
...state.detail[args.key]?.filter?._custom?.jellyfin,
|
...state.detail[args.key]?.filter?._custom?.jellyfin,
|
||||||
includeItemTypes: args?.itemType === LibraryItem.ALBUM ? 'MusicAlbum' : 'Audio',
|
includeItemTypes:
|
||||||
|
args?.itemType === LibraryItem.ALBUM
|
||||||
|
? 'MusicAlbum'
|
||||||
|
: 'Audio',
|
||||||
},
|
},
|
||||||
navidrome: {
|
navidrome: {
|
||||||
...state.detail[args.key]?.filter?._custom?.navidrome,
|
...state.detail[args.key]?.filter?._custom?.navidrome,
|
||||||
|
@ -160,7 +163,11 @@ export const useListStore = create<ListSlice>()(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return get()._actions.getFilter({ id, itemType: args.itemType, key: args.key });
|
return get()._actions.getFilter({
|
||||||
|
id,
|
||||||
|
itemType: args.itemType,
|
||||||
|
key: args.key,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
setGrid: (args) => {
|
setGrid: (args) => {
|
||||||
const [page, id] = args.key.split('_');
|
const [page, id] = args.key.split('_');
|
||||||
|
@ -172,7 +179,8 @@ export const useListStore = create<ListSlice>()(
|
||||||
filter: {} as FilterType,
|
filter: {} as FilterType,
|
||||||
grid: {
|
grid: {
|
||||||
itemsPerRow:
|
itemsPerRow:
|
||||||
state.item[page as keyof ListState['item']].grid?.itemsPerRow || 5,
|
state.item[page as keyof ListState['item']].grid
|
||||||
|
?.itemsPerRow || 5,
|
||||||
scrollOffset: 0,
|
scrollOffset: 0,
|
||||||
},
|
},
|
||||||
table: {
|
table: {
|
||||||
|
@ -266,13 +274,16 @@ export const useListStore = create<ListSlice>()(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
state.detail[args.key as keyof ListState['item']].table.pagination = {
|
state.detail[args.key as keyof ListState['item']].table.pagination =
|
||||||
...state.detail[args.key as keyof ListState['item']].table.pagination,
|
{
|
||||||
|
...state.detail[args.key as keyof ListState['item']].table
|
||||||
|
.pagination,
|
||||||
...args.data,
|
...args.data,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
state.item[args.key as keyof ListState['item']].table.pagination = {
|
state.item[args.key as keyof ListState['item']].table.pagination = {
|
||||||
...state.item[args.key as keyof ListState['item']].table.pagination,
|
...state.item[args.key as keyof ListState['item']].table
|
||||||
|
.pagination,
|
||||||
...args.data,
|
...args.data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,11 @@ export interface QueueData {
|
||||||
|
|
||||||
export interface PlayerSlice extends PlayerState {
|
export interface PlayerSlice extends PlayerState {
|
||||||
actions: {
|
actions: {
|
||||||
addToQueue: (args: { initialIndex: number; playType: Play; songs: QueueSong[] }) => PlayerData;
|
addToQueue: (args: {
|
||||||
|
initialIndex: number;
|
||||||
|
playType: Play;
|
||||||
|
songs: QueueSong[];
|
||||||
|
}) => PlayerData;
|
||||||
autoNext: () => PlayerData;
|
autoNext: () => PlayerData;
|
||||||
checkIsFirstTrack: () => boolean;
|
checkIsFirstTrack: () => boolean;
|
||||||
checkIsLastTrack: (type?: 'next' | 'prev') => boolean;
|
checkIsLastTrack: (type?: 'next' | 'prev') => boolean;
|
||||||
|
@ -115,9 +119,9 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
// Splice the initial song from the queue
|
// Splice the initial song from the queue
|
||||||
queueCopy.splice(index, 1);
|
queueCopy.splice(index, 1);
|
||||||
|
|
||||||
const shuffledSongIndicesWithoutInitial = shuffle(queueCopy).map(
|
const shuffledSongIndicesWithoutInitial = shuffle(
|
||||||
(song) => song.uniqueId,
|
queueCopy,
|
||||||
);
|
).map((song) => song.uniqueId);
|
||||||
|
|
||||||
// Add the initial song to the start of the shuffled queue
|
// Add the initial song to the start of the shuffled queue
|
||||||
const shuffledSongIndices = [
|
const shuffledSongIndices = [
|
||||||
|
@ -159,7 +163,10 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.queue.default = [...get().queue.default, ...songsToAddToQueue];
|
state.queue.default = [
|
||||||
|
...get().queue.default,
|
||||||
|
...songsToAddToQueue,
|
||||||
|
];
|
||||||
state.queue.shuffled = shuffledQueueWithNewSongs;
|
state.queue.shuffled = shuffledQueueWithNewSongs;
|
||||||
});
|
});
|
||||||
} else if (playType === Play.NEXT) {
|
} else if (playType === Play.NEXT) {
|
||||||
|
@ -215,10 +222,13 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
state.queue.previousNode = get().current.song;
|
state.queue.previousNode = get().current.song;
|
||||||
});
|
});
|
||||||
} else if (get().shuffle === PlayerShuffle.TRACK) {
|
} else if (get().shuffle === PlayerShuffle.TRACK) {
|
||||||
const nextShuffleIndex = isLastTrack ? 0 : get().current.shuffledIndex + 1;
|
const nextShuffleIndex = isLastTrack
|
||||||
|
? 0
|
||||||
|
: get().current.shuffledIndex + 1;
|
||||||
|
|
||||||
const nextSong = get().queue.default.find(
|
const nextSong = get().queue.default.find(
|
||||||
(song) => song.uniqueId === get().queue.shuffled[nextShuffleIndex],
|
(song) =>
|
||||||
|
song.uniqueId === get().queue.shuffled[nextShuffleIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
const nextSongIndex = get().queue.default.findIndex(
|
const nextSongIndex = get().queue.default.findIndex(
|
||||||
|
@ -325,7 +335,9 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
|
|
||||||
const next = nextSongIndex
|
const next = nextSongIndex
|
||||||
? (queue.find(
|
? (queue.find(
|
||||||
(song) => song.uniqueId === shuffledQueue[nextSongIndex as number],
|
(song) =>
|
||||||
|
song.uniqueId ===
|
||||||
|
shuffledQueue[nextSongIndex as number],
|
||||||
) as QueueSong)
|
) as QueueSong)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
@ -411,7 +423,10 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
queue: {
|
queue: {
|
||||||
current: queue[currentIndex],
|
current: queue[currentIndex],
|
||||||
length: get().queue.default.length || 0,
|
length: get().queue.default.length || 0,
|
||||||
next: nextSongIndex !== undefined ? queue[nextSongIndex] : undefined,
|
next:
|
||||||
|
nextSongIndex !== undefined
|
||||||
|
? queue[nextSongIndex]
|
||||||
|
: undefined,
|
||||||
previous: queue[currentIndex - 1],
|
previous: queue[currentIndex - 1],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -453,8 +468,12 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
moveToBottomOfQueue: (uniqueIds) => {
|
moveToBottomOfQueue: (uniqueIds) => {
|
||||||
const queue = get().queue.default;
|
const queue = get().queue.default;
|
||||||
|
|
||||||
const songsToMove = queue.filter((song) => uniqueIds.includes(song.uniqueId));
|
const songsToMove = queue.filter((song) =>
|
||||||
const songsToStay = queue.filter((song) => !uniqueIds.includes(song.uniqueId));
|
uniqueIds.includes(song.uniqueId),
|
||||||
|
);
|
||||||
|
const songsToStay = queue.filter(
|
||||||
|
(song) => !uniqueIds.includes(song.uniqueId),
|
||||||
|
);
|
||||||
|
|
||||||
const reorderedQueue = [...songsToStay, ...songsToMove];
|
const reorderedQueue = [...songsToStay, ...songsToMove];
|
||||||
|
|
||||||
|
@ -473,8 +492,12 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
moveToTopOfQueue: (uniqueIds) => {
|
moveToTopOfQueue: (uniqueIds) => {
|
||||||
const queue = get().queue.default;
|
const queue = get().queue.default;
|
||||||
|
|
||||||
const songsToMove = queue.filter((song) => uniqueIds.includes(song.uniqueId));
|
const songsToMove = queue.filter((song) =>
|
||||||
const songsToStay = queue.filter((song) => !uniqueIds.includes(song.uniqueId));
|
uniqueIds.includes(song.uniqueId),
|
||||||
|
);
|
||||||
|
const songsToStay = queue.filter(
|
||||||
|
(song) => !uniqueIds.includes(song.uniqueId),
|
||||||
|
);
|
||||||
|
|
||||||
const reorderedQueue = [...songsToMove, ...songsToStay];
|
const reorderedQueue = [...songsToMove, ...songsToStay];
|
||||||
|
|
||||||
|
@ -495,10 +518,13 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
const { repeat } = get();
|
const { repeat } = get();
|
||||||
|
|
||||||
if (get().shuffle === PlayerShuffle.TRACK) {
|
if (get().shuffle === PlayerShuffle.TRACK) {
|
||||||
const nextShuffleIndex = isLastTrack ? 0 : get().current.shuffledIndex + 1;
|
const nextShuffleIndex = isLastTrack
|
||||||
|
? 0
|
||||||
|
: get().current.shuffledIndex + 1;
|
||||||
|
|
||||||
const nextSong = get().queue.default.find(
|
const nextSong = get().queue.default.find(
|
||||||
(song) => song.uniqueId === get().queue.shuffled[nextShuffleIndex],
|
(song) =>
|
||||||
|
song.uniqueId === get().queue.shuffled[nextShuffleIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
const nextSongIndex = get().queue.default.findIndex(
|
const nextSongIndex = get().queue.default.findIndex(
|
||||||
|
@ -523,7 +549,9 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
if (repeat === PlayerRepeat.ALL) {
|
if (repeat === PlayerRepeat.ALL) {
|
||||||
nextIndex = isLastTrack ? 0 : get().current.index + 1;
|
nextIndex = isLastTrack ? 0 : get().current.index + 1;
|
||||||
} else {
|
} else {
|
||||||
nextIndex = isLastTrack ? get().current.index : get().current.index + 1;
|
nextIndex = isLastTrack
|
||||||
|
? get().current.index
|
||||||
|
: get().current.index + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
|
@ -558,10 +586,13 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
const { repeat } = get();
|
const { repeat } = get();
|
||||||
|
|
||||||
if (get().shuffle === PlayerShuffle.TRACK) {
|
if (get().shuffle === PlayerShuffle.TRACK) {
|
||||||
const prevShuffleIndex = isFirstTrack ? 0 : get().current.shuffledIndex - 1;
|
const prevShuffleIndex = isFirstTrack
|
||||||
|
? 0
|
||||||
|
: get().current.shuffledIndex - 1;
|
||||||
|
|
||||||
const prevSong = get().queue.default.find(
|
const prevSong = get().queue.default.find(
|
||||||
(song) => song.uniqueId === get().queue.shuffled[prevShuffleIndex],
|
(song) =>
|
||||||
|
song.uniqueId === get().queue.shuffled[prevShuffleIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
const prevIndex = get().queue.default.findIndex(
|
const prevIndex = get().queue.default.findIndex(
|
||||||
|
@ -601,12 +632,15 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
const queue = get().queue.default;
|
const queue = get().queue.default;
|
||||||
const currentSong = get().current.song;
|
const currentSong = get().current.song;
|
||||||
|
|
||||||
const newQueue = queue.filter((song) => !uniqueIds.includes(song.uniqueId));
|
const newQueue = queue.filter(
|
||||||
|
(song) => !uniqueIds.includes(song.uniqueId),
|
||||||
|
);
|
||||||
const newShuffledQueue = get().queue.shuffled.filter(
|
const newShuffledQueue = get().queue.shuffled.filter(
|
||||||
(uniqueId) => !uniqueIds.includes(uniqueId),
|
(uniqueId) => !uniqueIds.includes(uniqueId),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isCurrentSongRemoved = currentSong && uniqueIds.includes(currentSong?.uniqueId);
|
const isCurrentSongRemoved =
|
||||||
|
currentSong && uniqueIds.includes(currentSong?.uniqueId);
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.queue.default = newQueue;
|
state.queue.default = newQueue;
|
||||||
|
@ -639,12 +673,16 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
const reorderedQueue = afterUniqueId
|
const reorderedQueue = afterUniqueId
|
||||||
? [
|
? [
|
||||||
...queueWithoutSelectedRows.slice(0, moveBeforeIndex),
|
...queueWithoutSelectedRows.slice(0, moveBeforeIndex),
|
||||||
...queue.filter((song) => rowUniqueIds.includes(song.uniqueId)),
|
...queue.filter((song) =>
|
||||||
|
rowUniqueIds.includes(song.uniqueId),
|
||||||
|
),
|
||||||
...queueWithoutSelectedRows.slice(moveBeforeIndex),
|
...queueWithoutSelectedRows.slice(moveBeforeIndex),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
...queueWithoutSelectedRows,
|
...queueWithoutSelectedRows,
|
||||||
...queue.filter((song) => rowUniqueIds.includes(song.uniqueId)),
|
...queue.filter((song) =>
|
||||||
|
rowUniqueIds.includes(song.uniqueId),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
const currentSongIndex = reorderedQueue.findIndex(
|
const currentSongIndex = reorderedQueue.findIndex(
|
||||||
|
@ -712,7 +750,9 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
(song) => song.uniqueId === uniqueId,
|
(song) => song.uniqueId === uniqueId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const shuffledIndex = get().queue.shuffled.findIndex((id) => id === uniqueId);
|
const shuffledIndex = get().queue.shuffled.findIndex(
|
||||||
|
(id) => id === uniqueId,
|
||||||
|
);
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.current.time = 0;
|
state.current.time = 0;
|
||||||
|
@ -816,7 +856,9 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||||
(song) => song.uniqueId !== currentSongId,
|
(song) => song.uniqueId !== currentSongId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const shuffledSongIds = shuffle(queueWithoutCurrentSong).map((song) => song.uniqueId);
|
const shuffledSongIds = shuffle(queueWithoutCurrentSong).map(
|
||||||
|
(song) => song.uniqueId,
|
||||||
|
);
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.shuffle = type;
|
state.shuffle = type;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue