103 lines
3.8 KiB
JavaScript
103 lines
3.8 KiB
JavaScript
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
import { api, errorMessage } from '../lib/api.js'
|
|
|
|
const HISTORY_KEY = 'metatron.drive-upload-history-v1'
|
|
const finalStatuses = new Set(['complete', 'failed', 'canceled'])
|
|
|
|
function readHistory() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]')
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export function useUploads(onComplete) {
|
|
const [uploads, setUploadsState] = useState(readHistory)
|
|
const fileRefs = useRef(new Map())
|
|
const abortRefs = useRef(new Map())
|
|
|
|
const setUploads = useCallback((updater) => {
|
|
setUploadsState((current) => {
|
|
const next = typeof updater === 'function' ? updater(current) : updater
|
|
localStorage.setItem(HISTORY_KEY, JSON.stringify(next.slice(0, 200)))
|
|
return next
|
|
})
|
|
}, [])
|
|
|
|
const update = useCallback((id, patch) => {
|
|
setUploads((current) => current.map((item) => item.id === id ? { ...item, ...patch } : item))
|
|
}, [setUploads])
|
|
|
|
const run = useCallback(async (entry, file) => {
|
|
const controller = new AbortController()
|
|
abortRefs.current.set(entry.id, controller)
|
|
update(entry.id, { status: 'uploading', error: null })
|
|
try {
|
|
await api.upload(file, entry.folderId, {
|
|
signal: controller.signal,
|
|
onProgress: (progress) => update(entry.id, { progress: Math.max(0, Math.min(1, progress)) }),
|
|
})
|
|
update(entry.id, { status: 'complete', progress: 1, finishedAt: new Date().toISOString() })
|
|
window.desktop.notify('Upload complete', `${entry.name} is now in Metatron.Drive`)
|
|
onComplete?.(entry.folderId)
|
|
} catch (requestError) {
|
|
const canceled = requestError?.code === 'ERR_CANCELED'
|
|
update(entry.id, {
|
|
status: canceled ? 'canceled' : 'failed',
|
|
error: canceled ? 'Canceled by user' : errorMessage(requestError),
|
|
finishedAt: new Date().toISOString(),
|
|
})
|
|
if (!canceled) window.desktop.notify('Upload failed', `Could not upload ${entry.name}`)
|
|
} finally {
|
|
abortRefs.current.delete(entry.id)
|
|
}
|
|
}, [onComplete, update])
|
|
|
|
const enqueue = useCallback((files, folderId) => {
|
|
for (const file of files) {
|
|
const id = `${Date.now()}-${crypto.randomUUID()}`
|
|
const entry = {
|
|
id,
|
|
name: file.name,
|
|
size: file.size,
|
|
type: file.type || 'application/octet-stream',
|
|
folderId: folderId || null,
|
|
createdAt: new Date().toISOString(),
|
|
status: 'queued',
|
|
progress: 0,
|
|
error: null,
|
|
}
|
|
fileRefs.current.set(id, file)
|
|
setUploads((current) => [entry, ...current])
|
|
run(entry, file)
|
|
}
|
|
}, [run, setUploads])
|
|
|
|
const cancel = useCallback((id) => abortRefs.current.get(id)?.abort(), [])
|
|
|
|
const retry = useCallback((id) => {
|
|
const file = fileRefs.current.get(id)
|
|
const original = uploads.find((item) => item.id === id)
|
|
if (!file || !original) throw new Error('Select the original file again to retry after restarting the app.')
|
|
const retryEntry = { ...original, id: `${Date.now()}-${crypto.randomUUID()}`, createdAt: new Date().toISOString(), status: 'queued', progress: 0, error: null, finishedAt: null }
|
|
fileRefs.current.set(retryEntry.id, file)
|
|
setUploads((current) => [retryEntry, ...current])
|
|
run(retryEntry, file)
|
|
}, [run, setUploads, uploads])
|
|
|
|
const remove = useCallback((id) => {
|
|
abortRefs.current.get(id)?.abort()
|
|
abortRefs.current.delete(id)
|
|
fileRefs.current.delete(id)
|
|
setUploads((current) => current.filter((item) => item.id !== id))
|
|
}, [setUploads])
|
|
|
|
const clearFinished = useCallback(() => {
|
|
setUploads((current) => current.filter((item) => !finalStatuses.has(item.status)))
|
|
}, [setUploads])
|
|
|
|
const active = useMemo(() => uploads.filter((item) => !finalStatuses.has(item.status)), [uploads])
|
|
return { uploads, active, enqueue, cancel, retry, remove, clearFinished }
|
|
}
|