import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query'; import { apiClient } from '../api/client'; import { ENDPOINTS } from '../constants/api'; import type { UnifiedFileItem, PaginatedResponse, FileItem, Tag } from '../types'; import { fileStore } from '../services/fileStore'; function recordToUnifiedItem(record: ReturnType): UnifiedFileItem | null { if (!record) return null; return { id: record.id, backendResourceId: record.backendId ?? undefined, name: record.name, mimeType: record.mimeType, size: record.size, createdAt: record.createdAt, updatedAt: record.updatedAt, source: record.source as UnifiedFileItem['source'], syncStatus: record.syncStatus as UnifiedFileItem['syncStatus'], localUri: record.localUri ?? undefined, ocrText: record.ocrText ?? undefined, tags: record.tags ?? [], isFolder: record.isFolder === 1, parentResourceId: record.parentResourceId ?? undefined, ownerId: record.ownerId ?? undefined, thumbnailUrl: record.thumbnailUrl ?? undefined, isDeviceFile: record.source === 'local' && !record.backendId, }; } function recordsToUnifiedItems(records: ReturnType): { data: UnifiedFileItem[]; meta: { page: number; total: number }; } { return { data: records.files.map((r) => recordToUnifiedItem(r)!).filter(Boolean), meta: { page: 0, total: records.total }, }; } export function useFiles(parentId?: string | null, page: number = 1, limit: number = 100) { const queryKey = parentId ? ['resources', parentId] : ['resources', 'root', page, limit]; return useQuery({ queryKey, queryFn: async () => { if (parentId) { const backendRes = await apiClient.get<{ data: FileItem[] }>( `${ENDPOINTS.RESOURCES}/folders/${parentId}/resources?thumbnail=thumbnail_small`, ); fileStore.mergeFromBackend( backendRes.data.map((f) => ({ id: f.id, name: f.name, mimeType: f.mimeType, size: f.size, createdAt: f.createdAt, updatedAt: f.updatedAt, ocrText: f.ocrText, tags: f.tags, isFolder: f.isFolder, parentResourceId: f.parentResourceId, ownerId: f.ownerId, thumbnailUrl: f.thumbnailUrl, })), ); const children = fileStore.getChildrenByParent(parentId); return { data: children.map((r) => recordToUnifiedItem(r)!).filter(Boolean), meta: { page: 0, total: children.length }, }; } const backendRes = await apiClient.get>( `${ENDPOINTS.RESOURCES}?page=${page}&limit=${limit}&thumbnail=thumbnail_small`, ); fileStore.mergeFromBackend( backendRes.data.map((f) => ({ id: f.id, name: f.name, mimeType: f.mimeType, size: f.size, createdAt: f.createdAt, updatedAt: f.updatedAt, ocrText: f.ocrText, tags: f.tags, isFolder: f.isFolder, parentResourceId: f.parentResourceId, ownerId: f.ownerId, thumbnailUrl: f.thumbnailUrl, })), ); const cached = fileStore.getRootFiles(); return { data: cached.files.map((r) => recordToUnifiedItem(r)!).filter(Boolean), meta: { page, total: backendRes.meta?.total ?? cached.total }, }; }, placeholderData: keepPreviousData, initialData: () => { const cached = fileStore.getRootFiles(); if (cached.files.length === 0) return undefined; return { data: cached.files.map((r) => recordToUnifiedItem(r)!).filter(Boolean), meta: { page: 0, total: cached.total }, }; }, staleTime: 30_000, }); } export function useFile(id: string) { return useQuery({ queryKey: ['resources', id], queryFn: () => apiClient.get(`${ENDPOINTS.RESOURCES}/${id}?thumbnail=thumbnail_small`), enabled: !!id, }); } export function useDeleteFile() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (id: string) => { const result = await apiClient.delete(`${ENDPOINTS.RESOURCES}/${id}`); fileStore.deleteByBackendId(id); return result; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['resources'] }); }, }); } export function useAddTags() { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ fileId, tags }: { fileId: string; tags: string[] }) => apiClient.post(`${ENDPOINTS.RESOURCES}/${fileId}/tags`, { tags }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['resources'] }); }, }); } export function useMoveResources() { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ resourceIds, parentResourceId }: { resourceIds: string[]; parentResourceId: string | null }) => apiClient.post(ENDPOINTS.MOVE, { resource_ids: resourceIds, parent_resource_id: parentResourceId }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['resources'] }); }, }); } export function useFolders() { return useQuery({ queryKey: ['folders'], queryFn: async () => { const backendRes = await apiClient.get<{ data: FileItem[] }>(ENDPOINTS.FOLDERS); fileStore.mergeFromBackend( backendRes.data.map((f) => ({ id: f.id, name: f.name, mimeType: f.mimeType, size: f.size, createdAt: f.createdAt, updatedAt: f.updatedAt, ocrText: f.ocrText, tags: f.tags, isFolder: f.isFolder, parentResourceId: f.parentResourceId, ownerId: f.ownerId, thumbnailUrl: f.thumbnailUrl, })), ); return fileStore.getAllFolders(); }, initialData: () => { const folders = fileStore.getAllFolders(); return folders.length > 0 ? folders : undefined; }, staleTime: 60_000, }); } export function useFilesByParent(parentId: string) { return useFiles(parentId); } export function useDownloadFile() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (file: UnifiedFileItem): Promise => { if (file.syncStatus !== 'cloud') { return file.localUri ?? ''; } const res = await apiClient.get<{ url: string }>( `${ENDPOINTS.RESOURCES}/${file.backendResourceId ?? file.id}`, ); const { downloadAsync, documentDirectory, makeDirectoryAsync } = await import('expo-file-system/legacy'); const DOWNLOAD_DIR = `${documentDirectory}synced-files/`; await makeDirectoryAsync(DOWNLOAD_DIR, { intermediates: true }); let hash = 0; for (let i = 0; i < file.name.length; i++) { hash = ((hash << 5) - hash + file.name.charCodeAt(i)) | 0; } const cacheKey = Math.abs(hash).toString(36); const dot = file.name.lastIndexOf('.'); const ext = dot >= 0 ? file.name.slice(dot) : ''; const fileUri = `${DOWNLOAD_DIR}${cacheKey}${ext}`; const result = await downloadAsync(res.url, fileUri); fileStore.upsert({ id: file.backendResourceId ?? file.id, backendId: file.backendResourceId ?? file.id, name: file.name, mimeType: file.mimeType, size: file.size, source: 'synced', localUri: result.uri, syncStatus: 'synced', parentResourceId: file.parentResourceId ?? null, isFolder: 0, ocrText: file.ocrText ?? null, thumbnailUrl: file.thumbnailUrl ?? null, ownerId: file.ownerId ?? null, createdAt: file.createdAt, updatedAt: file.updatedAt ?? file.createdAt, lastSyncedAt: new Date().toISOString(), tags: file.tags, }); queryClient.invalidateQueries({ queryKey: ['resources'] }); return result.uri; }, }); } export function useFreeLocalSpace() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (fileIds: string[]) => { const { deleteAsync } = await import('expo-file-system/legacy'); for (const fid of fileIds) { const entry = fileStore.getByBackendId(fid); if (!entry) continue; if (entry.localUri) { try { await deleteAsync(entry.localUri, { idempotent: true }); } catch {} } fileStore.markAsCloudOnly(entry.id); } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['resources'] }); }, }); }