From ef5203333ea48509b2017538e2ddadfd72628839 Mon Sep 17 00:00:00 2001 From: Massimo Melina Date: Sun, 23 Jan 2022 10:57:02 +0100 Subject: [PATCH] paginated files list --- frontend/src/BrowseFiles.ts | 52 +++++++++++++++++++++++++++++------- frontend/src/Head.ts | 7 ++--- frontend/src/index.scss | 40 +++++++++++++++++++++++++++ frontend/src/menu.ts | 11 ++++---- frontend/src/state.ts | 4 +-- frontend/src/useFetchList.ts | 17 ++++++------ 6 files changed, 102 insertions(+), 29 deletions(-) diff --git a/frontend/src/BrowseFiles.ts b/frontend/src/BrowseFiles.ts index 7b74366b4..459b81845 100644 --- a/frontend/src/BrowseFiles.ts +++ b/frontend/src/BrowseFiles.ts @@ -12,7 +12,7 @@ export function usePath() { } export interface DirEntry { n:string, s?:number, m?:string, c?:string, - ext:string, isFolder:boolean, t?:Date, hidden?:boolean } // we memoize these value for speed + ext:string, isFolder:boolean, t?:Date } // we memoize these value for speed export type DirList = DirEntry[] export function BrowseFiles() { @@ -25,14 +25,46 @@ export function BrowseFiles() { } function FilesList() { - const snap = useSnapState() - const { list, loading } = snap + const { filteredList, list, loading, stoppedSearch } = useSnapState() const midnight = useMidnight() // as an optimization we calculate this only once per list - return h('ul', { className: 'dir' }, - !list.length ? (!loading && (snap.stoppedSearch ? 'Stopped before finding anything' : 'Nothing here')) - : list.map((entry: DirEntry) => - h(Entry, { key: entry.n, midnight, ...entry })), - loading && h(Spinner)) + const pageSize = 100 + const [page, setPage] = useState(0) + const offset = page * pageSize + const theList = filteredList || list + const total = theList.length + + useEffect(() => setPage(0), [theList]) + useEffect(() => document.scrollingElement?.scrollTo(0,0), [page]) + + return h(Fragment, {}, + h('ul', { className: 'dir' }, + !list.length ? (!loading && (stoppedSearch ? 'Stopped before finding anything' : 'Nothing here')) + : filteredList && !filteredList.length ? 'No match for this filter' + : theList.slice(offset, offset + pageSize).map((entry: DirEntry) => + h(Entry, { key: entry.n, midnight, ...entry })), + loading && h(Spinner), + ), + total > pageSize && h(Paging, { total, current:page, pageSize, pageChange:setPage }) + ) +} + +interface PagingProps { + total: number + current: number + pageSize: number + pageChange:(newPage:number) => void +} +function Paging({ total, current, pageSize, pageChange }: PagingProps) { + const nPages = Math.ceil(total / pageSize) + const pages = [] + for (let i=0; i{ let files = 0, folders = 0, size = 0 for (const x of list) { @@ -28,15 +28,16 @@ function FolderStats() { return { files, folders, size } }, [list]) const sel = Object.keys(selected).length + const fil = filteredList?.length return h('div', { id:'folder-stats' }, stoppedSearch ? hIcon('interrupted', { title:'Search was interrupted' }) - : list?.length>0 && loading && h(Spinner), + : list.length>0 && loading && h(Spinner), [ prefix('', stats.files,' file(s)'), prefix('', stats.folders, ' folder(s)'), stats.size ? formatBytes(stats.size) : '', sel && sel+' selected', - filteredEntries >= 0 && filteredEntries+' displayed', + fil !== undefined && fil < list.length && fil+' displayed', ].filter(Boolean).join(', ') ) } diff --git a/frontend/src/index.scss b/frontend/src/index.scss index bb846f953..8f71f7b0e 100644 --- a/frontend/src/index.scss +++ b/frontend/src/index.scss @@ -1,12 +1,15 @@ :root { --bg: #fff; --text: #555; + --faint-contrast: #0002; --mild-contrast: #0005; + --good-contrast: #000a; --button-bg: #a4bac9; --button-text: #444; .theme-dark { --bg: #000; --text: #999; + --faint-contrast: #fff2; --mild-contrast: #fff5; --good-contrast: #fffa; --button-bg: #345; @@ -31,6 +34,9 @@ body, button, select, input { font-size: 12pt; } #root { max-width: 50em; margin: auto; + min-height: 100vh; + display: flex; + flex-direction: column; } body, input { color: var(--text); @@ -144,6 +150,7 @@ header input { } ul.dir { + flex: 1; padding: 0; margin: 0; clear: both; @@ -206,6 +213,39 @@ button label { animation: 1s fade-in; } +#paging { + display: flex; + position: sticky; + bottom: 0; + background: var(--bg); + gap: .5em; + overflow-x: auto; + &>button { + flex: 1; + background: var(--button-bg); + text-align: center; + } +} + +/* Works on Firefox */ +* { + scrollbar-width: thin; + scrollbar-color: var(--button-bg) var(--faint-contrast); +} + +/* Works on Chrome, Edge, and Safari */ +*::-webkit-scrollbar { + width: 12px; +} +*::-webkit-scrollbar-track { + background: var(--faint-contrast); +} +*::-webkit-scrollbar-thumb { + background-color: var(--button-bg); + border-radius: 20px; + border: 1px solid var(--faint-contrast) +} + @media (max-width: 40em) { body, button, select { font-size: 14pt; } #menu-bar { diff --git a/frontend/src/menu.ts b/frontend/src/menu.ts index 17ab026d2..601e12f9b 100644 --- a/frontend/src/menu.ts +++ b/frontend/src/menu.ts @@ -73,12 +73,11 @@ export function MenuPanel() { label: 'Invert selection', onClick() { const sel = state.selected - for (const { hidden, n } of state.list) - if (!hidden) - if (sel[n]) - delete sel[n] - else - sel[n] = true + for (const { n } of state.list) + if (sel[n]) + delete sel[n] + else + sel[n] = true } }) ) diff --git a/frontend/src/state.ts b/frontend/src/state.ts index 5983c354e..75a363ebe 100644 --- a/frontend/src/state.ts +++ b/frontend/src/state.ts @@ -9,6 +9,7 @@ export const state = proxy<{ iconsClass: string, username: string, list: DirList, + filteredList?: DirList, loading: boolean, error: Error | null, listReloader: number, @@ -16,7 +17,6 @@ export const state = proxy<{ showFilter: boolean, selected: Record, // optimization: by using an object instead of an array, components are not rendered when the array changes, but only when their specific property change remoteSearch: string, - filteredEntries: number, sortBy: string, invertOrder: boolean, foldersFirst: boolean, @@ -25,6 +25,7 @@ export const state = proxy<{ iconsClass: '', username: '', list: [], + filteredList: undefined, loading: false, error: null, listReloader: 0, @@ -32,7 +33,6 @@ export const state = proxy<{ showFilter: false, selected: {}, remoteSearch: '', - filteredEntries: -1, sortBy: 'name', invertOrder: false, foldersFirst: true, diff --git a/frontend/src/useFetchList.ts b/frontend/src/useFetchList.ts index 5f11134cd..1c704a143 100644 --- a/frontend/src/useFetchList.ts +++ b/frontend/src/useFetchList.ts @@ -31,6 +31,7 @@ export default function useFetchList() { const API = 'file_list' const baseParams = { path:desiredPath, search, sse:true, omit:'c' } state.list = [] + state.filteredList = undefined state.selected = {} state.loading = true state.error = null @@ -113,12 +114,12 @@ subscribeKey(state, 'invertOrder', sortAgain) subscribeKey(state, 'foldersFirst', sortAgain) subscribeKey(state, 'patternFilter', v => { - const filter = v > '' && new RegExp(_.escapeRegExp(v),'i') - let n = 0 - for (const entry of state.list) { - entry.hidden = filter && !filter.test(entry.n) - if (!entry.hidden) - ++n - } - state.filteredEntries = filter ? n : -1 + if (!v) + return state.filteredList = undefined + const filter = new RegExp(_.escapeRegExp(v),'i') + const newList = [] + for (const entry of state.list) + if (filter.test(entry.n)) + newList.push(entry) + state.filteredList = newList })