migrate mobile with new endpoints
This commit is contained in:
@@ -77,7 +77,6 @@ export function useAutoSync() {
|
||||
|
||||
if (globalMode === 'auto') {
|
||||
// Mode auto global : tous les fichiers locaux sans backendId
|
||||
// (pas de filtre par dossier)
|
||||
} else {
|
||||
// Mode manuel : uniquement les fichiers des dossiers en mode auto
|
||||
const allFolders = safDirectory.getAll();
|
||||
@@ -85,7 +84,7 @@ export function useAutoSync() {
|
||||
allFolders.filter((f) => f.syncMode === 'auto').map((f) => f.id)
|
||||
);
|
||||
pendingFiles = pendingFiles.filter(
|
||||
(entry) => entry.parentFileId && autoFolderIds.has(entry.parentFileId)
|
||||
(entry) => entry.parentResourceId && autoFolderIds.has(entry.parentResourceId)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,7 +110,7 @@ export function useAutoSync() {
|
||||
}
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
isRunning.current = false;
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { apiClient } from '../api/client';
|
||||
import { ENDPOINTS } from '../constants/api';
|
||||
import { Device as DeviceType } from '../types';
|
||||
|
||||
const DEVICE_ID_KEY = 'vaultdrop_device_id';
|
||||
const DEVICE_NAME_KEY = 'vaultdrop_device_name';
|
||||
const DEVICE_SERVER_ID_KEY = 'vaultdrop_device_server_id';
|
||||
|
||||
function generateDefaultDeviceName(): string {
|
||||
const constants = Platform.constants as Record<string, unknown>;
|
||||
const brand = String(constants?.Manufacturer ?? '');
|
||||
const model = String(constants?.Model ?? '');
|
||||
const suffix = Math.random().toString(36).slice(2, 6);
|
||||
const base = [brand, model].filter(Boolean).join(' ') || Platform.OS;
|
||||
return `${Platform.OS === 'ios' ? 'iOS' : 'Android'} ${base} (${suffix})`;
|
||||
}
|
||||
|
||||
async function getOrCreateDeviceId(): Promise<string> {
|
||||
let deviceId = await SecureStore.getItemAsync(DEVICE_ID_KEY);
|
||||
if (!deviceId) {
|
||||
deviceId = `device_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
||||
await SecureStore.setItemAsync(DEVICE_ID_KEY, deviceId);
|
||||
}
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
export async function getStoredDeviceServerId(): Promise<string | null> {
|
||||
return SecureStore.getItemAsync(DEVICE_SERVER_ID_KEY);
|
||||
}
|
||||
|
||||
export async function getStoredDeviceName(): Promise<string | null> {
|
||||
return SecureStore.getItemAsync(DEVICE_NAME_KEY);
|
||||
}
|
||||
|
||||
export function useDeviceRegistration() {
|
||||
const [device, setDevice] = useState<{ localId: string; serverId: string | null; name: string } | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isRegistered, setIsRegistered] = useState<boolean | null>(null);
|
||||
|
||||
const register = useCallback(async (deviceName: string) => {
|
||||
const localId = await getOrCreateDeviceId();
|
||||
const result = await apiClient.post<{ id: string; device_name: string; role: string }>(ENDPOINTS.DEVICES, {
|
||||
device_name: deviceName,
|
||||
});
|
||||
|
||||
await SecureStore.setItemAsync(DEVICE_SERVER_ID_KEY, result.id);
|
||||
await SecureStore.setItemAsync(DEVICE_NAME_KEY, result.device_name);
|
||||
|
||||
setDevice({ localId, serverId: result.id, name: result.device_name });
|
||||
setIsRegistered(true);
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
const checkRegistration = useCallback(async () => {
|
||||
try {
|
||||
const localId = await getOrCreateDeviceId();
|
||||
const storedName = await getStoredDeviceName();
|
||||
|
||||
try {
|
||||
const devices = await apiClient.get<DeviceType[]>(ENDPOINTS.DEVICES);
|
||||
if (devices.length > 0) {
|
||||
const existing = devices[0];
|
||||
await SecureStore.setItemAsync(DEVICE_SERVER_ID_KEY, existing.id);
|
||||
const name = storedName || existing.device_name;
|
||||
setDevice({ localId, serverId: existing.id, name });
|
||||
setIsRegistered(true);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Not registered yet
|
||||
}
|
||||
|
||||
// Auto-register with a generated name instead of blocking
|
||||
const autoName = storedName || generateDefaultDeviceName();
|
||||
try {
|
||||
await register(autoName);
|
||||
} catch {
|
||||
setDevice({ localId, serverId: null, name: autoName });
|
||||
setIsRegistered(false);
|
||||
}
|
||||
} catch {
|
||||
setIsRegistered(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [register]);
|
||||
|
||||
useEffect(() => {
|
||||
checkRegistration();
|
||||
}, [checkRegistration]);
|
||||
|
||||
return {
|
||||
device,
|
||||
isLoading,
|
||||
isRegistered,
|
||||
register,
|
||||
checkRegistration,
|
||||
};
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { downloadAsync, documentDirectory, makeDirectoryAsync, getInfoAsync, deleteAsync } from 'expo-file-system/legacy';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '../api/client';
|
||||
import { FileItem, PaginatedResponse } from '../types';
|
||||
|
||||
const CACHE_DIR = `${documentDirectory}file-images/`;
|
||||
|
||||
function getCacheKey(name: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0;
|
||||
}
|
||||
return Math.abs(hash).toString(36);
|
||||
}
|
||||
|
||||
function getExtension(name: string): string {
|
||||
const dot = name.lastIndexOf('.');
|
||||
return dot >= 0 ? name.slice(dot) : '.jpg';
|
||||
}
|
||||
|
||||
export function useFileImage(url: string | undefined, fileName: string | undefined) {
|
||||
const [localUri, setLocalUri] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const refreshUrl = useCallback(async (): Promise<string | null> => {
|
||||
if (!fileName) return null;
|
||||
try {
|
||||
const res = await apiClient.getFileUrl(fileName);
|
||||
const freshUrl = res.data.url;
|
||||
|
||||
queryClient.setQueriesData<PaginatedResponse<FileItem>>(
|
||||
{ queryKey: ['files'] },
|
||||
(old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
data: old.data.map((f) =>
|
||||
f.name === fileName ? { ...f, url: freshUrl } : f
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return freshUrl;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, [fileName, queryClient]);
|
||||
|
||||
const download = useCallback(async (downloadUrl: string, fileUri: string) => {
|
||||
await makeDirectoryAsync(CACHE_DIR, { intermediates: true });
|
||||
const result = await downloadAsync(downloadUrl, fileUri);
|
||||
return result.uri;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!url || !fileName) return;
|
||||
|
||||
const resolvedUrl = url;
|
||||
let cancelled = false;
|
||||
const cacheKey = getCacheKey(fileName);
|
||||
const ext = getExtension(fileName);
|
||||
const fileUri = `${CACHE_DIR}${cacheKey}${ext}`;
|
||||
|
||||
async function load() {
|
||||
const info = await getInfoAsync(fileUri);
|
||||
if (info.exists) {
|
||||
if (!cancelled) setLocalUri(info.uri);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const uri = await download(resolvedUrl, fileUri);
|
||||
if (!cancelled) setLocalUri(uri);
|
||||
} catch {
|
||||
const freshUrl = await refreshUrl();
|
||||
if (freshUrl && !cancelled) {
|
||||
try {
|
||||
await deleteAsync(fileUri, { idempotent: true });
|
||||
const uri = await download(freshUrl, fileUri);
|
||||
if (!cancelled) setLocalUri(uri);
|
||||
} catch {
|
||||
if (!cancelled) setLocalUri(null);
|
||||
}
|
||||
} else if (!cancelled) {
|
||||
setLocalUri(null);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [url, fileName, download, refreshUrl]);
|
||||
|
||||
return { localUri, loading };
|
||||
}
|
||||
+33
-40
@@ -8,7 +8,7 @@ function recordToUnifiedItem(record: ReturnType<typeof fileStore.getById>): Unif
|
||||
if (!record) return null;
|
||||
return {
|
||||
id: record.id,
|
||||
backendFileId: record.backendId ?? undefined,
|
||||
backendResourceId: record.backendId ?? undefined,
|
||||
name: record.name,
|
||||
mimeType: record.mimeType,
|
||||
size: record.size,
|
||||
@@ -20,7 +20,8 @@ function recordToUnifiedItem(record: ReturnType<typeof fileStore.getById>): Unif
|
||||
ocrText: record.ocrText ?? undefined,
|
||||
tags: record.tags ?? [],
|
||||
isFolder: record.isFolder === 1,
|
||||
parentFileId: record.parentFileId ?? undefined,
|
||||
parentResourceId: record.parentResourceId ?? undefined,
|
||||
ownerId: record.ownerId ?? undefined,
|
||||
thumbnailUrl: record.thumbnailUrl ?? undefined,
|
||||
isDeviceFile: record.source === 'local' && !record.backendId,
|
||||
};
|
||||
@@ -38,15 +39,15 @@ function recordsToUnifiedItems(records: ReturnType<typeof fileStore.getPaginated
|
||||
|
||||
export function useFiles(parentId?: string | null, page: number = 1, limit: number = 50) {
|
||||
const queryKey = parentId
|
||||
? ['files', parentId]
|
||||
: ['files', 'root', page, limit];
|
||||
? ['resources', parentId]
|
||||
: ['resources', 'root', page, limit];
|
||||
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: async () => {
|
||||
if (parentId) {
|
||||
const backendRes = await apiClient.get<{ data: FileItem[] }>(
|
||||
`/files/folders/${parentId}/files?thumbnail=thumbnail`,
|
||||
`${ENDPOINTS.RESOURCES}/folders/${parentId}/resources?thumbnail=thumbnail_small`,
|
||||
);
|
||||
fileStore.mergeFromBackend(
|
||||
backendRes.data.map((f) => ({
|
||||
@@ -59,7 +60,8 @@ export function useFiles(parentId?: string | null, page: number = 1, limit: numb
|
||||
ocrText: f.ocrText,
|
||||
tags: f.tags,
|
||||
isFolder: f.isFolder,
|
||||
parentFileId: f.parentFileId,
|
||||
parentResourceId: f.parentResourceId,
|
||||
ownerId: f.ownerId,
|
||||
thumbnailUrl: f.thumbnailUrl,
|
||||
})),
|
||||
);
|
||||
@@ -72,7 +74,7 @@ export function useFiles(parentId?: string | null, page: number = 1, limit: numb
|
||||
}
|
||||
|
||||
const backendRes = await apiClient.get<PaginatedResponse<FileItem>>(
|
||||
`${ENDPOINTS.FILES}?page=${page}&limit=${limit}&thumbnail=thumbnail`,
|
||||
`${ENDPOINTS.RESOURCES}?page=${page}&limit=${limit}&thumbnail=thumbnail_small`,
|
||||
);
|
||||
fileStore.mergeFromBackend(
|
||||
backendRes.data.map((f) => ({
|
||||
@@ -85,7 +87,8 @@ export function useFiles(parentId?: string | null, page: number = 1, limit: numb
|
||||
ocrText: f.ocrText,
|
||||
tags: f.tags,
|
||||
isFolder: f.isFolder,
|
||||
parentFileId: f.parentFileId,
|
||||
parentResourceId: f.parentResourceId,
|
||||
ownerId: f.ownerId,
|
||||
thumbnailUrl: f.thumbnailUrl,
|
||||
})),
|
||||
);
|
||||
@@ -110,34 +113,23 @@ export function useFiles(parentId?: string | null, page: number = 1, limit: numb
|
||||
|
||||
export function useFile(id: string) {
|
||||
return useQuery({
|
||||
queryKey: ['files', id],
|
||||
queryFn: () => apiClient.get<FileItem>(`${ENDPOINTS.FILES}/${id}?thumbnail=thumbnail`),
|
||||
queryKey: ['resources', id],
|
||||
queryFn: () => apiClient.get<FileItem>(`${ENDPOINTS.RESOURCES}/${id}?thumbnail=thumbnail_small`),
|
||||
enabled: !!id,
|
||||
});
|
||||
}
|
||||
|
||||
export function useFileImage(fileId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['fileImage', fileId],
|
||||
queryFn: () =>
|
||||
apiClient.get<{ data: { id: string; name: string; url: string; size: number } }>(
|
||||
`${ENDPOINTS.FILE}/${fileId}`,
|
||||
),
|
||||
enabled: !!fileId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteFile() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const result = await apiClient.delete(`${ENDPOINTS.FILES}/${id}`);
|
||||
const result = await apiClient.delete(`${ENDPOINTS.RESOURCES}/${id}`);
|
||||
fileStore.deleteByBackendId(id);
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -146,22 +138,22 @@ export function useAddTags() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ fileId, tags, tagType }: { fileId: string; tags: string[]; tagType?: string }) =>
|
||||
apiClient.post(`${ENDPOINTS.FILES}/${fileId}/tags`, { tags, tag_type: tagType }),
|
||||
mutationFn: ({ fileId, tags }: { fileId: string; tags: string[] }) =>
|
||||
apiClient.post(`${ENDPOINTS.RESOURCES}/${fileId}/tags`, { tags }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useMoveFiles() {
|
||||
export function useMoveResources() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ fileIds, parentFileId }: { fileIds: string[]; parentFileId: string | null }) =>
|
||||
apiClient.post(ENDPOINTS.MOVE, { file_ids: fileIds, parent_file_id: parentFileId }),
|
||||
mutationFn: ({ resourceIds, parentResourceId }: { resourceIds: string[]; parentResourceId: string | null }) =>
|
||||
apiClient.post(ENDPOINTS.MOVE, { resource_ids: resourceIds, parent_resource_id: parentResourceId }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -182,7 +174,8 @@ export function useFolders() {
|
||||
ocrText: f.ocrText,
|
||||
tags: f.tags,
|
||||
isFolder: f.isFolder,
|
||||
parentFileId: f.parentFileId,
|
||||
parentResourceId: f.parentResourceId,
|
||||
ownerId: f.ownerId,
|
||||
thumbnailUrl: f.thumbnailUrl,
|
||||
})),
|
||||
);
|
||||
@@ -209,10 +202,9 @@ export function useDownloadFile() {
|
||||
return file.localUri ?? '';
|
||||
}
|
||||
|
||||
const res = await apiClient.get<{ data: { url: string; name: string } }>(
|
||||
`/files/${file.backendFileId}`,
|
||||
const res = await apiClient.get<{ url: string }>(
|
||||
`${ENDPOINTS.RESOURCES}/${file.backendResourceId ?? file.id}`,
|
||||
);
|
||||
const downloadUrl = res.data.url;
|
||||
|
||||
const { downloadAsync, documentDirectory, makeDirectoryAsync } = await import('expo-file-system/legacy');
|
||||
const DOWNLOAD_DIR = `${documentDirectory}synced-files/`;
|
||||
@@ -227,28 +219,29 @@ export function useDownloadFile() {
|
||||
const ext = dot >= 0 ? file.name.slice(dot) : '';
|
||||
const fileUri = `${DOWNLOAD_DIR}${cacheKey}${ext}`;
|
||||
|
||||
const result = await downloadAsync(downloadUrl, fileUri);
|
||||
const result = await downloadAsync(res.url, fileUri);
|
||||
|
||||
fileStore.upsert({
|
||||
id: file.backendFileId ?? file.id,
|
||||
backendId: file.backendFileId ?? file.id,
|
||||
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',
|
||||
parentFileId: file.parentFileId ?? null,
|
||||
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: ['files'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||
return result.uri;
|
||||
},
|
||||
});
|
||||
@@ -274,7 +267,7 @@ export function useFreeLocalSpace() {
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export function useLocalFiles() {
|
||||
for (const entry of registryEntries) {
|
||||
merged.set(entry.id, {
|
||||
id: entry.id,
|
||||
backendFileId: entry.backendId ?? undefined,
|
||||
backendResourceId: entry.backendId ?? undefined,
|
||||
name: entry.name,
|
||||
mimeType: entry.mimeType,
|
||||
size: entry.size,
|
||||
@@ -42,7 +42,7 @@ export function useLocalFiles() {
|
||||
localUri: entry.localUri ?? undefined,
|
||||
tags: entry.tags ?? [],
|
||||
isFolder: entry.isFolder === 1,
|
||||
parentFileId: entry.parentFileId ?? undefined,
|
||||
parentResourceId: entry.parentResourceId ?? undefined,
|
||||
isDeviceFile: entry.source === 'local' && !entry.backendId,
|
||||
});
|
||||
}
|
||||
@@ -61,7 +61,7 @@ export function useLocalFiles() {
|
||||
tags: [],
|
||||
isFolder: false,
|
||||
isDeviceFile: true,
|
||||
parentFileId: df.folderId,
|
||||
parentResourceId: df.folderId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+23
-19
@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { downloadAsync, documentDirectory, makeDirectoryAsync } from 'expo-file-system/legacy';
|
||||
import { fileStore } from '../services/fileStore';
|
||||
import { apiClient } from '../api/client';
|
||||
import { ENDPOINTS } from '../constants/api';
|
||||
import { setIsSyncing } from './useSyncQueue';
|
||||
|
||||
const SYNC_DIR = `${documentDirectory}synced-files/`;
|
||||
@@ -25,9 +26,11 @@ export function usePullSync() {
|
||||
size: number;
|
||||
createdAt: string;
|
||||
url?: string;
|
||||
}> }>('/files?page=1&limit=100&thumbnail=thumbnail');
|
||||
thumbnailUrl?: string;
|
||||
ownerId?: string;
|
||||
}> }>(`${ENDPOINTS.RESOURCES}?page=1&limit=100&thumbnail=thumbnail_small`);
|
||||
|
||||
const backendFiles = res.data ?? [];
|
||||
const backendResources = res.data ?? [];
|
||||
const registry = fileStore.getAllSynced();
|
||||
const existingBackendIds = new Set(
|
||||
registry.filter((e) => e.backendId).map((e) => e.backendId)
|
||||
@@ -35,35 +38,36 @@ export function usePullSync() {
|
||||
|
||||
let pulled = 0;
|
||||
|
||||
for (const bf of backendFiles) {
|
||||
if (existingBackendIds.has(bf.id)) continue;
|
||||
if (bf.size === 0) continue;
|
||||
for (const br of backendResources) {
|
||||
if (existingBackendIds.has(br.id)) continue;
|
||||
if (br.size === 0) continue;
|
||||
|
||||
try {
|
||||
const detail = await apiClient.get<{ data: { url: string } }>(`/files/${bf.id}`);
|
||||
const downloadUrl = detail.data.url;
|
||||
const detail = await apiClient.get<{ url: string }>(`${ENDPOINTS.RESOURCES}/${br.id}`);
|
||||
const downloadUrl = detail.url;
|
||||
|
||||
await makeDirectoryAsync(SYNC_DIR, { intermediates: true });
|
||||
const safeName = bf.name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const fileUri = `${SYNC_DIR}${bf.id}_${safeName}`;
|
||||
const safeName = br.name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||
const fileUri = `${SYNC_DIR}${br.id}_${safeName}`;
|
||||
|
||||
const result = await downloadAsync(downloadUrl, fileUri);
|
||||
|
||||
fileStore.upsert({
|
||||
id: bf.id,
|
||||
backendId: bf.id,
|
||||
name: bf.name,
|
||||
mimeType: bf.mimeType,
|
||||
size: bf.size,
|
||||
id: br.id,
|
||||
backendId: br.id,
|
||||
name: br.name,
|
||||
mimeType: br.mimeType,
|
||||
size: br.size,
|
||||
source: 'synced',
|
||||
localUri: result.uri,
|
||||
syncStatus: 'synced',
|
||||
parentFileId: null,
|
||||
parentResourceId: null,
|
||||
isFolder: 0,
|
||||
ocrText: null,
|
||||
thumbnailUrl: null,
|
||||
createdAt: bf.createdAt,
|
||||
updatedAt: bf.createdAt,
|
||||
thumbnailUrl: br.thumbnailUrl ?? null,
|
||||
ownerId: br.ownerId ?? null,
|
||||
createdAt: br.createdAt,
|
||||
updatedAt: br.createdAt,
|
||||
lastSyncedAt: new Date().toISOString(),
|
||||
});
|
||||
pulled++;
|
||||
@@ -73,7 +77,7 @@ export function usePullSync() {
|
||||
}
|
||||
|
||||
if (pulled > 0) {
|
||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||
}
|
||||
|
||||
return { pulled };
|
||||
|
||||
+41
-13
@@ -1,15 +1,43 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiClient } from '../api/client';
|
||||
import { ENDPOINTS } from '../constants/api';
|
||||
import { FileItem, PaginatedResponse } from '../types';
|
||||
import { useMemo } from 'react';
|
||||
import { fileStore } from '../services/fileStore';
|
||||
import { UnifiedFileItem } from '../types';
|
||||
|
||||
export function useSearch(query: string, page: number = 1, limit: number = 20) {
|
||||
return useQuery({
|
||||
queryKey: ['search', query, page, limit],
|
||||
queryFn: () =>
|
||||
apiClient.get<PaginatedResponse<FileItem>>(
|
||||
`${ENDPOINTS.FILES}/search?q=${encodeURIComponent(query)}&page=${page}&limit=${limit}`
|
||||
),
|
||||
enabled: query.length > 0,
|
||||
});
|
||||
function recordToUnifiedItem(record: ReturnType<typeof fileStore.getById>): 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,
|
||||
};
|
||||
}
|
||||
|
||||
export function useSearch(query: string) {
|
||||
const results = useMemo(() => {
|
||||
if (!query.trim()) return [];
|
||||
const records = fileStore.searchFts(query);
|
||||
if (records.length === 0) {
|
||||
const fallback = fileStore.search(query);
|
||||
return fallback.map((r) => recordToUnifiedItem(r)!).filter(Boolean);
|
||||
}
|
||||
return records.map((r) => recordToUnifiedItem(r)!).filter(Boolean);
|
||||
}, [query]);
|
||||
|
||||
return {
|
||||
data: results,
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '../api/client';
|
||||
import { ShareEntry } from '../types';
|
||||
|
||||
function shareEndpoint(resourceId: string) {
|
||||
return `/resources/${resourceId}/share`;
|
||||
}
|
||||
|
||||
export function useShares(resourceId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['shares', resourceId],
|
||||
queryFn: () => apiClient.get<ShareEntry[]>(shareEndpoint(resourceId)),
|
||||
enabled: !!resourceId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useGrantShare() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ resourceId, subjectUserId, role }: { resourceId: string; subjectUserId: string; role: string }) =>
|
||||
apiClient.post(shareEndpoint(resourceId), { subject_user_id: subjectUserId, role }),
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['shares', variables.resourceId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRevokeShare() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ resourceId, userId }: { resourceId: string; userId: string }) =>
|
||||
apiClient.delete(`${shareEndpoint(resourceId)}/${userId}`),
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['shares', variables.resourceId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useCheckAccess(resourceId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['access', resourceId],
|
||||
queryFn: () => apiClient.get<{ role: string; access: boolean }>(`/resources/${resourceId}/access`),
|
||||
enabled: !!resourceId,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { apiClient } from '../api/client';
|
||||
import { ENDPOINTS } from '../constants/api';
|
||||
import { SyncQueueItem } from '../types';
|
||||
|
||||
export function useSyncPull() {
|
||||
const isRunning = useRef(false);
|
||||
|
||||
const pull = useCallback(async (locationId?: string) => {
|
||||
if (isRunning.current) return { items: [] };
|
||||
isRunning.current = true;
|
||||
|
||||
try {
|
||||
const body = locationId ? { location_id: locationId } : {};
|
||||
const result = await apiClient.post<SyncQueueItem[]>(ENDPOINTS.SYNC_PULL, body);
|
||||
return { items: result };
|
||||
} finally {
|
||||
isRunning.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { pull };
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { apiClient } from '../api/client';
|
||||
import { ENDPOINTS } from '../constants/api';
|
||||
|
||||
export function useSyncPush() {
|
||||
const isRunning = useRef(false);
|
||||
|
||||
const push = useCallback(async (locationId: string) => {
|
||||
if (isRunning.current) return { pending: 0 };
|
||||
isRunning.current = true;
|
||||
|
||||
try {
|
||||
const result = await apiClient.post<{ pending: number; message: string }>(ENDPOINTS.SYNC_PUSH, {
|
||||
location_id: locationId,
|
||||
});
|
||||
return { pending: result.pending };
|
||||
} finally {
|
||||
isRunning.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { push };
|
||||
}
|
||||
@@ -36,8 +36,8 @@ export function useSyncQueue() {
|
||||
count++;
|
||||
} else {
|
||||
// mode manuel : uniquement les fichiers des dossiers en mode auto
|
||||
if (!entry.parentFileId) continue;
|
||||
const folder = safDirectory.getAll().find((f) => f.id === entry.parentFileId);
|
||||
if (!entry.parentResourceId) continue;
|
||||
const folder = safDirectory.getAll().find((f) => f.id === entry.parentResourceId);
|
||||
if (folder && folder.syncMode === 'auto') {
|
||||
count++;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export function useUpload() {
|
||||
return { uploaded, errors };
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user