Skip to content

Commit 5a77ed2

Browse files
[Feature] add virtual pagination in game list (library) (#2075)
feat: add virtual pagination in game list (library)
1 parent 4615be6 commit 5a77ed2

File tree

3 files changed

+107
-30
lines changed

3 files changed

+107
-30
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
"react": "^18.2.0",
155155
"react-dom": "^18.2.0",
156156
"react-i18next": "^11.16.7",
157+
"react-infinite-scroll-hook": "^4.0.4",
157158
"react-router-dom": "^6.3.0",
158159
"recharts": "^2.1.14",
159160
"shlex": "^2.1.2",
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import useInfiniteScroll from 'react-infinite-scroll-hook'
2+
import { useState, useEffect, useCallback } from 'react'
3+
4+
// TODO: improvement suggestion: paginate in backend
5+
export default function usePaginatedList<T>(
6+
list: T[],
7+
{ rpp, infinite }: { rpp: number; infinite?: boolean }
8+
) {
9+
const loadPage = useCallback(
10+
(page: number) => {
11+
const offset = rpp * (page - 1)
12+
return list.slice(offset, offset + rpp)
13+
},
14+
[list]
15+
)
16+
17+
const [paginatedList, setPaginatedList] = useState<T[]>(() => loadPage(1))
18+
const [page, setPage] = useState(1)
19+
20+
useEffect(() => {
21+
setPaginatedList(loadPage(1))
22+
}, [loadPage, list])
23+
24+
const hasMore = paginatedList.length !== list.length
25+
26+
const loadMore = useCallback(() => {
27+
if (!hasMore) {
28+
return
29+
}
30+
31+
setPage(page + 1)
32+
const newListPage = loadPage(page + 1)
33+
if (infinite) {
34+
setPaginatedList([...paginatedList, ...newListPage])
35+
} else {
36+
setPaginatedList(newListPage)
37+
}
38+
}, [hasMore, page, paginatedList, loadPage])
39+
40+
const [sentryRef] = useInfiniteScroll({
41+
loading: false,
42+
hasNextPage: hasMore,
43+
onLoadMore: loadMore
44+
})
45+
46+
return {
47+
loadMore: loadMore,
48+
page,
49+
paginatedList,
50+
hasMore,
51+
infiniteScrollSentryRef: sentryRef
52+
}
53+
}

src/frontend/screens/Library/components/GamesList/index.tsx

+53-30
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import cx from 'classnames'
44
import GameCard from '../GameCard'
55
import ContextProvider from 'frontend/state/ContextProvider'
66
import { useTranslation } from 'react-i18next'
7+
import usePaginatedList from 'frontend/hooks/usePaginatedList'
78

89
interface Props {
910
library: GameInfo[]
@@ -29,9 +30,51 @@ const GamesList = ({
2930
const { gameUpdates } = useContext(ContextProvider)
3031
const { t } = useTranslation()
3132

33+
const { infiniteScrollSentryRef, paginatedList, hasMore } = usePaginatedList(
34+
library,
35+
{
36+
rpp: 10,
37+
infinite: true
38+
}
39+
)
40+
41+
const renderGameInfo = (gameInfo: GameInfo) => {
42+
const {
43+
app_name,
44+
is_installed,
45+
runner,
46+
install: { is_dlc }
47+
} = gameInfo
48+
49+
if (is_dlc) {
50+
return null
51+
}
52+
if (!is_installed && onlyInstalled) {
53+
return null
54+
}
55+
56+
const hasUpdate = is_installed && gameUpdates?.includes(app_name)
57+
return (
58+
<GameCard
59+
key={app_name}
60+
hasUpdate={hasUpdate}
61+
buttonClick={() => handleGameCardClick(app_name, runner, gameInfo)}
62+
forceCard={layout === 'grid'}
63+
isRecent={isRecent}
64+
gameInfo={gameInfo}
65+
/>
66+
)
67+
}
68+
3269
return (
3370
<div
34-
style={!library.length ? { backgroundColor: 'transparent' } : {}}
71+
style={
72+
!library.length
73+
? {
74+
backgroundColor: 'transparent'
75+
}
76+
: {}
77+
}
3578
className={cx({
3679
gameList: layout === 'grid',
3780
gameListLayout: layout === 'list',
@@ -46,35 +89,15 @@ const GamesList = ({
4689
<span>{t('wine.actions', 'Action')}</span>
4790
</div>
4891
)}
49-
{!!library.length &&
50-
library.map((gameInfo) => {
51-
const {
52-
app_name,
53-
is_installed,
54-
runner,
55-
install: { is_dlc }
56-
} = gameInfo
57-
if (is_dlc) {
58-
return null
59-
}
60-
if (!is_installed && onlyInstalled) {
61-
return null
62-
}
63-
64-
const hasUpdate = is_installed && gameUpdates?.includes(app_name)
65-
return (
66-
<GameCard
67-
key={app_name}
68-
hasUpdate={hasUpdate}
69-
buttonClick={() =>
70-
handleGameCardClick(app_name, runner, gameInfo)
71-
}
72-
forceCard={layout === 'grid'}
73-
isRecent={isRecent}
74-
gameInfo={gameInfo}
75-
/>
76-
)
77-
})}
92+
{paginatedList.map((item) => {
93+
return renderGameInfo(item)
94+
})}
95+
{hasMore && (
96+
<div
97+
ref={infiniteScrollSentryRef}
98+
style={{ width: 100, height: 40, backgroundColor: 'transparent' }}
99+
/>
100+
)}
78101
</div>
79102
)
80103
}

0 commit comments

Comments
 (0)