Feat: FileManager (#98) (#703)

* feat: add file manager module

- Complete file manager implementation with UI/UX
- Add drive management functionality
- Add file upload/download with progress tracking
- Add stamp integration and handling
- Add bulk operations and context menus

Co-authored-by: Roland Seres <roland.seres90@gmail.com>
Co-authored-by: nidishk <nidishkrishnan45@gmail.com>
This commit is contained in:
Bálint Ujvári
2025-11-12 11:26:00 +01:00
committed by GitHub
parent 1249c0df71
commit 5bfe2a0331
107 changed files with 21529 additions and 5578 deletions
@@ -0,0 +1,137 @@
import { useCallback, useMemo, useRef, useState, useContext } from 'react'
import type { FileInfo } from '@solarpunkltd/file-manager-lib'
import { Context as FMContext } from '../../../providers/FileManager'
import { startDownloadingQueue } from '../utils/download'
import { formatBytes, getFileId } from '../utils/common'
import { DownloadProgress, TrackDownloadProps } from '../constants/transfers'
export function useBulkActions(opts: {
listToRender: FileInfo[]
trackDownload: (props: TrackDownloadProps) => (dp: DownloadProgress) => void
}) {
const { listToRender, trackDownload } = opts
const { fm } = useContext(FMContext)
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
const allIds = useMemo(() => listToRender.map(getFileId), [listToRender])
const selectedCount = useMemo(() => allIds.filter(id => selectedIds.has(id)).length, [allIds, selectedIds])
const allChecked = useMemo(() => allIds.length > 0 && selectedCount === allIds.length, [allIds.length, selectedCount])
const someChecked = useMemo(() => selectedCount > 0 && !allChecked, [selectedCount, allChecked])
const fileInputRef = useRef<HTMLInputElement | null>(null)
const selectedFiles = useMemo(
() => listToRender.filter(fi => selectedIds.has(getFileId(fi))),
[listToRender, selectedIds],
)
const toggleOne = useCallback((fi: FileInfo, checked: boolean) => {
const id = getFileId(fi)
setSelectedIds(prev => {
const next = new Set(prev)
if (checked) {
next.add(id)
} else {
next.delete(id)
}
return next
})
}, [])
const selectAll = useCallback(() => setSelectedIds(new Set(allIds)), [allIds])
const clearAll = useCallback(() => setSelectedIds(new Set()), [])
const bulkUploadFromPicker = useCallback(() => {
fileInputRef.current?.click()
}, [])
const bulkDownload = useCallback(
async (list: FileInfo[]) => {
if (!fm || !list?.length) return
const trackers: Array<(progress: DownloadProgress) => void> = []
for (const fi of list) {
const rawSize = fi.customMetadata?.size as string | number | undefined
const prettySize = formatBytes(rawSize)
const expected = rawSize ? Number(rawSize) : undefined
const tracker = trackDownload({ name: fi.name, size: prettySize, expectedSize: expected })
trackers.push(tracker)
}
await startDownloadingQueue(fm, list, trackers)
},
[fm, trackDownload],
)
const bulkTrash = useCallback(
async (list: FileInfo[]) => {
if (!fm || !list?.length) return
await Promise.allSettled(list.map(f => fm.trashFile(f)))
clearAll()
},
[fm, clearAll],
)
const bulkRestore = useCallback(
async (list: FileInfo[]) => {
if (!fm || !list?.length) return
await Promise.allSettled(list.map(f => fm.recoverFile(f)))
clearAll()
},
[fm, clearAll],
)
const bulkForget = useCallback(
async (list: FileInfo[]) => {
if (!fm || !list?.length) return
await Promise.allSettled(list.map(f => fm.forgetFile(f)))
clearAll()
},
[fm, clearAll],
)
return useMemo(
() => ({
// selection
selectedIds,
setSelectedIds,
selectedFiles,
selectedCount,
allChecked,
someChecked,
toggleOne,
selectAll,
clearAll,
// file input (for bulk upload)
fileInputRef,
bulkUploadFromPicker,
// actions
bulkDownload,
bulkTrash,
bulkRestore,
bulkForget,
}),
[
selectedIds,
selectedFiles,
selectedCount,
allChecked,
someChecked,
toggleOne,
selectAll,
clearAll,
bulkUploadFromPicker,
bulkDownload,
bulkTrash,
bulkRestore,
bulkForget,
],
)
}