diff --git a/src/__tests__/App.test.tsx b/src/__tests__/App.test.tsx index 0851164..d8f1d1d 100644 --- a/src/__tests__/App.test.tsx +++ b/src/__tests__/App.test.tsx @@ -12,7 +12,6 @@ import { ConfigPage } from '../redux/configSlice'; import { FolderSelection } from '../redux/folderSlice'; import { FavoritePage } from '../redux/favoriteSlice'; import App from '../App'; -import { AlbumPage } from '../redux/albumSlice'; import { Server } from '../types'; import { ArtistPage } from '../redux/artistSlice'; import { View } from '../redux/viewSlice'; @@ -416,34 +415,6 @@ const favoriteState: FavoritePage = { }, }; -const albumState: AlbumPage = { - active: { - filter: 'random', - }, - advancedFilters: { - enabled: false, - nav: 'filters', - properties: { - starred: false, - genre: { - list: [], - type: 'and', - }, - artist: { - list: [], - type: 'and', - }, - year: { - from: 0, - to: 0, - }, - sort: { - type: 'asc', - }, - }, - }, -}; - const artistState: ArtistPage = { active: { list: { @@ -460,6 +431,37 @@ const artistState: ArtistPage = { }; const viewState: View = { + album: { + filter: 'alphabeticalByName', + sort: { + type: 'asc', + }, + advancedFilters: { + enabled: false, + nav: 'filters', + properties: { + starred: false, + genre: { + list: [], + type: 'and', + }, + artist: { + list: [], + type: 'and', + }, + year: { + from: 0, + to: 0, + }, + }, + }, + pagination: { + serverSide: true, + recordsPerPage: 19, + activePage: 147, + pages: 285, + }, + }, music: { filter: 'random', sort: { @@ -482,7 +484,6 @@ const mockInitialState = { folder: folderState, config: configState, favorite: favoriteState, - album: albumState, artist: artistState, view: viewState, }; diff --git a/src/api/api.ts b/src/api/api.ts index 057f3dd..07ae2e8 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -167,6 +167,13 @@ const getStreamUrl = (id: string, useLegacyAuth: boolean) => { ); }; +const normalizeAPIResult = (items: any, totalRecordCount?: number) => { + return { + data: items, + totalRecordCount, + }; +}; + const normalizeSong = (item: any) => { return { id: item.id, @@ -363,7 +370,11 @@ export const getAlbums = async ( if (!res.data.albumList2.album || res.data.albumList2.album.length === 0) { // Flatten and return once there are no more albums left const flattenedAlbums = _.flatten(recursiveData); - return (flattenedAlbums || []).map((entry: any) => normalizeAlbum(entry)); + + return normalizeAPIResult( + (flattenedAlbums || []).map((entry: any) => normalizeAlbum(entry)), + flattenedAlbums.length + ); } // On every iteration, push the existing combined album array and increase the offset @@ -385,7 +396,10 @@ export const getAlbums = async ( } const { data } = await api.get(`/getAlbumList2`, { params: options }); - return (data.albumList2.album || []).map((entry: any) => normalizeAlbum(entry)); + return normalizeAPIResult( + (data.albumList2.album || []).map((entry: any) => normalizeAlbum(entry)), + data.albumList2.album.length + ); }; export const getRandomSongs = async (options: { diff --git a/src/api/jellyfinApi.ts b/src/api/jellyfinApi.ts index c3873f2..d321256 100644 --- a/src/api/jellyfinApi.ts +++ b/src/api/jellyfinApi.ts @@ -414,23 +414,30 @@ export const getAlbums = async (options: { }, }); - return (data.Items || []).map((entry: any) => normalizeAlbum(entry)); + return normalizeAPIResult( + (data.Items || []).map((entry: any) => normalizeAlbum(entry)), + data.TotalRecordCount + ); } const { data } = await jellyfinApi.get(`/users/${auth.username}/items`, { params: { fields: 'Genres, DateCreated, ChildCount, ParentId', + genres: !sortType ? options.type : undefined, includeItemTypes: 'MusicAlbum', limit: options.size, startIndex: options.offset, parentId: options.musicFolderId, recursive: true, - sortBy: sortType!.replacement, - sortOrder: sortType!.sortOrder, + sortBy: sortType ? sortType!.replacement : 'SortName', + sortOrder: sortType ? sortType!.sortOrder : 'Ascending', }, }); - return (data.Items || []).map((entry: any) => normalizeAlbum(entry)); + return normalizeAPIResult( + (data.Items || []).map((entry: any) => normalizeAlbum(entry)), + data.TotalRecordCount + ); }; export const getSongs = async (options: { diff --git a/src/components/card/Card.tsx b/src/components/card/Card.tsx index 1bdbb19..072b0d3 100644 --- a/src/components/card/Card.tsx +++ b/src/components/card/Card.tsx @@ -276,7 +276,7 @@ const Card = ({ alt="img" effect="opacity" cardsize={size} - visibleByDefault={notVisibleByDefault ? false : cacheImages} + visibleByDefault={!notVisibleByDefault} afterLoad={() => { if (cacheImages) { cacheImage( diff --git a/src/components/dashboard/Dashboard.tsx b/src/components/dashboard/Dashboard.tsx index a83ad41..2838c5a 100644 --- a/src/components/dashboard/Dashboard.tsx +++ b/src/components/dashboard/Dashboard.tsx @@ -9,9 +9,9 @@ import GenericPageHeader from '../layout/GenericPageHeader'; import ScrollingMenu from '../scrollingmenu/ScrollingMenu'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { setStar } from '../../redux/playQueueSlice'; -import { setActive } from '../../redux/albumSlice'; import { apiController } from '../../api/controller'; -import { Server } from '../../types'; +import { Item, Server } from '../../types'; +import { setFilter, setPagination } from '../../redux/viewSlice'; const Dashboard = () => { const { t } = useTranslation(); @@ -20,7 +20,6 @@ const Dashboard = () => { const queryClient = useQueryClient(); const folder = useAppSelector((state) => state.folder); const config = useAppSelector((state) => state.config); - const album = useAppSelector((state) => state.album); const [musicFolder, setMusicFolder] = useState({ loaded: false, id: undefined }); useEffect(() => { @@ -100,33 +99,33 @@ const Dashboard = () => { }); dispatch(setStar({ id: [rowData.id], type: 'star' })); queryClient.setQueryData(['recentAlbums', musicFolder.id], (oldData: any) => { - const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id })); + const starredIndices = _.keys(_.pickBy(oldData.data, { id: rowData.id })); starredIndices.forEach((index) => { - oldData[index].starred = Date.now(); + oldData.data[index].starred = Date.now(); }); return oldData; }); queryClient.setQueryData(['newestAlbums', musicFolder.id], (oldData: any) => { - const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id })); + const starredIndices = _.keys(_.pickBy(oldData.data, { id: rowData.id })); starredIndices.forEach((index) => { - oldData[index].starred = Date.now(); + oldData.data[index].starred = Date.now(); }); return oldData; }); queryClient.setQueryData(['randomAlbums', musicFolder.id], (oldData: any) => { - const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id })); + const starredIndices = _.keys(_.pickBy(oldData.data, { id: rowData.id })); starredIndices.forEach((index) => { - oldData[index].starred = Date.now(); + oldData.data[index].starred = Date.now(); }); return oldData; }); queryClient.setQueryData(['frequentAlbums', musicFolder.id], (oldData: any) => { - const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id })); + const starredIndices = _.keys(_.pickBy(oldData.data, { id: rowData.id })); starredIndices.forEach((index) => { - oldData[index].starred = Date.now(); + oldData.data[index].starred = Date.now(); }); return oldData; @@ -139,33 +138,33 @@ const Dashboard = () => { }); dispatch(setStar({ id: [rowData.id], type: 'unstar' })); queryClient.setQueryData(['recentAlbums', musicFolder.id], (oldData: any) => { - const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id })); + const starredIndices = _.keys(_.pickBy(oldData.data, { id: rowData.id })); starredIndices.forEach((index) => { - oldData[index].starred = undefined; + oldData.data[index].starred = undefined; }); return oldData; }); queryClient.setQueryData(['newestAlbums', musicFolder.id], (oldData: any) => { - const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id })); + const starredIndices = _.keys(_.pickBy(oldData.data, { id: rowData.id })); starredIndices.forEach((index) => { - oldData[index].starred = undefined; + oldData.data[index].starred = undefined; }); return oldData; }); queryClient.setQueryData(['randomAlbums', musicFolder.id], (oldData: any) => { - const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id })); + const starredIndices = _.keys(_.pickBy(oldData.data, { id: rowData.id })); starredIndices.forEach((index) => { - oldData[index].starred = undefined; + oldData.data[index].starred = undefined; }); return oldData; }); queryClient.setQueryData(['frequentAlbums', musicFolder.id], (oldData: any) => { - const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id })); + const starredIndices = _.keys(_.pickBy(oldData.data, { id: rowData.id })); starredIndices.forEach((index) => { - oldData[index].starred = undefined; + oldData.data[index].starred = undefined; }); return oldData; @@ -188,7 +187,7 @@ const Dashboard = () => { { }} cardSize={config.lookAndFeel.gridView.cardSize} onClickTitle={() => { - dispatch(setActive({ ...album.active, filter: 'recent' })); + dispatch(setFilter({ listType: Item.Album, data: 'recent' })); + dispatch(setPagination({ listType: Item.Album, data: { activePage: 1 } })); setTimeout(() => { history.push(`/library/album?sortType=recent`); }, 50); @@ -213,7 +213,7 @@ const Dashboard = () => { { }} cardSize={config.lookAndFeel.gridView.cardSize} onClickTitle={() => { - dispatch(setActive({ ...album.active, filter: 'newest' })); + dispatch(setFilter({ listType: Item.Album, data: 'newest' })); + dispatch(setPagination({ listType: Item.Album, data: { activePage: 1 } })); setTimeout(() => { history.push(`/library/album?sortType=newest`); }, 50); @@ -238,7 +239,7 @@ const Dashboard = () => { { }} cardSize={config.lookAndFeel.gridView.cardSize} onClickTitle={() => { - dispatch(setActive({ ...album.active, filter: 'random' })); + dispatch(setFilter({ listType: Item.Album, data: 'random' })); + dispatch(setPagination({ listType: Item.Album, data: { activePage: 1 } })); setTimeout(() => { history.push(`/library/album?sortType=random`); }, 50); @@ -263,7 +265,7 @@ const Dashboard = () => { { }} cardSize={config.lookAndFeel.gridView.cardSize} onClickTitle={() => { - dispatch(setActive({ ...album.active, filter: 'frequent' })); + dispatch(setFilter({ listType: Item.Album, data: 'frequent' })); + dispatch(setPagination({ listType: Item.Album, data: { activePage: 1 } })); setTimeout(() => { history.push(`/library/album?sortType=frequent`); }, 50); diff --git a/src/components/library/AdvancedFilters.tsx b/src/components/library/AdvancedFilters.tsx index 2d74097..4d5f81b 100644 --- a/src/components/library/AdvancedFilters.tsx +++ b/src/components/library/AdvancedFilters.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { ButtonToolbar, ControlLabel, Divider, FlexboxGrid, RadioGroup } from 'rsuite'; import styled from 'styled-components'; import { useAppDispatch } from '../../redux/hooks'; +import { Item } from '../../types'; import { StyledButton, StyledCheckbox, @@ -154,7 +155,7 @@ const AdvancedFilters = ({ filteredData, originalData, filter, setAdvancedFilter defaultChecked={filter.enabled} checked={filter.enabled} onChange={(e: boolean) => { - dispatch(setAdvancedFilters({ filter: 'enabled', value: e })); + dispatch(setAdvancedFilters({ listType: Item.Album, filter: 'enabled', value: e })); }} /> @@ -166,6 +167,7 @@ const AdvancedFilters = ({ filteredData, originalData, filter, setAdvancedFilter onChange={(_v: any, e: boolean) => { dispatch( setAdvancedFilters({ + listType: Item.Album, filter: 'starred', value: e, }) @@ -186,6 +188,7 @@ const AdvancedFilters = ({ filteredData, originalData, filter, setAdvancedFilter onClick={() => { dispatch( setAdvancedFilters({ + listType: Item.Album, filter: 'genre', value: { ...filter.properties.genre, list: [] }, }) @@ -202,7 +205,11 @@ const AdvancedFilters = ({ filteredData, originalData, filter, setAdvancedFilter defaultValue={filter.properties.genre.type} onChange={(e: string) => { dispatch( - setAdvancedFilters({ filter: 'genre', value: { ...filter.properties.genre, type: e } }) + setAdvancedFilters({ + listType: Item.Album, + filter: 'genre', + value: { ...filter.properties.genre, type: e }, + }) ); }} > @@ -245,6 +252,7 @@ const AdvancedFilters = ({ filteredData, originalData, filter, setAdvancedFilter onChange={(e: string[]) => { dispatch( setAdvancedFilters({ + listType: Item.Album, filter: 'genre', value: { ...filter.properties.genre, list: e }, }) @@ -265,6 +273,7 @@ const AdvancedFilters = ({ filteredData, originalData, filter, setAdvancedFilter onClick={() => { dispatch( setAdvancedFilters({ + listType: Item.Album, filter: 'artist', value: { ...filter.properties.artist, list: [] }, }) @@ -282,6 +291,7 @@ const AdvancedFilters = ({ filteredData, originalData, filter, setAdvancedFilter onChange={(e: string) => { dispatch( setAdvancedFilters({ + listType: Item.Album, filter: 'artist', value: { ...filter.properties.artist, type: e }, }) @@ -327,6 +337,7 @@ const AdvancedFilters = ({ filteredData, originalData, filter, setAdvancedFilter onChange={(e: string[]) => { dispatch( setAdvancedFilters({ + listType: Item.Album, filter: 'artist', value: { ...filter.properties.artist, list: e }, }) @@ -351,6 +362,7 @@ const AdvancedFilters = ({ filteredData, originalData, filter, setAdvancedFilter onClick={() => { dispatch( setAdvancedFilters({ + listType: Item.Album, filter: 'year', value: { from: 0, to: 0 }, }) @@ -375,6 +387,7 @@ const AdvancedFilters = ({ filteredData, originalData, filter, setAdvancedFilter onChange={(e: number) => { dispatch( setAdvancedFilters({ + listType: Item.Album, filter: 'year', value: { ...filter.properties.year, from: Number(e) }, }) @@ -394,6 +407,7 @@ const AdvancedFilters = ({ filteredData, originalData, filter, setAdvancedFilter onChange={(e: number) => { dispatch( setAdvancedFilters({ + listType: Item.Album, filter: 'year', value: { ...filter.properties.year, to: Number(e) }, }) diff --git a/src/components/library/AlbumList.tsx b/src/components/library/AlbumList.tsx index 2ca6b9a..75f5b1c 100644 --- a/src/components/library/AlbumList.tsx +++ b/src/components/library/AlbumList.tsx @@ -11,7 +11,6 @@ import ListViewType from '../viewtypes/ListViewType'; import useSearchQuery from '../../hooks/useSearchQuery'; import GenericPageHeader from '../layout/GenericPageHeader'; import GenericPage from '../layout/GenericPage'; -import PageLoader from '../loader/PageLoader'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { toggleSelected, @@ -27,7 +26,6 @@ import { StyledTag, } from '../shared/styled'; import { FilterButton, RefreshButton } from '../shared/ToolbarButtons'; -import { setActive, setAdvancedFilters } from '../../redux/albumSlice'; import { setSearchQuery } from '../../redux/miscSlice'; import { apiController } from '../../api/controller'; import { Item, Server } from '../../types'; @@ -35,6 +33,9 @@ import AdvancedFilters from './AdvancedFilters'; import useAdvancedFilter from '../../hooks/useAdvancedFilter'; import ColumnSort from '../shared/ColumnSort'; import useColumnSort from '../../hooks/useColumnSort'; +import { setFilter, setPagination, setAdvancedFilters, setColumnSort } from '../../redux/viewSlice'; +import useGridScroll from '../../hooks/useGridScroll'; +import useListScroll from '../../hooks/useListScroll'; export const ALBUM_SORT_TYPES = [ { label: i18next.t('A-Z (Name)'), value: 'alphabeticalByName', role: i18next.t('Default') }, @@ -51,43 +52,65 @@ const AlbumList = () => { const history = useHistory(); const queryClient = useQueryClient(); const folder = useAppSelector((state) => state.folder); - const album = useAppSelector((state) => state.album); const config = useAppSelector((state) => state.config); const misc = useAppSelector((state) => state.misc); + const view = useAppSelector((state) => state.view); const [isRefreshing, setIsRefreshing] = useState(false); const [sortTypes, setSortTypes] = useState([]); const [viewType, setViewType] = useState(settings.getSync('albumViewType')); - const [musicFolder, setMusicFolder] = useState(undefined); + const [musicFolder, setMusicFolder] = useState({ loaded: false, id: undefined }); const albumFilterPickerContainerRef = useRef(null); - const [isRefresh, setIsRefresh] = useState(false); + const [currentQueryKey, setCurrentQueryKey] = useState(['albumList']); + + const gridRef = useRef(); + const listRef = useRef(); + const { gridScroll } = useGridScroll(gridRef); + const { listScroll } = useListScroll(listRef); useEffect(() => { if (folder.applied.albums) { - setMusicFolder(folder.musicFolder); + setMusicFolder({ loaded: true, id: folder.musicFolder }); + } else { + setMusicFolder({ loaded: true, id: undefined }); + } + }, [folder.applied.albums, folder.musicFolder]); + + useEffect(() => { + if (config.serverType === Server.Subsonic || !view.album.pagination.serverSide) { + // Client-side paging won't require a separate key for the active page + setCurrentQueryKey(['albumList', view.album.filter, musicFolder.id]); + } else { + setCurrentQueryKey(['albumList', view.album.filter, view.album.pagination, musicFolder.id]); } - }, [folder]); + }, [config.serverType, musicFolder.id, view.album.filter, view.album.pagination]); const { isLoading, isError, data: albums, error }: any = useQuery( - ['albumList', album.active.filter, musicFolder], + currentQueryKey, () => - album.active.filter === 'random' + view.album.filter === 'random' || + (view.album.pagination.recordsPerPage !== 0 && view.album.pagination.serverSide) ? apiController({ serverType: config.serverType, endpoint: 'getAlbums', args: config.serverType === Server.Subsonic ? { - type: 'random', - size: 100, + type: view.album.filter, + size: 500, offset: 0, musicFolderId: musicFolder, + recursive: view.album.filter !== 'random', } : { - type: 'random', - size: 100, - offset: 0, + type: view.album.filter, + size: + view.album.pagination.recordsPerPage === 0 + ? 100 + : view.album.pagination.recordsPerPage, + offset: + (view.album.pagination.activePage - 1) * view.album.pagination.recordsPerPage, recursive: false, - musicFolderId: musicFolder, + musicFolderId: musicFolder.id, }, }) : apiController({ @@ -96,23 +119,31 @@ const AlbumList = () => { args: config.serverType === Server.Subsonic ? { - type: album.active.filter, + type: view.album.filter, size: 500, offset: 0, - musicFolderId: musicFolder, + musicFolderId: musicFolder.id, recursive: true, } : { - type: album.active.filter, + type: view.album.filter, recursive: true, - musicFolderId: musicFolder, + musicFolderId: musicFolder.id, }, }), { - cacheTime: 3600000, // Stay in cache for 1 hour - staleTime: Infinity, // Only allow manual refresh + cacheTime: + view.album.pagination.recordsPerPage !== 0 && config.serverType === Server.Jellyfin + ? 600000 + : Infinity, + staleTime: + view.album.pagination.recordsPerPage !== 0 && config.serverType === Server.Jellyfin + ? 600000 + : Infinity, + enabled: currentQueryKey !== ['albumList'] && musicFolder.loaded, } ); + const { data: genres }: any = useQuery(['genreList'], async () => { const res = await apiController({ serverType: config.serverType, @@ -131,7 +162,7 @@ const AlbumList = () => { }); }); - const searchedData = useSearchQuery(misc.searchQuery, albums, [ + const searchedData = useSearchQuery(misc.searchQuery, albums?.data, [ 'title', 'artist', 'genre', @@ -145,18 +176,36 @@ const AlbumList = () => { byGenreData, byStarredData, byYearData, - } = useAdvancedFilter(albums, album.advancedFilters); + } = useAdvancedFilter(albums?.data, view.album.advancedFilters); - const { sortColumns, sortedData } = useColumnSort( - filteredData, - Item.Album, - album.advancedFilters.properties.sort - ); + const { sortColumns, sortedData } = useColumnSort(filteredData, Item.Album, view.album.sort); useEffect(() => { setSortTypes(_.compact(_.concat(ALBUM_SORT_TYPES, genres))); }, [genres]); + useEffect(() => { + if (albums?.data && sortedData?.length) { + const pages = + Math.floor( + (view.album.pagination.serverSide && config.serverType === Server.Jellyfin + ? albums?.totalRecordCount + : sortedData?.length) / view.album.pagination.recordsPerPage + ) + 1; + + if (pages && view.album.pagination.pages !== pages) { + dispatch( + setPagination({ + listType: Item.Album, + data: { + pages, + }, + }) + ); + } + } + }, [albums, config.serverType, dispatch, sortedData?.length, view.album.pagination]); + let timeout: any = null; const handleRowClick = (e: any, rowData: any, tableData: any) => { if (timeout === null) { @@ -194,10 +243,10 @@ const AlbumList = () => { endpoint: 'star', args: { id: rowData.id, type: 'album' }, }); - queryClient.setQueryData(['albumList', album.active.filter, musicFolder], (oldData: any) => { - const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id })); + queryClient.setQueryData(['albumList', view.album.filter, musicFolder.id], (oldData: any) => { + const starredIndices = _.keys(_.pickBy(oldData.data, { id: rowData.id })); starredIndices.forEach((index) => { - oldData[index].starred = Date.now(); + oldData.data[index].starred = Date.now(); }); return oldData; @@ -208,10 +257,10 @@ const AlbumList = () => { endpoint: 'unstar', args: { id: rowData.id, type: 'album' }, }); - queryClient.setQueryData(['albumList', album.active.filter, musicFolder], (oldData: any) => { - const starredIndices = _.keys(_.pickBy(oldData, { id: rowData.id })); + queryClient.setQueryData(['albumList', view.album.filter, musicFolder.id], (oldData: any) => { + const starredIndices = _.keys(_.pickBy(oldData.data, { id: rowData.id })); starredIndices.forEach((index) => { - oldData[index].starred = undefined; + oldData.data[index].starred = undefined; }); return oldData; @@ -226,10 +275,10 @@ const AlbumList = () => { args: { ids: [rowData.id], rating: e }, }); - queryClient.setQueryData(['albumList', album.active.filter, musicFolder], (oldData: any) => { - const ratedIndices = _.keys(_.pickBy(oldData, { id: rowData.id })); + queryClient.setQueryData(['albumList', view.album.filter, musicFolder.id], (oldData: any) => { + const ratedIndices = _.keys(_.pickBy(oldData.data, { id: rowData.id })); ratedIndices.forEach((index) => { - oldData[index].userRating = e; + oldData.data[index].userRating = e; }); return oldData; @@ -256,8 +305,8 @@ const AlbumList = () => { container={() => albumFilterPickerContainerRef.current} size="sm" width={180} - defaultValue={album.active.filter} - value={album.active.filter} + defaultValue={view.album.filter} + value={view.album.filter} groupBy="role" data={sortTypes || ALBUM_SORT_TYPES} disabledItemValues={ @@ -266,17 +315,18 @@ const AlbumList = () => { cleanable={false} placeholder={t('Sort Type')} onChange={async (value: string) => { - setIsRefresh(true); await queryClient.cancelQueries([ 'albumList', - album.active.filter, - musicFolder, + view.album.filter, + musicFolder.id, ]); dispatch(setSearchQuery('')); - dispatch(setActive({ ...album.active, filter: value })); + dispatch(setFilter({ listType: Item.Album, data: value })); + dispatch(setPagination({ listType: Item.Album, data: { activePage: 1 } })); localStorage.setItem('scroll_grid_albumList', '0'); localStorage.setItem('scroll_list_albumList', '0'); - setIsRefresh(false); + gridScroll(0); + listScroll(0); }} /> @@ -293,8 +343,12 @@ const AlbumList = () => { speaker={
- {album.advancedFilters.nav === 'filters' && ( + {view.album.advancedFilters.nav === 'filters' && ( { byStarredData, byYearData, }} - originalData={albums} - filter={album.advancedFilters} + originalData={albums?.data} + filter={view.album.advancedFilters} setAdvancedFilters={setAdvancedFilters} /> )} - {album.advancedFilters.nav === 'sort' && ( + {view.album.advancedFilters.nav === 'sort' && ( dispatch( - setAdvancedFilters({ - filter: 'sort', - value: { - ...album.advancedFilters.properties.sort, + setColumnSort({ + listType: Item.Album, + data: { + ...view.album.sort, column: undefined, }, }) @@ -339,17 +393,17 @@ const AlbumList = () => { } setSortType={(e: string) => dispatch( - setAdvancedFilters({ - filter: 'sort', - value: { ...album.advancedFilters.properties.sort, type: e }, + setColumnSort({ + listType: Item.Album, + data: { ...view.album.sort, type: e }, }) ) } setSortColumn={(e: string) => dispatch( - setAdvancedFilters({ - filter: 'sort', - value: { ...album.advancedFilters.properties.sort, column: e }, + setColumnSort({ + listType: Item.Album, + data: { ...view.album.sort, column: e }, }) ) } @@ -361,7 +415,7 @@ const AlbumList = () => { { /> } > - {isLoading && } {isError &&
Error: {error}
} - {!isLoading && !isError && sortedData?.length > 0 && viewType === 'list' && ( + {!isError && viewType === 'list' && ( { 'deletePlaylist', 'viewInFolder', ]} + loading={isLoading} handleFavorite={handleRowFavorite} initialScrollOffset={Number(localStorage.getItem('scroll_list_albumList'))} onScroll={(scrollIndex: number) => { localStorage.setItem('scroll_list_albumList', String(Math.abs(scrollIndex))); }} + paginationProps={ + view.album.pagination.recordsPerPage !== 0 && { + disabled: misc.searchQuery !== '' ? true : null, + pages: view.album.pagination.pages, + activePage: view.album.pagination.activePage, + maxButtons: 3, + prev: true, + next: true, + ellipsis: true, + boundaryLinks: true, + startIndex: + view.album.pagination.recordsPerPage * (view.album.pagination.activePage - 1) + 1, + endIndex: view.album.pagination.recordsPerPage * view.album.pagination.activePage, + handleGoToButton: (e: number) => { + localStorage.setItem('scroll_list_albumList', '0'); + dispatch( + setPagination({ + listType: Item.Album, + data: { + activePage: e, + }, + }) + ); + }, + onSelect: async (e: number) => { + localStorage.setItem('scroll_list_albumList', '0'); + await queryClient.cancelQueries(['albumList'], { active: true }); + dispatch( + setPagination({ + listType: Item.Album, + data: { + activePage: e, + }, + }) + ); + listScroll(0); + }, + } + } /> )} - {!isLoading && !isError && sortedData?.length > 0 && viewType === 'grid' && ( + {!isError && viewType === 'grid' && ( { onScroll={(scrollIndex: number) => { localStorage.setItem('scroll_grid_albumList', String(scrollIndex)); }} - refresh={isRefresh} + loading={isLoading} + paginationProps={ + view.album.pagination.recordsPerPage !== 0 && { + disabled: misc.searchQuery !== '' ? true : null, + pages: view.album.pagination.pages, + activePage: view.album.pagination.activePage, + maxButtons: 3, + prev: true, + next: true, + ellipsis: true, + boundaryLinks: true, + startIndex: + view.album.pagination.recordsPerPage * (view.album.pagination.activePage - 1) + 1, + endIndex: view.album.pagination.recordsPerPage * view.album.pagination.activePage, + handleGoToButton: (e: number) => { + localStorage.setItem('scroll_grid_albumList', '0'); + dispatch( + setPagination({ + listType: Item.Album, + data: { + activePage: e, + }, + }) + ); + }, + onSelect: async (e: number) => { + localStorage.setItem('scroll_grid_albumList', '0'); + await queryClient.cancelQueries(['albumList']); + dispatch( + setPagination({ + listType: Item.Album, + data: { + activePage: e, + }, + }) + ); + gridScroll(0); + }, + } + } /> )} diff --git a/src/components/library/AlbumView.tsx b/src/components/library/AlbumView.tsx index 6586e46..a38ba3f 100644 --- a/src/components/library/AlbumView.tsx +++ b/src/components/library/AlbumView.tsx @@ -53,16 +53,16 @@ import { StyledPopover, StyledTagLink, } from '../shared/styled'; -import { setActive } from '../../redux/albumSlice'; import { BlurredBackground, BlurredBackgroundWrapper, PageHeaderSubtitleDataLine, } from '../layout/styled'; import { apiController } from '../../api/controller'; -import { Artist, Genre, Server } from '../../types'; +import { Artist, Genre, Item, Server } from '../../types'; import { setPlaylistRate } from '../../redux/playlistSlice'; import Card from '../card/Card'; +import { setFilter, setPagination } from '../../redux/viewSlice'; interface AlbumParams { id: string; @@ -72,7 +72,6 @@ const AlbumView = ({ ...rest }: any) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const misc = useAppSelector((state) => state.misc); - const album = useAppSelector((state) => state.album); const config = useAppSelector((state) => state.config); const history = useHistory(); const queryClient = useQueryClient(); @@ -399,7 +398,15 @@ const AlbumView = ({ ...rest }: any) => { tabIndex={0} onClick={() => { if (!rest.isModal) { - dispatch(setActive({ ...album.active, filter: d.title })); + dispatch( + setFilter({ + listType: Item.Album, + data: d.title, + }) + ); + dispatch( + setPagination({ listType: Item.Album, data: { activePage: 1 } }) + ); localStorage.setItem('scroll_list_albumList', '0'); localStorage.setItem('scroll_grid_albumList', '0'); setTimeout(() => { @@ -411,7 +418,15 @@ const AlbumView = ({ ...rest }: any) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); if (!rest.isModal) { - dispatch(setActive({ ...album.active, filter: d.title })); + dispatch( + setFilter({ + listType: Item.Album, + data: d.title, + }) + ); + dispatch( + setPagination({ listType: Item.Album, data: { activePage: 1 } }) + ); localStorage.setItem('scroll_list_albumList', '0'); localStorage.setItem('scroll_grid_albumList', '0'); setTimeout(() => { diff --git a/src/components/library/ArtistView.tsx b/src/components/library/ArtistView.tsx index 9d49c92..2e96dbe 100644 --- a/src/components/library/ArtistView.tsx +++ b/src/components/library/ArtistView.tsx @@ -62,7 +62,7 @@ import ScrollingMenu from '../scrollingmenu/ScrollingMenu'; import useColumnSort from '../../hooks/useColumnSort'; import { setPlaylistRate } from '../../redux/playlistSlice'; import CustomTooltip from '../shared/CustomTooltip'; -import { setActive } from '../../redux/albumSlice'; +import { setFilter, setPagination } from '../../redux/viewSlice'; const fac = new FastAverageColor(); @@ -77,7 +77,6 @@ const ArtistView = ({ ...rest }: any) => { const history = useHistory(); const location = useLocation(); const misc = useAppSelector((state) => state.misc); - const album = useAppSelector((state) => state.album); const config = useAppSelector((state) => state.config); const folder = useAppSelector((state) => state.folder); const [viewType, setViewType] = useState(settings.getSync('albumViewType') || 'list'); @@ -609,7 +608,16 @@ const ArtistView = ({ ...rest }: any) => { tabIndex={0} onClick={() => { if (!rest.isModal) { - dispatch(setActive({ ...album.active, filter: d.title })); + dispatch( + setFilter({ + listType: Item.Album, + data: d.title, + }) + ); + dispatch( + setPagination({ listType: Item.Album, data: { activePage: 1 } }) + ); + localStorage.setItem('scroll_list_albumList', '0'); localStorage.setItem('scroll_grid_albumList', '0'); setTimeout(() => { @@ -621,7 +629,19 @@ const ArtistView = ({ ...rest }: any) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); if (!rest.isModal) { - dispatch(setActive({ ...album.active, filter: d.title })); + dispatch( + setFilter({ + listType: Item.Album, + data: d.title, + }) + ); + dispatch( + setPagination({ + listType: Item.Album, + data: { activePage: 1 }, + }) + ); + localStorage.setItem('scroll_list_albumList', '0'); localStorage.setItem('scroll_grid_albumList', '0'); setTimeout(() => { diff --git a/src/components/library/GenreList.tsx b/src/components/library/GenreList.tsx index b8977de..d4c5867 100644 --- a/src/components/library/GenreList.tsx +++ b/src/components/library/GenreList.tsx @@ -15,16 +15,16 @@ import { toggleRangeSelected, toggleSelected, } from '../../redux/multiSelectSlice'; -import { setActive } from '../../redux/albumSlice'; import { apiController } from '../../api/controller'; import { StyledTag } from '../shared/styled'; +import { setFilter, setPagination } from '../../redux/viewSlice'; +import { Item } from '../../types'; const GenreList = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const history = useHistory(); const config = useAppSelector((state) => state.config); - const album = useAppSelector((state) => state.album); const misc = useAppSelector((state) => state.misc); const folder = useAppSelector((state) => state.folder); const { isLoading, isError, data: genres, error }: any = useQuery(['genrePageList'], async () => { @@ -56,7 +56,8 @@ const GenreList = () => { const handleRowDoubleClick = (rowData: any) => { window.clearTimeout(timeout); timeout = null; - dispatch(setActive({ ...album.active, filter: rowData.title })); + dispatch(setFilter({ listType: Item.Album, data: rowData.title })); + dispatch(setPagination({ listType: Item.Album, data: { activePage: 1 } })); localStorage.setItem('scroll_list_albumList', '0'); localStorage.setItem('scroll_grid_albumList', '0'); dispatch(clearSelected()); diff --git a/src/components/library/MusicList.tsx b/src/components/library/MusicList.tsx index 1ab8c7e..24ce333 100644 --- a/src/components/library/MusicList.tsx +++ b/src/components/library/MusicList.tsx @@ -20,16 +20,17 @@ import { StyledInputPicker, StyledInputPickerContainer, StyledTag } from '../sha import { RefreshButton } from '../shared/ToolbarButtons'; import { setSearchQuery } from '../../redux/miscSlice'; import { apiController } from '../../api/controller'; -import { Item } from '../../types'; +import { Item, Server } from '../../types'; import useColumnSort from '../../hooks/useColumnSort'; import { fixPlayer2Index, setPlayQueueByRowClick, setStar } from '../../redux/playQueueSlice'; import { setFilter, setPagination } from '../../redux/viewSlice'; import { setStatus } from '../../redux/playerSlice'; +import useListScroll from '../../hooks/useListScroll'; +// prettier-ignore export const MUSIC_SORT_TYPES = [ { label: i18next.t('A-Z (Name)'), value: 'alphabeticalByName', role: i18next.t('Default') }, { label: i18next.t('A-Z (Album)'), value: 'alphabeticalByAlbum', role: i18next.t('Default') }, - // eslint-disable-next-line prettier/prettier { label: i18next.t('A-Z (Album Artist)'), value: 'alphabeticalByArtist', role: i18next.t('Default') }, { label: i18next.t('A-Z (Artist)'), value: 'alphabeticalByTrackArtist', replacement: 'Artist' }, { label: i18next.t('Most Played'), value: 'frequent', role: i18next.t('Default') }, @@ -50,32 +51,30 @@ const MusicList = () => { const [isRefreshing, setIsRefreshing] = useState(false); const [sortTypes, setSortTypes] = useState([]); const [musicFolder, setMusicFolder] = useState({ loaded: false, id: undefined }); - const musicFilterPickerContainerRef = useRef(null); const [currentQueryKey, setCurrentQueryKey] = useState(['musicList']); + const listRef = useRef(); + const { listScroll } = useListScroll(listRef); + useEffect(() => { if (folder.applied.music) { setMusicFolder({ loaded: true, id: folder.musicFolder }); } else { setMusicFolder({ loaded: true, id: undefined }); } + }, [folder.applied.music, folder.musicFolder]); + useEffect(() => { setCurrentQueryKey([ 'musicList', view.music.filter, view.music.pagination.activePage, musicFolder.id, ]); - }, [ - folder.applied.music, - folder.musicFolder, - musicFolder.id, - view.music.filter, - view.music.pagination.activePage, - ]); + }, [musicFolder.id, view.music.filter, view.music.pagination.activePage]); - const { isLoading, isError, data: musicData, error }: any = useQuery( + const { isLoading, isError, data: songs, error }: any = useQuery( currentQueryKey, () => view.music.filter === 'random' || view.music.pagination.recordsPerPage !== 0 @@ -116,32 +115,52 @@ const MusicList = () => { cacheTime: view.music.pagination.recordsPerPage !== 0 ? 600000 : Infinity, staleTime: view.music.pagination.recordsPerPage !== 0 ? 600000 : Infinity, enabled: currentQueryKey !== ['musicList'] && musicFolder.loaded, - onSuccess: (e) => { - dispatch( - setPagination({ - listType: Item.Music, - data: { - pages: Math.floor(e.totalRecordCount / view.music.pagination.recordsPerPage) + 1, - }, - }) - ); - }, } ); - const searchedData = useSearchQuery(misc.searchQuery, musicData?.data, [ + const searchedData = useSearchQuery(misc.searchQuery, songs?.data, [ 'title', 'artist', 'genre', 'year', ]); - const { sortedData } = useColumnSort(musicData?.data, Item.Album, view.music.sort); + const { sortedData } = useColumnSort(songs?.data, Item.Album, view.music.sort); useEffect(() => { setSortTypes(MUSIC_SORT_TYPES); }, []); + useEffect(() => { + if (songs?.data && sortedData?.length) { + const pages = + Math.floor( + (view.music.pagination.serverSide && config.serverType === Server.Jellyfin + ? songs?.totalRecordCount + : sortedData?.length) / view.music.pagination.recordsPerPage + ) + 1; + + if (pages && view.music.pagination.pages !== pages) { + dispatch( + setPagination({ + listType: Item.Music, + data: { + pages, + }, + }) + ); + } + } + }, [ + songs, + config.serverType, + dispatch, + sortedData?.length, + view.music.pagination.serverSide, + view.music.pagination.recordsPerPage, + view.music.pagination.pages, + ]); + let timeout: any = null; const handleRowClick = (e: any, rowData: any, tableData: any) => { if (timeout === null) { @@ -165,7 +184,7 @@ const MusicList = () => { dispatch(clearSelected()); dispatch( setPlayQueueByRowClick({ - entries: musicData.data, + entries: songs.data, currentIndex: rowData.rowIndex, currentSongId: rowData.id, uniqueSongId: rowData.uniqueId, @@ -244,7 +263,7 @@ const MusicList = () => { <> {t('Songs')}{' '} - {musicData?.totalRecordCount || '...'} + {songs?.totalRecordCount || '...'} } @@ -286,6 +305,7 @@ const MusicList = () => { {isError &&
Error: {error}
} {!isError && ( { }, }) ); + listScroll(0); }, } } diff --git a/src/components/loader/CenterLoader.tsx b/src/components/loader/CenterLoader.tsx new file mode 100644 index 0000000..2f13fd8 --- /dev/null +++ b/src/components/loader/CenterLoader.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Loader } from 'rsuite'; + +const CenterLoader = () => { + return ( +
+ +
+ ); +}; + +export default CenterLoader; diff --git a/src/components/settings/ConfigPanels/LookAndFeelConfig.tsx b/src/components/settings/ConfigPanels/LookAndFeelConfig.tsx index 187e750..221eb21 100644 --- a/src/components/settings/ConfigPanels/LookAndFeelConfig.tsx +++ b/src/components/settings/ConfigPanels/LookAndFeelConfig.tsx @@ -5,7 +5,7 @@ import settings from 'electron-settings'; import { Nav, Icon, RadioGroup, Whisper } from 'rsuite'; import { WhisperInstance } from 'rsuite/lib/Whisper'; import { Trans, useTranslation } from 'react-i18next'; -import { ConfigPanel } from '../styled'; +import { ConfigOptionDescription, ConfigPanel } from '../styled'; import { StyledInputPicker, StyledNavItem, @@ -18,6 +18,7 @@ import { StyledToggle, StyledButton, StyledPopover, + StyledCheckbox, } from '../../shared/styled'; import ListViewConfig from './ListViewConfig'; import { Fonts } from '../Fonts'; @@ -571,27 +572,81 @@ export const PaginationConfigPanel = ({ bordered }: any) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const view = useAppSelector((state) => state.view); + const config = useAppSelector((state) => state.config); return ( - + {t( 'The number of items that will be retrieved per page. Setting this to 0 will disable pagination.' )} + + { - dispatch( - setPagination({ listType: Item.Music, data: { recordsPerPage: Number(e) } }) - ); - settings.setSync('pagination.music', Number(e)); - }} - /> + <> + { + dispatch( + setPagination({ + listType: Item.Music, + data: { activePage: 1, recordsPerPage: Number(e) }, + }) + ); + settings.setSync('pagination.music.recordsPerPage', Number(e)); + }} + /> + {config.serverType === Server.Jellyfin && ( + { + settings.setSync('pagination.music.serverSide', e); + dispatch(setPagination({ listType: Item.Music, data: { serverSide: e } })); + }} + > + {t('Server-side')} + + )} + + } + /> + + { + dispatch( + setPagination({ + listType: Item.Album, + data: { activePage: 1, recordsPerPage: Number(e) }, + }) + ); + settings.setSync('pagination.album.recordsPerPage', Number(e)); + }} + /> + {config.serverType === Server.Jellyfin && ( + { + settings.setSync('pagination.album.serverSide', e); + dispatch(setPagination({ listType: Item.Album, data: { serverSide: e } })); + }} + > + {t('Server-side')} + + )} + } /> diff --git a/src/components/shared/Paginator.tsx b/src/components/shared/Paginator.tsx index 23ed16a..74d1cce 100644 --- a/src/components/shared/Paginator.tsx +++ b/src/components/shared/Paginator.tsx @@ -11,7 +11,16 @@ import { const Paginator = ({ startIndex, endIndex, handleGoToButton, children, ...rest }: any) => { return ( <> - + {children} diff --git a/src/components/shared/setDefaultSettings.ts b/src/components/shared/setDefaultSettings.ts index 8380e15..bd0d068 100644 --- a/src/components/shared/setDefaultSettings.ts +++ b/src/components/shared/setDefaultSettings.ts @@ -131,8 +131,20 @@ const setDefaultSettings = (force: boolean) => { settings.setSync('musicFolder.music', true); } - if (force || !settings.hasSync('pagination.music')) { - settings.setSync('pagination.music', 50); + if (force || !settings.hasSync('pagination.music.recordsPerPage')) { + settings.setSync('pagination.music.recordsPerPage', 50); + } + + if (force || !settings.hasSync('pagination.music.serverSide')) { + settings.setSync('pagination.music.serverSide', true); + } + + if (force || !settings.hasSync('pagination.album.recordsPerPage')) { + settings.setSync('pagination.album.recordsPerPage', 50); + } + + if (force || !settings.hasSync('pagination.album.serverSide')) { + settings.setSync('pagination.album.serverSide', false); } if (force || !settings.hasSync('volume')) { diff --git a/src/components/viewtypes/GridViewType.tsx b/src/components/viewtypes/GridViewType.tsx index 1a0de01..34dc7a4 100644 --- a/src/components/viewtypes/GridViewType.tsx +++ b/src/components/viewtypes/GridViewType.tsx @@ -1,11 +1,13 @@ // Referenced from: https://codesandbox.io/s/jjkz5y130w?file=/index.js:700-703 -import React, { createRef, useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import settings from 'electron-settings'; import { FixedSizeList as List } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; import Card from '../card/Card'; import 'react-virtualized/styles.css'; import { useAppSelector } from '../../redux/hooks'; +import Paginator from '../shared/Paginator'; +import CenterLoader from '../loader/CenterLoader'; const GridCard = ({ data, index, style }: any) => { const { cardHeight, cardWidth, columnCount, gapSize, itemCount } = data; @@ -93,14 +95,13 @@ function ListWrapper({ musicFolderId, initialScrollOffset, onScroll, - refresh, + gridRef, }: any) { const cardHeight = size + 55; const cardWidth = size; // How many cards can we show per row, given the current width? const columnCount = Math.floor((width - gapSize + 3) / (cardWidth + gapSize + 2)); const rowCount = Math.ceil(itemCount / columnCount); - const gridRef = createRef(); const itemData = useMemo( () => ({ @@ -141,28 +142,25 @@ function ListWrapper({ ] ); - useEffect(() => { - if (refresh) { - gridRef.current.scrollTo(0); - } - }, [gridRef, refresh]); - return ( - { - onScroll(scrollOffset); - }} - > - {GridCard} - + <> + { + onScroll(scrollOffset); + }} + overscanCount={4} + > + {GridCard} + + ); } @@ -176,7 +174,9 @@ const GridViewType = ({ handleFavorite, initialScrollOffset, onScroll, - refresh, + paginationProps, + loading, + gridRef, }: any) => { const cacheImages = Boolean(settings.getSync('cacheImages')); const misc = useAppSelector((state) => state.misc); @@ -191,30 +191,48 @@ const GridViewType = ({ }, [folder]); return ( - - {({ height, width }: any) => ( - {})} - refresh={refresh} - /> - )} - + <> + + {({ height, width }: any) => ( + <> + {data?.length ? ( + {})} + paginationProps={paginationProps} + loading={loading} + gridRef={gridRef} + /> + ) : ( + + )} + + {paginationProps && paginationProps?.recordsPerPage !== 0 && ( +
+ +
+ )} + + )} +
+ ); }; diff --git a/src/components/viewtypes/ListViewTable.tsx b/src/components/viewtypes/ListViewTable.tsx index 521cd55..d81bbd0 100644 --- a/src/components/viewtypes/ListViewTable.tsx +++ b/src/components/viewtypes/ListViewTable.tsx @@ -57,11 +57,11 @@ import { setPageSort, setPlaybackFilter, } from '../../redux/configSlice'; -import { setActive } from '../../redux/albumSlice'; import { setStatus } from '../../redux/playerSlice'; -import { GenericItem } from '../../types'; +import { GenericItem, Item } from '../../types'; import { CoverArtWrapper } from '../layout/styled'; import Paginator from '../shared/Paginator'; +import { setFilter, setPagination } from '../../redux/viewSlice'; const StyledTable = styled(Table)<{ rowHeight: number; $isDragging: boolean }>` .rs-table-row.selected { @@ -132,7 +132,6 @@ const ListViewTable = ({ const configState = useAppSelector((state) => state.config); const playQueue = useAppSelector((state) => state.playQueue); const multiSelect = useAppSelector((state) => state.multiSelect); - const album = useAppSelector((state) => state.album); const [sortColumn, setSortColumn] = useState(); const [sortType, setSortType] = useState(); const [sortedData, setSortedData] = useState(data); @@ -682,7 +681,7 @@ const ListViewTable = ({ effect="opacity" width={rowHeight - 10} height={rowHeight - 10} - visibleByDefault={cacheImages.enabled} + visibleByDefault afterLoad={() => { if (cacheImages.enabled) { cacheImage( @@ -862,7 +861,7 @@ const ListViewTable = ({ effect="opacity" width={rowHeight - 10} height={rowHeight - 10} - visibleByDefault={cacheImages.enabled} + visibleByDefault afterLoad={() => { if (cacheImages.enabled) { cacheImage( @@ -955,11 +954,15 @@ const ListViewTable = ({ onClick={(e: any) => { if (!e.ctrlKey && !e.shiftKey) { dispatch( - setActive({ - ...album.active, - filter: genre.title, + setFilter({ listType: Item.Album, data: genre.title }) + ); + dispatch( + setPagination({ + listType: Item.Album, + data: { activePage: 1 }, }) ); + localStorage.setItem('scroll_list_albumList', '0'); localStorage.setItem('scroll_grid_albumList', '0'); setTimeout(() => { diff --git a/src/hooks/useAdvancedFilter.ts b/src/hooks/useAdvancedFilter.ts index f6d6f29..f7bf364 100644 --- a/src/hooks/useAdvancedFilter.ts +++ b/src/hooks/useAdvancedFilter.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import _ from 'lodash'; -import { AdvancedFilters } from '../redux/albumSlice'; +import { AdvancedFilters } from '../redux/viewSlice'; const useAdvancedFilter = (data: any[], filters: AdvancedFilters) => { const [filteredData, setFilteredData] = useState([]); diff --git a/src/hooks/useGridScroll.ts b/src/hooks/useGridScroll.ts new file mode 100644 index 0000000..1303a96 --- /dev/null +++ b/src/hooks/useGridScroll.ts @@ -0,0 +1,16 @@ +import { useCallback } from 'react'; + +const useGridScroll = (ref: any) => { + const gridScroll = useCallback( + (position: number) => { + setTimeout(() => { + ref?.current?.scrollTo(position); + }); + }, + [ref] + ); + + return { gridScroll }; +}; + +export default useGridScroll; diff --git a/src/hooks/useListScroll.ts b/src/hooks/useListScroll.ts new file mode 100644 index 0000000..14a7548 --- /dev/null +++ b/src/hooks/useListScroll.ts @@ -0,0 +1,16 @@ +import { useCallback } from 'react'; + +const useListScroll = (ref: any) => { + const listScroll = useCallback( + (position: number) => { + setTimeout(() => { + ref?.current?.table?.current?.scrollTop(position); + }); + }, + [ref] + ); + + return { listScroll }; +}; + +export default useListScroll; diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 4c5ac41..bdaa2f4 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -148,6 +148,7 @@ "If your audio files are not playing properly or are not in a supported web streaming format, you will need to enable this (requires app restart).": "Wenn Audiodateien nicht richtig abgespielt werden oder nicht in einem unterstützten Webstreaming-Format vorliegen, aktiviere diese Funktion (Neustart der App erforderlich).", "Images": "Bilder", "Integrates with Discord's rich presence to display the currently playing song as your status.": "Integriert sich in Discords Rich Presence um den aktuell gespielten Song als Status anzuzeigen.", + "Items per page (Albums)": "Items per page (Albums)", "Items per page (Songs)": "Elemente pro Seite (Songs)", "Language": "Sprache", "Latest Albums ": "Neuste Alben ", @@ -264,6 +265,7 @@ "Send player updates to your server. This is required by servers such as Jellyfin and Navidrome to track play counts and use external services such as Last.fm.": "Sende Player-Updates an den Server. Dies wird von Servern wie Jellyfin und Navidrome vorrausgesetzt, um die Wiedergaben zu zählen und externe Dienste wie Last.fm zu nutzen.", "Server": "Server", "Server type": "Server Art", + "Server-side": "Server-side", "Session expired. Logging out.": "Sitzung abgelaufen. Abmeldung läuft.", "Set rating": "Bewertung festlegen", "Sets a dynamic background based on the currently playing song.": "Legt einen dynamischen Hintergrund fest, der auf dem aktuell gespielten Lied basiert.", diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index cf0f617..1746307 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -148,6 +148,7 @@ "If your audio files are not playing properly or are not in a supported web streaming format, you will need to enable this (requires app restart).": "If your audio files are not playing properly or are not in a supported web streaming format, you will need to enable this (requires app restart).", "Images": "Images", "Integrates with Discord's rich presence to display the currently playing song as your status.": "Integrates with Discord's rich presence to display the currently playing song as your status.", + "Items per page (Albums)": "Items per page (Albums)", "Items per page (Songs)": "Items per page (Songs)", "Language": "Language", "Latest Albums ": "Latest Albums ", @@ -264,6 +265,7 @@ "Send player updates to your server. This is required by servers such as Jellyfin and Navidrome to track play counts and use external services such as Last.fm.": "Send player updates to your server. This is required by servers such as Jellyfin and Navidrome to track play counts and use external services such as Last.fm.", "Server": "Server", "Server type": "Server type", + "Server-side": "Server-side", "Session expired. Logging out.": "Session expired. Logging out.", "Set rating": "Set rating", "Sets a dynamic background based on the currently playing song.": "Sets a dynamic background based on the currently playing song.", diff --git a/src/redux/albumSlice.ts b/src/redux/albumSlice.ts deleted file mode 100644 index bb07898..0000000 --- a/src/redux/albumSlice.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import settings from 'electron-settings'; -import { mockSettings } from '../shared/mockSettings'; -import { Sort } from '../types'; - -const parsedSettings = process.env.NODE_ENV === 'test' ? mockSettings : settings.getSync(); - -export interface AlbumPage { - active: { - filter: string; - }; - advancedFilters: AdvancedFilters; -} - -export interface AdvancedFilters { - enabled: boolean; - nav: 'filters' | 'sort'; - properties: { - starred: boolean; - genre: { - list: any[]; - type: 'and' | 'or'; - }; - artist: { - list: any[]; - type: 'and' | 'or'; - }; - year: { - from: number; - to: number; - }; - sort: Sort; - }; -} - -const initialState: AlbumPage = { - active: { - filter: - parsedSettings.serverType === 'jellyfin' && - ['frequent', 'recent'].includes(String(parsedSettings.albumSortDefault)) - ? 'random' - : String(parsedSettings.albumSortDefault), - }, - advancedFilters: { - enabled: false, - nav: 'filters', - properties: { - starred: false, - genre: { - list: [], - type: 'and', - }, - artist: { - list: [], - type: 'and', - }, - year: { - from: 0, - to: 0, - }, - sort: { - column: undefined, - type: 'asc', - }, - }, - }, -}; - -const albumSlice = createSlice({ - name: 'album', - initialState, - reducers: { - setActive: (state, action: PayloadAction) => { - state.active = action.payload; - }, - - setAdvancedFilters: ( - state, - action: PayloadAction<{ - filter: 'enabled' | 'starred' | 'genre' | 'artist' | 'year' | 'sort' | 'nav'; - value: any; - }> - ) => { - if (action.payload.filter === 'enabled') { - state.advancedFilters.enabled = action.payload.value; - } - - if (action.payload.filter === 'starred') { - state.advancedFilters.properties.starred = action.payload.value; - } - - if (action.payload.filter === 'genre') { - state.advancedFilters.properties.genre = action.payload.value; - } - - if (action.payload.filter === 'artist') { - state.advancedFilters.properties.artist = action.payload.value; - } - - if (action.payload.filter === 'year') { - state.advancedFilters.properties.year = action.payload.value; - } - - if (action.payload.filter === 'sort') { - state.advancedFilters.properties.sort = action.payload.value; - } - - if (action.payload.filter === 'nav') { - state.advancedFilters.nav = action.payload.value; - } - }, - }, -}); - -export const { setActive, setAdvancedFilters } = albumSlice.actions; -export default albumSlice.reducer; diff --git a/src/redux/store.ts b/src/redux/store.ts index 01ee1ed..2f1c414 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -8,7 +8,6 @@ import playlistReducer from './playlistSlice'; import folderReducer from './folderSlice'; import configReducer from './configSlice'; import favoriteReducer from './favoriteSlice'; -import albumReducer from './albumSlice'; import artistReducer from './artistSlice'; import viewReducer from './viewSlice'; @@ -22,7 +21,6 @@ export const store = configureStore({ folder: folderReducer, config: configReducer, favorite: favoriteReducer, - album: albumReducer, artist: artistReducer, view: viewReducer, }, diff --git a/src/redux/viewSlice.ts b/src/redux/viewSlice.ts index 5b6e65d..44aa25b 100644 --- a/src/redux/viewSlice.ts +++ b/src/redux/viewSlice.ts @@ -5,7 +5,33 @@ import { Item, Sort, Pagination } from '../types'; const parsedSettings: any = process.env.NODE_ENV === 'test' ? mockSettings : settings.getSync(); +export interface AdvancedFilters { + enabled: boolean; + nav: 'filters' | 'sort'; + properties: { + starred: boolean; + genre: { + list: any[]; + type: 'and' | 'or'; + }; + artist: { + list: any[]; + type: 'and' | 'or'; + }; + year: { + from: number; + to: number; + }; + }; +} + export interface View { + album: { + filter: string; + sort: Sort; + advancedFilters: AdvancedFilters; + pagination: Pagination; + }; music: { filter: string; sort: Sort; @@ -13,7 +39,43 @@ export interface View { }; } -const initialState: any = { +const initialState: View = { + album: { + filter: + parsedSettings.serverType === 'jellyfin' && + ['frequent', 'recent'].includes(String(parsedSettings.albumSortDefault)) + ? 'random' + : String(parsedSettings.albumSortDefault), + sort: { + column: undefined, + type: 'asc', + }, + advancedFilters: { + enabled: false, + nav: 'filters', + properties: { + starred: false, + genre: { + list: [], + type: 'and', + }, + artist: { + list: [], + type: 'and', + }, + year: { + from: 0, + to: 0, + }, + }, + }, + pagination: { + serverSide: parsedSettings.pagination.album.serverSide, + recordsPerPage: parsedSettings.pagination.album.recordsPerPage, + activePage: 1, + pages: 1, + }, + }, music: { filter: String(parsedSettings.musicSortDefault) || 'random', sort: { @@ -21,7 +83,8 @@ const initialState: any = { type: 'asc', }, pagination: { - recordsPerPage: parsedSettings.pagination.music, + serverSide: parsedSettings.pagination.music.serverSide, + recordsPerPage: parsedSettings.pagination.music.recordsPerPage, activePage: 1, pages: 1, }, @@ -33,18 +96,75 @@ const viewSlice = createSlice({ initialState, reducers: { setFilter: (state, action: PayloadAction<{ listType: Item; data: any }>) => { + if (action.payload.listType === Item.Album) { + state.album.filter = action.payload.data; + } + if (action.payload.listType === Item.Music) { state.music.filter = action.payload.data; } }, + setAdvancedFilters: ( + state, + action: PayloadAction<{ + listType: Item; + filter: 'enabled' | 'starred' | 'genre' | 'artist' | 'year' | 'nav'; + value: any; + }> + ) => { + if (action.payload.listType === Item.Album) { + if (action.payload.filter === 'enabled') { + state.album.advancedFilters.enabled = action.payload.value; + } + + if (action.payload.filter === 'starred') { + state.album.advancedFilters.properties.starred = action.payload.value; + } + + if (action.payload.filter === 'genre') { + state.album.advancedFilters.properties.genre = action.payload.value; + } + + if (action.payload.filter === 'artist') { + state.album.advancedFilters.properties.artist = action.payload.value; + } + + if (action.payload.filter === 'year') { + state.album.advancedFilters.properties.year = action.payload.value; + } + + if (action.payload.filter === 'nav') { + state.album.advancedFilters.nav = action.payload.value; + } + } + }, + + setColumnSort: (state, action: PayloadAction<{ listType: Item; data: any }>) => { + if (action.payload.listType === Item.Album) { + state.album.sort = action.payload.data; + } + }, + setPagination: ( state, action: PayloadAction<{ listType: Item; - data: { enabled?: boolean; activePage?: number; pages?: number; recordsPerPage?: number }; + data: { + enabled?: boolean; + activePage?: number; + pages?: number; + recordsPerPage?: number; + serverSide?: boolean; + }; }> ) => { + if (action.payload.listType === Item.Album) { + state.album.pagination = { + ...state.album.pagination, + ...action.payload.data, + }; + } if (action.payload.listType === Item.Music) { state.music.pagination = { ...state.music.pagination, @@ -55,5 +175,5 @@ const viewSlice = createSlice({ }, }); -export const { setFilter, setPagination } = viewSlice.actions; +export const { setFilter, setAdvancedFilters, setColumnSort, setPagination } = viewSlice.actions; export default viewSlice.reducer; diff --git a/src/shared/mockSettings.ts b/src/shared/mockSettings.ts index 6367411..3f0b332 100644 --- a/src/shared/mockSettings.ts +++ b/src/shared/mockSettings.ts @@ -26,7 +26,14 @@ export const mockSettings = { scrobble: false, transcode: false, pagination: { - music: 100, + music: { + recordsPerPage: 50, + serverSide: true, + }, + album: { + recordsPerPage: 50, + serverSide: false, + }, }, playbackFilters: [ { diff --git a/src/types.ts b/src/types.ts index 7d54f3b..41781e2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -189,5 +189,6 @@ export interface Sort { export interface Pagination { pages?: number; activePage?: number; + serverSide?: boolean; recordsPerPage: number; }