2026-06-30 22:24:34 +05:30

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 }
}