import { ref, computed } from 'vue' import { defineStore } from 'pinia' import { fileBrowserClient, type FileBrowserItem } from '@/api/filebrowser-client' export const useCloudStore = defineStore('cloud', () => { const currentPath = ref('/') const items = ref([]) const loading = ref(false) const error = ref(null) const authenticated = ref(false) const upload = ref<{ active: boolean; paused: boolean; filename: string; destination: string; originRoute: string; sent: number; total: number; completed: number; count: number; error: string | null; cancelled: boolean } | null>(null) let uploadController: AbortController | null = null function cancelUpload() { uploadController?.abort() } function dismissUpload() { if (!upload.value?.active) upload.value = null } async function uploadFiles(files: File[], originRoute = '') { if (!files.length || upload.value?.active) return const destination = currentPath.value const controller = new AbortController() uploadController = controller const task = { active: true, paused: false, filename: files[0]!.name, destination, originRoute, sent: 0, total: files.reduce((n, f) => n + f.size, 0), completed: 0, count: files.length, error: null as string | null, cancelled: false } upload.value = task let completedBytes = 0 try { for (const file of files) { if (controller.signal.aborted) throw new DOMException('Upload cancelled', 'AbortError') upload.value.filename = file.name await fileBrowserClient.upload(destination, file, { resumable: true, signal: controller.signal, onPaused: (paused) => { if (upload.value?.active) upload.value.paused = paused }, onProgress: (sent) => { if (upload.value?.active) upload.value.sent = completedBytes + sent } }) completedBytes += file.size upload.value.sent = completedBytes upload.value.completed++ } } catch (error) { upload.value.cancelled = controller.signal.aborted if (!upload.value.cancelled) upload.value.error = error instanceof Error ? error.message : 'Upload failed' } finally { upload.value.active = false uploadController = null // Navigation must never redirect subsequent files into the new folder. pathCache.delete(destination) if (currentPath.value === destination) await refresh() } } // Per-path listing cache: re-entering a folder paints the last listing // immediately (no spinner) while the fresh listing loads behind it. const pathCache = new Map() // Last-wins guard for overlapping navigations (fast folder hopping). let navSeq = 0 const breadcrumbs = computed(() => { const parts = currentPath.value.split('/').filter(Boolean) const crumbs = [{ name: 'Home', path: '/' }] let path = '' for (const part of parts) { path += `/${part}` crumbs.push({ name: part, path }) } return crumbs }) const sortedItems = computed(() => { const dirs = items.value.filter((i) => i.isDir) const files = items.value.filter((i) => !i.isDir) dirs.sort((a, b) => a.name.localeCompare(b.name)) files.sort((a, b) => a.name.localeCompare(b.name)) return [...dirs, ...files] }) async function init(): Promise { if (authenticated.value) return true const ok = await fileBrowserClient.login() authenticated.value = ok return ok } async function navigate(path: string): Promise { const seq = ++navSeq const apply = (p: string, result: FileBrowserItem[]) => { pathCache.set(p, result) if (seq !== navSeq) return // a newer navigation superseded this one items.value = result currentPath.value = p } // Stale-while-revalidate: show the cached listing for this path // immediately (no spinner), then refresh it underneath. const cached = pathCache.get(path) if (cached) { items.value = cached currentPath.value = path } else { loading.value = true } error.value = null try { if (!authenticated.value) { const ok = await init() if (!ok) { error.value = 'Failed to authenticate with File Browser' return } } try { apply(path, await fileBrowserClient.listDirectory(path)) } catch { // Directory may not exist — try to create it, then retry if (path !== '/') { try { const parentPath = path.substring(0, path.lastIndexOf('/')) || '/' const dirName = path.substring(path.lastIndexOf('/') + 1) await fileBrowserClient.createFolder(parentPath, dirName) apply(path, await fileBrowserClient.listDirectory(path)) } catch { // Fall back to root apply('/', await fileBrowserClient.listDirectory('/')) } } else { throw new Error('Failed to list root directory') } } } catch (e) { // Keep showing the cached listing on a failed revalidate. if (!cached) error.value = e instanceof Error ? e.message : 'Failed to load files' } finally { if (seq === navSeq) loading.value = false } } async function refresh(): Promise { await navigate(currentPath.value) } async function uploadFile(file: File): Promise { await fileBrowserClient.upload(currentPath.value, file) await refresh() } async function deleteItem(path: string): Promise { await fileBrowserClient.deleteItem(path) await refresh() } function downloadUrl(path: string): string { return fileBrowserClient.downloadUrl(path) } async function fetchBlobUrl(path: string): Promise { return fileBrowserClient.fetchBlobUrl(path) } async function streamUrl(path: string): Promise { return fileBrowserClient.streamUrl(path) } async function downloadFile(path: string): Promise { return fileBrowserClient.downloadFile(path) } function reset(): void { currentPath.value = '/' items.value = [] loading.value = false error.value = null pathCache.clear() } return { upload, uploadFiles, cancelUpload, dismissUpload, currentPath, items, loading, error, authenticated, breadcrumbs, sortedItems, init, navigate, refresh, uploadFile, deleteItem, downloadUrl, fetchBlobUrl, streamUrl, downloadFile, reset, } })