From edf587952373d4004156176ba4f713b81cf04725 Mon Sep 17 00:00:00 2001 From: Finomosec <1665799+Finomosec@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:22:12 +0200 Subject: [PATCH 1/2] feat: add pagination to album songs view Large albums (300+ tracks) freeze the browser because all songs are loaded at once with perPage={0}. Add pagination with configurable page sizes (100/250/500) and fetch all songs on-demand for play/shuffle actions. Closes #2539, closes #1186, closes #4397 Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/album/AlbumActions.jsx | 68 +++++++++++++++++++++++++++++------ ui/src/album/AlbumShow.jsx | 5 +-- ui/src/album/AlbumSongs.jsx | 15 +++++++- 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/ui/src/album/AlbumActions.jsx b/ui/src/album/AlbumActions.jsx index 96cfab09a..507b43a7e 100644 --- a/ui/src/album/AlbumActions.jsx +++ b/ui/src/album/AlbumActions.jsx @@ -5,6 +5,8 @@ import { Button, sanitizeListRestProps, TopToolbar, + useDataProvider, + useNotify, useRecordContext, useTranslate, } from 'react-admin' @@ -52,30 +54,74 @@ const AlbumActions = ({ }) => { const dispatch = useDispatch() const translate = useTranslate() + const dataProvider = useDataProvider() + const notify = useNotify() const classes = useStyles() const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md')) const isNotSmall = useMediaQuery((theme) => theme.breakpoints.up('sm')) + const getAllSongsAndDispatch = React.useCallback( + (action) => { + if (ids?.length === record.songCount) { + return dispatch(action(data, ids)) + } + dataProvider + .getList('song', { + pagination: { page: 1, perPage: 0 }, + sort: { field: 'album', order: 'ASC' }, + filter: { album_id: record.id }, + }) + .then((res) => { + const allData = res.data.reduce( + (acc, curr) => ({ ...acc, [curr.id]: curr }), + {}, + ) + dispatch(action(allData)) + }) + .catch(() => { + notify('ra.page.error', 'warning') + }) + }, + [dataProvider, dispatch, record, data, ids, notify], + ) + const handlePlay = React.useCallback(() => { - dispatch(playTracks(data, ids)) - }, [dispatch, data, ids]) + getAllSongsAndDispatch(playTracks) + }, [getAllSongsAndDispatch]) const handlePlayNext = React.useCallback(() => { - dispatch(playNext(data, ids)) - }, [dispatch, data, ids]) + getAllSongsAndDispatch(playNext) + }, [getAllSongsAndDispatch]) const handlePlayLater = React.useCallback(() => { - dispatch(addTracks(data, ids)) - }, [dispatch, data, ids]) + getAllSongsAndDispatch(addTracks) + }, [getAllSongsAndDispatch]) const handleShuffle = React.useCallback(() => { - dispatch(shuffleTracks(data, ids)) - }, [dispatch, data, ids]) + getAllSongsAndDispatch(shuffleTracks) + }, [getAllSongsAndDispatch]) const handleAddToPlaylist = React.useCallback(() => { - const selectedIds = ids.filter((id) => !data[id].missing) - dispatch(openAddToPlaylist({ selectedIds })) - }, [dispatch, data, ids]) + if (ids?.length === record.songCount) { + const selectedIds = ids.filter((id) => !data[id].missing) + return dispatch(openAddToPlaylist({ selectedIds })) + } + dataProvider + .getList('song', { + pagination: { page: 1, perPage: 0 }, + sort: { field: 'album', order: 'ASC' }, + filter: { album_id: record.id }, + }) + .then((res) => { + const selectedIds = res.data + .filter((s) => !s.missing) + .map((s) => s.id) + dispatch(openAddToPlaylist({ selectedIds })) + }) + .catch(() => { + notify('ra.page.error', 'warning') + }) + }, [dataProvider, dispatch, record, data, ids, notify]) const handleShare = React.useCallback(() => { dispatch(openShareMenu([record.id], 'album', record.name)) diff --git a/ui/src/album/AlbumShow.jsx b/ui/src/album/AlbumShow.jsx index c9e944999..68dee861c 100644 --- a/ui/src/album/AlbumShow.jsx +++ b/ui/src/album/AlbumShow.jsx @@ -4,6 +4,7 @@ import { ShowContextProvider, useShowContext, useShowController, + Pagination, Title as RaTitle, } from 'react-admin' import { makeStyles } from '@material-ui/core/styles' @@ -40,8 +41,7 @@ const AlbumShowLayout = (props) => { reference="song" target="album_id" sort={{ field: 'album', order: 'ASC' }} - perPage={0} - pagination={null} + perPage={50} > { actions={ } + pagination={} /> )} diff --git a/ui/src/album/AlbumSongs.jsx b/ui/src/album/AlbumSongs.jsx index 8a7fd2ae4..4c942035a 100644 --- a/ui/src/album/AlbumSongs.jsx +++ b/ui/src/album/AlbumSongs.jsx @@ -89,6 +89,7 @@ const useStyles = makeStyles( const AlbumSongs = (props) => { const { data, ids } = props + const listContext = useListContext(props) const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md')) const classes = useStyles({ isDesktop }) const dispatch = useDispatch() @@ -210,6 +211,8 @@ const AlbumSongs = (props) => { } /> + {props.pagination && + React.cloneElement(props.pagination, listContext)} ) } @@ -217,7 +220,17 @@ const AlbumSongs = (props) => { const SanitizedAlbumSongs = (props) => { removeAlbumCommentsFromSongs(props) const { loaded, loading, total, ...rest } = useListContext(props) - return <>{loaded && } + return ( + <> + {loaded && ( + + )} + + ) } export default SanitizedAlbumSongs From 0f686a301c52428c5d9a690e2cd6da641faa5dc9 Mon Sep 17 00:00:00 2001 From: Finomosec <1665799+Finomosec@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:29:27 +0200 Subject: [PATCH 2/2] fix: pass ordered song IDs to player actions and guard against undefined ids - Pass allIds to dispatched actions so player knows track order - Check ids is defined before comparing length to prevent crash during initial load when both ids and record.songCount are undefined Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/album/AlbumActions.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/src/album/AlbumActions.jsx b/ui/src/album/AlbumActions.jsx index 507b43a7e..bd782b25c 100644 --- a/ui/src/album/AlbumActions.jsx +++ b/ui/src/album/AlbumActions.jsx @@ -62,7 +62,7 @@ const AlbumActions = ({ const getAllSongsAndDispatch = React.useCallback( (action) => { - if (ids?.length === record.songCount) { + if (ids && ids.length === record.songCount) { return dispatch(action(data, ids)) } dataProvider @@ -76,7 +76,8 @@ const AlbumActions = ({ (acc, curr) => ({ ...acc, [curr.id]: curr }), {}, ) - dispatch(action(allData)) + const allIds = res.data.map((s) => s.id) + dispatch(action(allData, allIds)) }) .catch(() => { notify('ra.page.error', 'warning') @@ -102,7 +103,7 @@ const AlbumActions = ({ }, [getAllSongsAndDispatch]) const handleAddToPlaylist = React.useCallback(() => { - if (ids?.length === record.songCount) { + if (ids && ids.length === record.songCount) { const selectedIds = ids.filter((id) => !data[id].missing) return dispatch(openAddToPlaylist({ selectedIds })) }