mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
Merge e79493b7502cfe3faa0fc8604610b0001104f593 into 08a71320eae544af22bab98f119f8669c5c66282
This commit is contained in:
commit
fade205497
@ -31,6 +31,7 @@ import {
|
|||||||
activityReducer,
|
activityReducer,
|
||||||
settingsReducer,
|
settingsReducer,
|
||||||
replayGainReducer,
|
replayGainReducer,
|
||||||
|
infiniteScrollReducer,
|
||||||
downloadMenuDialogReducer,
|
downloadMenuDialogReducer,
|
||||||
shareDialogReducer,
|
shareDialogReducer,
|
||||||
} from './reducers'
|
} from './reducers'
|
||||||
@ -72,6 +73,7 @@ const adminStore = createAdminStore({
|
|||||||
activity: activityReducer,
|
activity: activityReducer,
|
||||||
settings: settingsReducer,
|
settings: settingsReducer,
|
||||||
replayGain: replayGainReducer,
|
replayGain: replayGainReducer,
|
||||||
|
infiniteScroll: infiniteScrollReducer,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -6,3 +6,4 @@ export * from './dialogs'
|
|||||||
export * from './replayGain'
|
export * from './replayGain'
|
||||||
export * from './serverEvents'
|
export * from './serverEvents'
|
||||||
export * from './settings'
|
export * from './settings'
|
||||||
|
export * from './infiniteScroll'
|
||||||
|
|||||||
6
ui/src/actions/infiniteScroll.js
Normal file
6
ui/src/actions/infiniteScroll.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export const SET_INFINITE_SCROLL = 'SET_INFINITE_SCROLL'
|
||||||
|
|
||||||
|
export const setInfiniteScroll = (enabled) => ({
|
||||||
|
type: SET_INFINITE_SCROLL,
|
||||||
|
data: enabled,
|
||||||
|
})
|
||||||
@ -18,6 +18,7 @@ import {
|
|||||||
import FavoriteIcon from '@material-ui/icons/Favorite'
|
import FavoriteIcon from '@material-ui/icons/Favorite'
|
||||||
import { withWidth } from '@material-ui/core'
|
import { withWidth } from '@material-ui/core'
|
||||||
import {
|
import {
|
||||||
|
InfiniteScrollWrapper,
|
||||||
List,
|
List,
|
||||||
QuickFilter,
|
QuickFilter,
|
||||||
Title,
|
Title,
|
||||||
@ -179,6 +180,9 @@ const randomStartingSeed = Math.random().toString()
|
|||||||
const AlbumList = (props) => {
|
const AlbumList = (props) => {
|
||||||
const { width } = props
|
const { width } = props
|
||||||
const albumView = useSelector((state) => state.albumView)
|
const albumView = useSelector((state) => state.albumView)
|
||||||
|
const infiniteScrollEnabled = useSelector(
|
||||||
|
(state) => state.infiniteScroll?.enabled ?? false,
|
||||||
|
)
|
||||||
const [perPage, perPageOptions] = useAlbumsPerPage(width)
|
const [perPage, perPageOptions] = useAlbumsPerPage(width)
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const version = useVersion()
|
const version = useVersion()
|
||||||
@ -234,14 +238,22 @@ const AlbumList = (props) => {
|
|||||||
actions={<AlbumListActions />}
|
actions={<AlbumListActions />}
|
||||||
filters={<AlbumFilter />}
|
filters={<AlbumFilter />}
|
||||||
perPage={perPage}
|
perPage={perPage}
|
||||||
pagination={<Pagination rowsPerPageOptions={perPageOptions} />}
|
pagination={
|
||||||
|
infiniteScrollEnabled ? (
|
||||||
|
false
|
||||||
|
) : (
|
||||||
|
<Pagination rowsPerPageOptions={perPageOptions} />
|
||||||
|
)
|
||||||
|
}
|
||||||
title={<AlbumListTitle albumListType={albumListType} />}
|
title={<AlbumListTitle albumListType={albumListType} />}
|
||||||
>
|
>
|
||||||
{albumView.grid ? (
|
<InfiniteScrollWrapper>
|
||||||
<AlbumGridView albumListType={albumListType} {...props} />
|
{albumView.grid ? (
|
||||||
) : (
|
<AlbumGridView albumListType={albumListType} {...props} />
|
||||||
<AlbumTableView {...props} />
|
) : (
|
||||||
)}
|
<AlbumTableView {...props} />
|
||||||
|
)}
|
||||||
|
</InfiniteScrollWrapper>
|
||||||
</List>
|
</List>
|
||||||
<ExpandInfoDialog content={<AlbumInfo />} />
|
<ExpandInfoDialog content={<AlbumInfo />} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
72
ui/src/common/InfiniteScrollWrapper.jsx
Normal file
72
ui/src/common/InfiniteScrollWrapper.jsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { CircularProgress, Box } from '@material-ui/core'
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import { InfiniteScrollProvider, useInfiniteScroll } from './useInfiniteScroll'
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
sentinel: {
|
||||||
|
width: '100%',
|
||||||
|
height: '1px',
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const InfiniteScrollContent = ({ children, ...props }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const {
|
||||||
|
accumulatedData,
|
||||||
|
accumulatedIds,
|
||||||
|
loading,
|
||||||
|
loaded,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingMore,
|
||||||
|
sentinelRef,
|
||||||
|
} = useInfiniteScroll()
|
||||||
|
|
||||||
|
const infiniteProps = {
|
||||||
|
...props,
|
||||||
|
data: accumulatedData,
|
||||||
|
ids: accumulatedIds,
|
||||||
|
loading,
|
||||||
|
loaded,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{React.Children.map(children, (child) =>
|
||||||
|
React.cloneElement(child, infiniteProps),
|
||||||
|
)}
|
||||||
|
{isFetchingMore && (
|
||||||
|
<Box className={classes.loadingContainer}>
|
||||||
|
<CircularProgress size={24} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{hasNextPage && <div ref={sentinelRef} className={classes.sentinel} />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InfiniteScrollWrapper = ({ children, ...props }) => {
|
||||||
|
const infiniteScrollEnabled = useSelector(
|
||||||
|
(state) => state.infiniteScroll?.enabled ?? false,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!infiniteScrollEnabled) {
|
||||||
|
return React.Children.map(children, (child) =>
|
||||||
|
React.cloneElement(child, props),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfiniteScrollProvider>
|
||||||
|
<InfiniteScrollContent {...props}>{children}</InfiniteScrollContent>
|
||||||
|
</InfiniteScrollProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -41,3 +41,5 @@ export * from './formatRange.js'
|
|||||||
export * from './playlistUtils.js'
|
export * from './playlistUtils.js'
|
||||||
export * from './PathField.jsx'
|
export * from './PathField.jsx'
|
||||||
export * from './ParticipantsInfo'
|
export * from './ParticipantsInfo'
|
||||||
|
export * from './useInfiniteScroll.jsx'
|
||||||
|
export * from './InfiniteScrollWrapper'
|
||||||
|
|||||||
312
ui/src/common/useInfiniteScroll.jsx
Normal file
312
ui/src/common/useInfiniteScroll.jsx
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
} from 'react'
|
||||||
|
import { useListContext, useGetList } from 'react-admin'
|
||||||
|
import { useHistory, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
|
const InfiniteScrollContext = createContext(null)
|
||||||
|
|
||||||
|
const scrollStateCache = new Map()
|
||||||
|
|
||||||
|
const getStorageKey = (pathname, filterValues) => {
|
||||||
|
const filterKey = JSON.stringify(filterValues || {})
|
||||||
|
return `${pathname}:${filterKey}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveScrollState = (pathname, filterValues, pagesLoaded, scrollY) => {
|
||||||
|
const key = getStorageKey(pathname, filterValues)
|
||||||
|
scrollStateCache.set(key, { pagesLoaded, scrollY })
|
||||||
|
}
|
||||||
|
|
||||||
|
const getScrollState = (pathname, filterValues) => {
|
||||||
|
const key = getStorageKey(pathname, filterValues)
|
||||||
|
return scrollStateCache.get(key) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearScrollState = (pathname, filterValues) => {
|
||||||
|
const key = getStorageKey(pathname, filterValues)
|
||||||
|
scrollStateCache.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InfiniteScrollProvider = ({ children }) => {
|
||||||
|
const history = useHistory()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: page1Data,
|
||||||
|
ids: page1Ids,
|
||||||
|
total,
|
||||||
|
perPage,
|
||||||
|
loading: page1Loading,
|
||||||
|
loaded: page1Loaded,
|
||||||
|
resource,
|
||||||
|
filterValues,
|
||||||
|
sort,
|
||||||
|
filter,
|
||||||
|
} = useListContext()
|
||||||
|
|
||||||
|
const [nextPageToFetch, setNextPageToFetch] = useState(null)
|
||||||
|
const [additionalPagesData, setAdditionalPagesData] = useState({})
|
||||||
|
const [isFetchingMore, setIsFetchingMore] = useState(false)
|
||||||
|
const [isRestoring, setIsRestoring] = useState(false)
|
||||||
|
const [targetPage, setTargetPage] = useState(null)
|
||||||
|
const [pendingScrollY, setPendingScrollY] = useState(null)
|
||||||
|
|
||||||
|
const initializedRef = useRef(false)
|
||||||
|
const navigationTypeRef = useRef(null)
|
||||||
|
const sentinelRef = useRef(null)
|
||||||
|
|
||||||
|
const combinedFilter = useMemo(
|
||||||
|
() => ({
|
||||||
|
...filter,
|
||||||
|
...filterValues,
|
||||||
|
}),
|
||||||
|
[filter, filterValues],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined' && window.performance) {
|
||||||
|
const navEntries = window.performance.getEntriesByType('navigation')
|
||||||
|
if (navEntries.length > 0) {
|
||||||
|
navigationTypeRef.current = navEntries[0].type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: additionalData,
|
||||||
|
ids: additionalIds,
|
||||||
|
loaded: additionalLoaded,
|
||||||
|
} = useGetList(
|
||||||
|
resource,
|
||||||
|
{ page: nextPageToFetch || 1, perPage },
|
||||||
|
sort,
|
||||||
|
combinedFilter,
|
||||||
|
{ enabled: nextPageToFetch !== null && nextPageToFetch > 1 },
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxLoadedPage = useMemo(() => {
|
||||||
|
const additionalPages = Object.keys(additionalPagesData).map(Number)
|
||||||
|
if (additionalPages.length > 0) {
|
||||||
|
return Math.max(...additionalPages)
|
||||||
|
}
|
||||||
|
return page1Loaded ? 1 : 0
|
||||||
|
}, [additionalPagesData, page1Loaded])
|
||||||
|
|
||||||
|
const hasNextPage = useMemo(() => {
|
||||||
|
if (!total || !perPage) return false
|
||||||
|
const totalPages = Math.ceil(total / perPage)
|
||||||
|
return maxLoadedPage < totalPages
|
||||||
|
}, [total, perPage, maxLoadedPage])
|
||||||
|
|
||||||
|
const { accumulatedData, accumulatedIds } = useMemo(() => {
|
||||||
|
const accData = { ...page1Data }
|
||||||
|
const accIds = [...(page1Ids || [])]
|
||||||
|
const seenIds = new Set(accIds)
|
||||||
|
|
||||||
|
const sortedPages = Object.keys(additionalPagesData)
|
||||||
|
.map(Number)
|
||||||
|
.sort((a, b) => a - b)
|
||||||
|
|
||||||
|
for (const pageNum of sortedPages) {
|
||||||
|
const pageInfo = additionalPagesData[pageNum]
|
||||||
|
if (pageInfo) {
|
||||||
|
Object.assign(accData, pageInfo.data)
|
||||||
|
for (const id of pageInfo.ids) {
|
||||||
|
if (!seenIds.has(id)) {
|
||||||
|
seenIds.add(id)
|
||||||
|
accIds.push(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { accumulatedData: accData, accumulatedIds: accIds }
|
||||||
|
}, [page1Data, page1Ids, additionalPagesData])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initializedRef.current || !page1Loaded) return
|
||||||
|
initializedRef.current = true
|
||||||
|
|
||||||
|
const isBackNavigation =
|
||||||
|
history.action === 'POP' || navigationTypeRef.current === 'back_forward'
|
||||||
|
|
||||||
|
if (isBackNavigation) {
|
||||||
|
const saved = getScrollState(location.pathname, filterValues)
|
||||||
|
if (saved && saved.pagesLoaded > 1) {
|
||||||
|
setIsRestoring(true)
|
||||||
|
setTargetPage(saved.pagesLoaded)
|
||||||
|
if (saved.scrollY > 0) {
|
||||||
|
setPendingScrollY(saved.scrollY)
|
||||||
|
}
|
||||||
|
setNextPageToFetch(2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearScrollState(location.pathname, filterValues)
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}, [page1Loaded, history.action, location.pathname, filterValues])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
nextPageToFetch &&
|
||||||
|
nextPageToFetch > 1 &&
|
||||||
|
additionalLoaded &&
|
||||||
|
additionalData &&
|
||||||
|
additionalIds
|
||||||
|
) {
|
||||||
|
setAdditionalPagesData((prev) => {
|
||||||
|
if (!prev[nextPageToFetch]) {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[nextPageToFetch]: {
|
||||||
|
data: { ...additionalData },
|
||||||
|
ids: [...additionalIds],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prev
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsFetchingMore(false)
|
||||||
|
|
||||||
|
if (isRestoring && targetPage && nextPageToFetch < targetPage) {
|
||||||
|
setNextPageToFetch(nextPageToFetch + 1)
|
||||||
|
} else if (isRestoring && targetPage && nextPageToFetch >= targetPage) {
|
||||||
|
if (pendingScrollY) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.scrollTo(0, pendingScrollY)
|
||||||
|
setPendingScrollY(null)
|
||||||
|
setIsRestoring(false)
|
||||||
|
setTargetPage(null)
|
||||||
|
}, 100)
|
||||||
|
} else {
|
||||||
|
setIsRestoring(false)
|
||||||
|
setTargetPage(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
nextPageToFetch,
|
||||||
|
additionalLoaded,
|
||||||
|
additionalData,
|
||||||
|
additionalIds,
|
||||||
|
isRestoring,
|
||||||
|
targetPage,
|
||||||
|
pendingScrollY,
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isRestoring && maxLoadedPage > 0 && page1Loaded) {
|
||||||
|
const handleScroll = () => {
|
||||||
|
saveScrollState(
|
||||||
|
location.pathname,
|
||||||
|
filterValues,
|
||||||
|
maxLoadedPage,
|
||||||
|
window.scrollY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleScroll()
|
||||||
|
|
||||||
|
let scrollTimeout
|
||||||
|
const throttledScroll = () => {
|
||||||
|
clearTimeout(scrollTimeout)
|
||||||
|
scrollTimeout = setTimeout(handleScroll, 150)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', throttledScroll, { passive: true })
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', throttledScroll)
|
||||||
|
clearTimeout(scrollTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [maxLoadedPage, isRestoring, location.pathname, filterValues, page1Loaded])
|
||||||
|
|
||||||
|
const loadMore = useCallback(() => {
|
||||||
|
if (!page1Loading && !isFetchingMore && hasNextPage && !isRestoring) {
|
||||||
|
setIsFetchingMore(true)
|
||||||
|
setNextPageToFetch(maxLoadedPage + 1)
|
||||||
|
}
|
||||||
|
}, [page1Loading, isFetchingMore, hasNextPage, maxLoadedPage, isRestoring])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sentinel = sentinelRef.current
|
||||||
|
if (!sentinel) return
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
const entry = entries[0]
|
||||||
|
if (
|
||||||
|
entry.isIntersecting &&
|
||||||
|
hasNextPage &&
|
||||||
|
!page1Loading &&
|
||||||
|
!isFetchingMore &&
|
||||||
|
!isRestoring
|
||||||
|
) {
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: null,
|
||||||
|
rootMargin: '200px',
|
||||||
|
threshold: 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
observer.observe(sentinel)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [hasNextPage, page1Loading, isFetchingMore, loadMore, isRestoring])
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
accumulatedData,
|
||||||
|
accumulatedIds,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingMore,
|
||||||
|
loading: page1Loading || isFetchingMore || isRestoring,
|
||||||
|
sentinelRef,
|
||||||
|
total,
|
||||||
|
loaded: page1Loaded && !isRestoring,
|
||||||
|
loadMore,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
accumulatedData,
|
||||||
|
accumulatedIds,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingMore,
|
||||||
|
page1Loading,
|
||||||
|
isRestoring,
|
||||||
|
total,
|
||||||
|
page1Loaded,
|
||||||
|
loadMore,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfiniteScrollContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</InfiniteScrollContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
export const useInfiniteScroll = () => {
|
||||||
|
const context = useContext(InfiniteScrollContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error(
|
||||||
|
'useInfiniteScroll must be used within an InfiniteScrollProvider',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@ -610,6 +610,7 @@
|
|||||||
"language": "Language",
|
"language": "Language",
|
||||||
"defaultView": "Default View",
|
"defaultView": "Default View",
|
||||||
"desktop_notifications": "Desktop Notifications",
|
"desktop_notifications": "Desktop Notifications",
|
||||||
|
"infinite_scroll": "Infinite scroll on album list",
|
||||||
"lastfmNotConfigured": "Last.fm API-Key is not configured",
|
"lastfmNotConfigured": "Last.fm API-Key is not configured",
|
||||||
"lastfmScrobbling": "Scrobble to Last.fm",
|
"lastfmScrobbling": "Scrobble to Last.fm",
|
||||||
"listenBrainzScrobbling": "Scrobble to ListenBrainz",
|
"listenBrainzScrobbling": "Scrobble to ListenBrainz",
|
||||||
|
|||||||
34
ui/src/personal/InfiniteScrollToggle.jsx
Normal file
34
ui/src/personal/InfiniteScrollToggle.jsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { useTranslate } from 'react-admin'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { setInfiniteScroll } from '../actions'
|
||||||
|
import { FormControl, FormControlLabel, Switch } from '@material-ui/core'
|
||||||
|
|
||||||
|
export const InfiniteScrollToggle = () => {
|
||||||
|
const translate = useTranslate()
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
const currentSetting = useSelector(
|
||||||
|
(state) => state.infiniteScroll?.enabled ?? false,
|
||||||
|
)
|
||||||
|
|
||||||
|
const toggleInfiniteScroll = (event) => {
|
||||||
|
dispatch(setInfiniteScroll(event.target.checked))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormControl>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
id={'infinite-scroll'}
|
||||||
|
color="primary"
|
||||||
|
checked={currentSetting}
|
||||||
|
onChange={toggleInfiniteScroll}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<span>{translate('menu.personal.options.infinite_scroll')}</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import { LastfmScrobbleToggle } from './LastfmScrobbleToggle'
|
|||||||
import { ListenBrainzScrobbleToggle } from './ListenBrainzScrobbleToggle'
|
import { ListenBrainzScrobbleToggle } from './ListenBrainzScrobbleToggle'
|
||||||
import config from '../config'
|
import config from '../config'
|
||||||
import { ReplayGainToggle } from './ReplayGainToggle'
|
import { ReplayGainToggle } from './ReplayGainToggle'
|
||||||
|
import { InfiniteScrollToggle } from './InfiniteScrollToggle'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
root: { marginTop: '1em' },
|
root: { marginTop: '1em' },
|
||||||
@ -26,6 +27,7 @@ const Personal = () => {
|
|||||||
<SelectLanguage />
|
<SelectLanguage />
|
||||||
<SelectDefaultView />
|
<SelectDefaultView />
|
||||||
{config.enableReplayGain && <ReplayGainToggle />}
|
{config.enableReplayGain && <ReplayGainToggle />}
|
||||||
|
<InfiniteScrollToggle />
|
||||||
<NotificationsToggle />
|
<NotificationsToggle />
|
||||||
{config.lastFMEnabled && <LastfmScrobbleToggle />}
|
{config.lastFMEnabled && <LastfmScrobbleToggle />}
|
||||||
{config.listenBrainzEnabled && <ListenBrainzScrobbleToggle />}
|
{config.listenBrainzEnabled && <ListenBrainzScrobbleToggle />}
|
||||||
|
|||||||
@ -6,3 +6,4 @@ export * from './albumView'
|
|||||||
export * from './activityReducer'
|
export * from './activityReducer'
|
||||||
export * from './settingsReducer'
|
export * from './settingsReducer'
|
||||||
export * from './replayGainReducer'
|
export * from './replayGainReducer'
|
||||||
|
export * from './infiniteScrollReducer'
|
||||||
|
|||||||
21
ui/src/reducers/infiniteScrollReducer.js
Normal file
21
ui/src/reducers/infiniteScrollReducer.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { SET_INFINITE_SCROLL } from '../actions'
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
enabled: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const infiniteScrollReducer = (
|
||||||
|
previousState = initialState,
|
||||||
|
payload,
|
||||||
|
) => {
|
||||||
|
const { type, data } = payload
|
||||||
|
switch (type) {
|
||||||
|
case SET_INFINITE_SCROLL:
|
||||||
|
return {
|
||||||
|
...previousState,
|
||||||
|
enabled: data,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return previousState
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -65,6 +65,8 @@ const createAdminStore = ({
|
|||||||
}))(state.player),
|
}))(state.player),
|
||||||
albumView: state.albumView,
|
albumView: state.albumView,
|
||||||
settings: state.settings,
|
settings: state.settings,
|
||||||
|
// Only persist the 'enabled' setting, not transient page/scroll state
|
||||||
|
infiniteScroll: { enabled: state.infiniteScroll?.enabled ?? false },
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
1000,
|
1000,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user