paginated files list
Massimo Melina committed
Jan 23, 2022 at 10:57 UTC
ef5203333ea48509b2017538e2ddadfd72628839
6 files changed
+102
-29
frontend/src/BrowseFiles.ts
+42
-10
@@ -12,7 +12,7 @@ export function usePath() {
12
}
13
14
export interface DirEntry { n:string, s?:number, m?:string, c?:string,
15
- ext:string, isFolder:boolean, t?:Date, hidden?:boolean } // we memoize these value for speed
15
+ ext:string, isFolder:boolean, t?:Date } // we memoize these value for speed
16
export type DirList = DirEntry[]
17
18
export function BrowseFiles() {
@@ -25,14 +25,46 @@ export function BrowseFiles() {
25
}
26
27
function FilesList() {
28
- const snap = useSnapState()
29
- const { list, loading } = snap
28
+ const { filteredList, list, loading, stoppedSearch } = useSnapState()
29
const midnight = useMidnight() // as an optimization we calculate this only once per list
31
- return h('ul', { className: 'dir' },
32
- !list.length ? (!loading && (snap.stoppedSearch ? 'Stopped before finding anything' : 'Nothing here'))
33
- : list.map((entry: DirEntry) =>
34
- h(Entry, { key: entry.n, midnight, ...entry })),
35
- loading && h(Spinner))
30
+ const pageSize = 100
31
+ const [page, setPage] = useState(0)
32
+ const offset = page * pageSize
33
+ const theList = filteredList || list
34
+ const total = theList.length
35
+
36
+ useEffect(() => setPage(0), [theList])
37
+ useEffect(() => document.scrollingElement?.scrollTo(0,0), [page])
38
+
39
+ return h(Fragment, {},
40
+ h('ul', { className: 'dir' },
41
+ !list.length ? (!loading && (stoppedSearch ? 'Stopped before finding anything' : 'Nothing here'))
42
+ : filteredList && !filteredList.length ? 'No match for this filter'
43
+ : theList.slice(offset, offset + pageSize).map((entry: DirEntry) =>
44
+ h(Entry, { key: entry.n, midnight, ...entry })),
45
+ loading && h(Spinner),
46
+ ),
47
+ total > pageSize && h(Paging, { total, current:page, pageSize, pageChange:setPage })
48
+ )
49
+}
50
+
51
+interface PagingProps {
52
+ total: number
53
+ current: number
54
+ pageSize: number
55
+ pageChange:(newPage:number) => void
56
+}
57
+function Paging({ total, current, pageSize, pageChange }: PagingProps) {
58
+ const nPages = Math.ceil(total / pageSize)
59
+ const pages = []
60
+ for (let i=0; i<nPages; i++)
61
+ pages.push(h('button', {
62
+ ...i===current && { className:'toggled' },
63
+ onClick(){
64
+ pageChange(i)
65
+ }
66
+ }, i*pageSize || 1))
67
+ return h('div', { id:'paging' }, ...pages)
68
}
69
70
function useMidnight() {
@@ -56,13 +88,13 @@ function isMobile() {
88
}
89
90
const Entry = memo(function(entry: DirEntry & { midnight: Date }) {
59
- let { n: relativePath, hidden, isFolder } = entry
91
+ let { n: relativePath, isFolder } = entry
92
const base = usePath()
93
const { showFilter, selected } = useSnapState()
94
const href = fixUrl(relativePath)
95
const containerDir = isFolder ? '' : relativePath.substring(0, relativePath.lastIndexOf('/')+1)
96
const name = relativePath.substring(containerDir.length)
65
- return h('li', { className: (isFolder ? 'folder' : 'file') + (hidden ? ' hidden' : '') },
97
+ return h('li', { className: isFolder ? 'folder' : 'file' },
98
showFilter && h(Checkbox, {
99
value: selected[relativePath],
100
onChange(v){
frontend/src/Head.ts
+4
-3
@@ -15,7 +15,7 @@ export function Head() {
15
}
16
17
function FolderStats() {
18
- const { list, loading, filteredEntries, selected, stoppedSearch } = useSnapState()
18
+ const { list, loading, filteredList, selected, stoppedSearch } = useSnapState()
19
const stats = useMemo(() =>{
20
let files = 0, folders = 0, size = 0
21
for (const x of list) {
@@ -28,15 +28,16 @@ function FolderStats() {
28
return { files, folders, size }
29
}, [list])
30
const sel = Object.keys(selected).length
31
+ const fil = filteredList?.length
32
return h('div', { id:'folder-stats' },
33
stoppedSearch ? hIcon('interrupted', { title:'Search was interrupted' })
33
- : list?.length>0 && loading && h(Spinner),
34
+ : list.length>0 && loading && h(Spinner),
35
[
36
prefix('', stats.files,' file(s)'),
37
prefix('', stats.folders, ' folder(s)'),
38
stats.size ? formatBytes(stats.size) : '',
39
sel && sel+' selected',
39
- filteredEntries >= 0 && filteredEntries+' displayed',
40
+ fil !== undefined && fil < list.length && fil+' displayed',
41
].filter(Boolean).join(', ')
42
)
43
}
frontend/src/index.scss
+40
@@ -1,12 +1,15 @@
1
:root {
2
--bg: #fff;
3
--text: #555;
4
+ --faint-contrast: #0002;
5
--mild-contrast: #0005;
6
+ --good-contrast: #000a;
7
--button-bg: #a4bac9;
8
--button-text: #444;
9
.theme-dark {
10
--bg: #000;
11
--text: #999;
12
+ --faint-contrast: #fff2;
13
--mild-contrast: #fff5;
14
--good-contrast: #fffa;
15
--button-bg: #345;
@@ -31,6 +34,9 @@ body, button, select, input { font-size: 12pt; }
34
#root {
35
max-width: 50em;
36
margin: auto;
37
+ min-height: 100vh;
38
+ display: flex;
39
+ flex-direction: column;
40
}
41
body, input {
42
color: var(--text);
@@ -144,6 +150,7 @@ header input {
150
}
151
152
ul.dir {
153
+ flex: 1;
154
padding: 0;
155
margin: 0;
156
clear: both;
@@ -206,6 +213,39 @@ button label {
213
animation: 1s fade-in;
214
}
215
216
+#paging {
217
+ display: flex;
218
+ position: sticky;
219
+ bottom: 0;
220
+ background: var(--bg);
221
+ gap: .5em;
222
+ overflow-x: auto;
223
+ &>button {
224
+ flex: 1;
225
+ background: var(--button-bg);
226
+ text-align: center;
227
+ }
228
+}
229
+
230
+/* Works on Firefox */
231
+* {
232
+ scrollbar-width: thin;
233
+ scrollbar-color: var(--button-bg) var(--faint-contrast);
234
+}
235
+
236
+/* Works on Chrome, Edge, and Safari */
237
+*::-webkit-scrollbar {
238
+ width: 12px;
239
+}
240
+*::-webkit-scrollbar-track {
241
+ background: var(--faint-contrast);
242
+}
243
+*::-webkit-scrollbar-thumb {
244
+ background-color: var(--button-bg);
245
+ border-radius: 20px;
246
+ border: 1px solid var(--faint-contrast)
247
+}
248
+
249
@media (max-width: 40em) {
250
body, button, select { font-size: 14pt; }
251
#menu-bar {
frontend/src/menu.ts
+5
-6
@@ -73,12 +73,11 @@ export function MenuPanel() {
73
label: 'Invert selection',
74
onClick() {
75
const sel = state.selected
76
- for (const { hidden, n } of state.list)
77
- if (!hidden)
78
- if (sel[n])
79
- delete sel[n]
80
- else
81
- sel[n] = true
76
+ for (const { n } of state.list)
77
+ if (sel[n])
78
+ delete sel[n]
79
+ else
80
+ sel[n] = true
81
}
82
})
83
)
frontend/src/state.ts
+2
-2
@@ -9,6 +9,7 @@ export const state = proxy<{
9
iconsClass: string,
10
username: string,
11
list: DirList,
12
+ filteredList?: DirList,
13
loading: boolean,
14
error: Error | null,
15
listReloader: number,
@@ -16,7 +17,6 @@ export const state = proxy<{
17
showFilter: boolean,
18
selected: Record<string,true>, // optimization: by using an object instead of an array, components are not rendered when the array changes, but only when their specific property change
19
remoteSearch: string,
19
- filteredEntries: number,
20
sortBy: string,
21
invertOrder: boolean,
22
foldersFirst: boolean,
@@ -25,6 +25,7 @@ export const state = proxy<{
25
iconsClass: '',
26
username: '',
27
list: [],
28
+ filteredList: undefined,
29
loading: false,
30
error: null,
31
listReloader: 0,
@@ -32,7 +33,6 @@ export const state = proxy<{
33
showFilter: false,
34
selected: {},
35
remoteSearch: '',
35
- filteredEntries: -1,
36
sortBy: 'name',
37
invertOrder: false,
38
foldersFirst: true,
frontend/src/useFetchList.ts
+9
-8
@@ -31,6 +31,7 @@ export default function useFetchList() {
31
const API = 'file_list'
32
const baseParams = { path:desiredPath, search, sse:true, omit:'c' }
33
state.list = []
34
+ state.filteredList = undefined
35
state.selected = {}
36
state.loading = true
37
state.error = null
@@ -113,12 +114,12 @@ subscribeKey(state, 'invertOrder', sortAgain)
114
subscribeKey(state, 'foldersFirst', sortAgain)
115
116
subscribeKey(state, 'patternFilter', v => {
116
- const filter = v > '' && new RegExp(_.escapeRegExp(v),'i')
117
- let n = 0
118
- for (const entry of state.list) {
119
- entry.hidden = filter && !filter.test(entry.n)
120
- if (!entry.hidden)
121
- ++n
122
- }
123
- state.filteredEntries = filter ? n : -1
117
+ if (!v)
118
+ return state.filteredList = undefined
119
+ const filter = new RegExp(_.escapeRegExp(v),'i')
120
+ const newList = []
121
+ for (const entry of state.list)
122
+ if (filter.test(entry.n))
123
+ newList.push(entry)
124
+ state.filteredList = newList
125
})