migrate mobile with new endpoints
This commit is contained in:
+23
-7
@@ -7,6 +7,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
|
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
|
||||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||||
|
import { DeviceProvider, useDevice } from './contexts/DeviceContext';
|
||||||
import { LoginScreen } from './app/login';
|
import { LoginScreen } from './app/login';
|
||||||
import { RegisterScreen } from './app/register';
|
import { RegisterScreen } from './app/register';
|
||||||
import { HomeScreen } from './app/index';
|
import { HomeScreen } from './app/index';
|
||||||
@@ -39,9 +40,10 @@ const queryClient = new QueryClient({
|
|||||||
const persister = createMMKVPersister();
|
const persister = createMMKVPersister();
|
||||||
|
|
||||||
function AppNavigator() {
|
function AppNavigator() {
|
||||||
const { user, isLoading } = useAuth();
|
const { user, isLoading: authLoading } = useAuth();
|
||||||
|
const { isLoading: deviceLoading } = useDevice();
|
||||||
|
|
||||||
if (isLoading) {
|
if (authLoading || (user && deviceLoading)) {
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||||
<ActivityIndicator size="large" />
|
<ActivityIndicator size="large" />
|
||||||
@@ -53,7 +55,13 @@ function AppNavigator() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationContainer>
|
<NavigationContainer>
|
||||||
<Stack.Navigator initialRouteName={needsOnboarding ? 'Onboarding' : user ? 'Home' : 'Login'}>
|
<Stack.Navigator
|
||||||
|
initialRouteName={
|
||||||
|
needsOnboarding ? 'Onboarding'
|
||||||
|
: user ? 'Home'
|
||||||
|
: 'Login'
|
||||||
|
}
|
||||||
|
>
|
||||||
{user ? (
|
{user ? (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -94,6 +102,17 @@ function AppNavigator() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AppContent() {
|
||||||
|
return (
|
||||||
|
<AuthProvider>
|
||||||
|
<DeviceProvider>
|
||||||
|
<AppNavigator />
|
||||||
|
<StatusBar style="auto" />
|
||||||
|
</DeviceProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
@@ -104,10 +123,7 @@ export default function App() {
|
|||||||
maxAge: 1000 * 60 * 60 * 24, // 24h
|
maxAge: 1000 * 60 * 60 * 24, // 24h
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AuthProvider>
|
<AppContent />
|
||||||
<AppNavigator />
|
|
||||||
<StatusBar style="auto" />
|
|
||||||
</AuthProvider>
|
|
||||||
</PersistQueryClientProvider>
|
</PersistQueryClientProvider>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -109,10 +109,6 @@ class ApiClient {
|
|||||||
async delete<T>(endpoint: string): Promise<T> {
|
async delete<T>(endpoint: string): Promise<T> {
|
||||||
return this.request<T>(endpoint, { method: 'DELETE' });
|
return this.request<T>(endpoint, { method: 'DELETE' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFileUrl(fileName: string): Promise<{ data: { url: string; name: string } }> {
|
|
||||||
return this.request(`${ENDPOINTS.FILE}/${fileName}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiClient = new ApiClient(API_BASE_URL);
|
export const apiClient = new ApiClient(API_BASE_URL);
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator, Platform } from 'react-native';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import type { NavigationProp } from '@react-navigation/native';
|
||||||
|
import { useDevice } from '../contexts/DeviceContext';
|
||||||
|
|
||||||
|
export function DeviceSetupScreen() {
|
||||||
|
const navigation = useNavigation<NavigationProp<any>>();
|
||||||
|
const { register, deviceName } = useDevice();
|
||||||
|
const [name, setName] = useState(deviceName || `${Platform.OS.charAt(0).toUpperCase() + Platform.OS.slice(1)} de ${Platform.OS === 'ios' ? 'l\'utilisateur' : 'utilisateur'}`);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
if (!name.trim()) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await register(name.trim());
|
||||||
|
navigation.reset({ index: 0, routes: [{ name: 'Home' }] });
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err?.message || "Échec de l'enregistrement");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.content}>
|
||||||
|
<Text style={styles.icon}>📱</Text>
|
||||||
|
<Text style={styles.title}>Enregistrement du device</Text>
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
|
Ce device doit être enregistré comme espace de stockage pour utiliser VaultDrop.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Nom du device"
|
||||||
|
placeholderTextColor="#999"
|
||||||
|
value={name}
|
||||||
|
onChangeText={setName}
|
||||||
|
autoFocus
|
||||||
|
returnKeyType="done"
|
||||||
|
onSubmitEditing={handleRegister}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error && <Text style={styles.error}>{error}</Text>}
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, !name.trim() && styles.buttonDisabled]}
|
||||||
|
onPress={handleRegister}
|
||||||
|
disabled={isLoading || !name.trim()}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<ActivityIndicator size="small" color="#fff" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.buttonText}>Enregistrer</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#f5f5f5',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: 24,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 24,
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 16,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 4,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
fontSize: 48,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#333',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: 15,
|
||||||
|
color: '#666',
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: 22,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
width: '100%',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#ddd',
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 12,
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#333',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
color: '#E53935',
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: '#1976D2',
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
buttonDisabled: {
|
||||||
|
backgroundColor: '#ccc',
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
});
|
||||||
+19
-20
@@ -14,14 +14,14 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { RouteProp, useRoute } from '@react-navigation/native';
|
import { RouteProp, useRoute } from '@react-navigation/native';
|
||||||
import { useFile, useFileImage, useDownloadFile } from '../hooks/useFiles';
|
import { useFile, useDownloadFile } from '../hooks/useFiles';
|
||||||
import { fileStore } from '../services/fileStore';
|
import { fileStore } from '../services/fileStore';
|
||||||
import { TagChip } from '../components/TagChip';
|
import { TagChip } from '../components/TagChip';
|
||||||
import { FileThumbnail } from '../components/FileThumbnail';
|
import { FileThumbnail } from '../components/FileThumbnail';
|
||||||
import { SyncStatusBadge } from '../components/SyncStatusBadge';
|
import { SyncStatusBadge } from '../components/SyncStatusBadge';
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
import { ZoomableImage } from '../components/ZoomableImage';
|
import { ZoomableImage } from '../components/ZoomableImage';
|
||||||
import type { Thumbnail, SyncStatus } from '../types';
|
import type { Variant, SyncStatus } from '../types';
|
||||||
|
|
||||||
const SCREEN_WIDTH = Dimensions.get('window').width;
|
const SCREEN_WIDTH = Dimensions.get('window').width;
|
||||||
|
|
||||||
@@ -44,13 +44,12 @@ type FileDetailRouteProp = RouteProp<RootStackParamList, 'FileDetail'>;
|
|||||||
|
|
||||||
function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; deviceFile?: DeviceFileParam; onSelectImage?: (state: ModalState) => void }) {
|
function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; deviceFile?: DeviceFileParam; onSelectImage?: (state: ModalState) => void }) {
|
||||||
const isDevice = !!deviceFile;
|
const isDevice = !!deviceFile;
|
||||||
const { data: imageData, isLoading: imageLoading } = useFileImage(isDevice ? '' : fileId);
|
|
||||||
const { data: fileData } = useFile(isDevice ? '' : fileId);
|
const { data: fileData } = useFile(isDevice ? '' : fileId);
|
||||||
const downloadFile = useDownloadFile();
|
const downloadFile = useDownloadFile();
|
||||||
const [downloading, setDownloading] = useState(false);
|
const [downloading, setDownloading] = useState(false);
|
||||||
|
|
||||||
const uri = isDevice ? deviceFile.localUri : (imageData?.data?.url);
|
|
||||||
const file = fileData as any;
|
const file = fileData as any;
|
||||||
|
const uri = isDevice ? deviceFile.localUri : file?.url;
|
||||||
|
|
||||||
const localEntry = fileStore.getByBackendId(fileId);
|
const localEntry = fileStore.getByBackendId(fileId);
|
||||||
const syncStatus: SyncStatus = isDevice
|
const syncStatus: SyncStatus = isDevice
|
||||||
@@ -59,9 +58,10 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
|
|||||||
? (localEntry.syncStatus === 'cloud' ? 'cloud' : 'synced')
|
? (localEntry.syncStatus === 'cloud' ? 'cloud' : 'synced')
|
||||||
: (uri ? 'synced' : 'cloud');
|
: (uri ? 'synced' : 'cloud');
|
||||||
|
|
||||||
const fullThumbnails: Thumbnail[] = (file?.data?.thumbnails ?? [])
|
const fullVariants: Variant[] = (file?.variants ?? [])
|
||||||
.filter((t: Thumbnail) => t.resolutionLabel === 'full')
|
.filter((v: Variant) => v.variantType === 'thumbnail_full')
|
||||||
.sort((a: Thumbnail, b: Thumbnail) => a.pageNumber - b.pageNumber);
|
.sort((a: Variant, b: Variant) => a.pageNumber - b.pageNumber);
|
||||||
|
const fullThumbnails = fullVariants;
|
||||||
|
|
||||||
const hasPages = fullThumbnails.length > 0;
|
const hasPages = fullThumbnails.length > 0;
|
||||||
|
|
||||||
@@ -71,14 +71,14 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
|
|||||||
try {
|
try {
|
||||||
await downloadFile.mutateAsync({
|
await downloadFile.mutateAsync({
|
||||||
id: fileId,
|
id: fileId,
|
||||||
backendFileId: fileId,
|
backendResourceId: fileId,
|
||||||
name: file.data?.name ?? fileId,
|
name: file?.name ?? fileId,
|
||||||
mimeType: file.mimeType ?? 'application/octet-stream',
|
mimeType: file?.mimeType ?? 'application/octet-stream',
|
||||||
size: file.data?.size ?? 0,
|
size: file?.size ?? 0,
|
||||||
createdAt: file.createdAt ?? new Date().toISOString(),
|
createdAt: file?.createdAt ?? new Date().toISOString(),
|
||||||
source: 'cloud',
|
source: 'cloud',
|
||||||
syncStatus: 'cloud',
|
syncStatus: 'cloud',
|
||||||
tags: file.data?.tags ?? [],
|
tags: file?.tags ?? [],
|
||||||
isFolder: false,
|
isFolder: false,
|
||||||
});
|
});
|
||||||
} catch {} finally {
|
} catch {} finally {
|
||||||
@@ -120,11 +120,10 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
|
|||||||
) : file ? (
|
) : file ? (
|
||||||
<View style={styles.cloudOnlyContainer}>
|
<View style={styles.cloudOnlyContainer}>
|
||||||
<FileThumbnail
|
<FileThumbnail
|
||||||
thumbnailUrl={file.data?.thumbnailUrl}
|
thumbnailUrl={file?.thumbnailUrl}
|
||||||
mimeType={file.mimeType ?? 'application/pdf'}
|
mimeType={file?.mimeType ?? 'application/pdf'}
|
||||||
fileName={file.name ?? fileId}
|
fileName={file?.name ?? fileId}
|
||||||
size={SCREEN_WIDTH * 0.6}
|
size={SCREEN_WIDTH * 0.6}
|
||||||
isLoading={imageLoading}
|
|
||||||
/>
|
/>
|
||||||
{syncStatus === 'cloud' && (
|
{syncStatus === 'cloud' && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -151,13 +150,13 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
|
|||||||
|
|
||||||
<View style={styles.details}>
|
<View style={styles.details}>
|
||||||
<View style={styles.detailHeader}>
|
<View style={styles.detailHeader}>
|
||||||
<Text style={styles.fileName}>{deviceFile?.name ?? imageData?.data?.name ?? file?.name ?? fileId}</Text>
|
<Text style={styles.fileName}>{deviceFile?.name ?? file?.name ?? fileId}</Text>
|
||||||
<SyncStatusBadge status={syncStatus} size={20} />
|
<SyncStatusBadge status={syncStatus} size={20} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{(deviceFile || imageData?.data?.size != null) && (
|
{(deviceFile || file?.size != null) && (
|
||||||
<Text style={styles.meta}>
|
<Text style={styles.meta}>
|
||||||
Taille : {((deviceFile ? 0 : imageData?.data?.size) ?? 0 / 1024).toFixed(1)} Ko
|
Taille : {((deviceFile ? 0 : file?.size) ?? 0 / 1024).toFixed(1)} Ko
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { RouteProp, useRoute, useNavigation } from '@react-navigation/native';
|
import { RouteProp, useRoute, useNavigation } from '@react-navigation/native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { useFile, useFileImage, useAddTags } from '../hooks/useFiles';
|
import { useFile, useAddTags } from '../hooks/useFiles';
|
||||||
import { usePdfGeneration } from '../hooks/usePdfGeneration';
|
import { usePdfGeneration } from '../hooks/usePdfGeneration';
|
||||||
import { useUpload } from '../hooks/useUpload';
|
import { useUpload } from '../hooks/useUpload';
|
||||||
import { TagChip } from '../components/TagChip';
|
import { TagChip } from '../components/TagChip';
|
||||||
@@ -38,10 +38,9 @@ interface FileEditItemProps {
|
|||||||
|
|
||||||
function FileEditItem({ fileId, selected, onPress }: FileEditItemProps) {
|
function FileEditItem({ fileId, selected, onPress }: FileEditItemProps) {
|
||||||
const { data: fileData, isLoading: fileLoading } = useFile(fileId);
|
const { data: fileData, isLoading: fileLoading } = useFile(fileId);
|
||||||
const { data: imageData, isLoading: imageLoading } = useFileImage(fileId);
|
|
||||||
const file = fileData as any;
|
const file = fileData as any;
|
||||||
const uri = imageData?.data?.url;
|
const uri = file?.url;
|
||||||
const isLoading = fileLoading || imageLoading;
|
const isLoading = fileLoading;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -135,8 +134,8 @@ export function FileEditScreen() {
|
|||||||
const imageUris: { uri: string }[] = [];
|
const imageUris: { uri: string }[] = [];
|
||||||
|
|
||||||
for (const fileId of targetIds) {
|
for (const fileId of targetIds) {
|
||||||
const data = await apiClient.get<{ data: { url: string } }>(`${ENDPOINTS.FILES}/${fileId}`);
|
const data = await apiClient.get<{ url: string }>(`${ENDPOINTS.RESOURCES}/${fileId}`);
|
||||||
const url = data?.data?.url;
|
const url = data?.url;
|
||||||
if (url) {
|
if (url) {
|
||||||
imageUris.push({ uri: url });
|
imageUris.push({ uri: url });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, Alert }
|
|||||||
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
|
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { useDeleteFile, useFileImage, useFiles, useFreeLocalSpace } from '../hooks/useFiles';
|
import { useDeleteFile, useFiles, useFreeLocalSpace } from '../hooks/useFiles';
|
||||||
import { UnifiedFileItem } from '../types';
|
import { UnifiedFileItem } from '../types';
|
||||||
import { isFolder } from '../types';
|
import { isFolder } from '../types';
|
||||||
import { FileThumbnail } from '../components/FileThumbnail';
|
import { FileThumbnail } from '../components/FileThumbnail';
|
||||||
@@ -34,7 +34,6 @@ function FolderGridItem({ file, onPress, onLongPress, selected, onFolderPress }:
|
|||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
onFolderPress?: () => void;
|
onFolderPress?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { data, isLoading } = useFileImage(file.id);
|
|
||||||
const folder = isFolder(file);
|
const folder = isFolder(file);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,12 +45,11 @@ function FolderGridItem({ file, onPress, onLongPress, selected, onFolderPress }:
|
|||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<FileThumbnail
|
<FileThumbnail
|
||||||
uri={data?.data?.url ?? file.localUri}
|
uri={file.url ?? file.localUri}
|
||||||
thumbnailUrl={file.thumbnailUrl}
|
thumbnailUrl={file.thumbnailUrl}
|
||||||
mimeType={file.mimeType}
|
mimeType={file.mimeType}
|
||||||
fileName={file.name}
|
fileName={file.name}
|
||||||
size={ITEM_SIZE}
|
size={ITEM_SIZE}
|
||||||
isLoading={isLoading}
|
|
||||||
syncStatus={file.syncStatus}
|
syncStatus={file.syncStatus}
|
||||||
/>
|
/>
|
||||||
{selected && (
|
{selected && (
|
||||||
@@ -137,9 +135,9 @@ export function FolderScreen() {
|
|||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const f = files.find((fi) => fi.id === id);
|
const f = files.find((fi) => fi.id === id);
|
||||||
if (f?.backendFileId) {
|
if (f?.backendResourceId) {
|
||||||
await apiClient.delete(`${ENDPOINTS.FILES}/${f.backendFileId}`);
|
await apiClient.delete(`${ENDPOINTS.RESOURCES}/${f.backendResourceId}`);
|
||||||
fileStore.deleteByBackendId(f.backendFileId);
|
fileStore.deleteByBackendId(f.backendResourceId);
|
||||||
} else {
|
} else {
|
||||||
fileStore.deleteById(id);
|
fileStore.deleteById(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useNavigation } from '@react-navigation/native';
|
|||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { useDeleteFile, useAddTags, useMoveFiles, useFolders, useFiles, useFreeLocalSpace } from '../hooks/useFiles';
|
import { useDeleteFile, useAddTags, useMoveResources, useFolders, useFiles, useFreeLocalSpace } from '../hooks/useFiles';
|
||||||
import { UnifiedFileItem, isFolder } from '../types';
|
import { UnifiedFileItem, isFolder } from '../types';
|
||||||
import { SearchBar, SearchFilters } from '../components/SearchBar';
|
import { SearchBar, SearchFilters } from '../components/SearchBar';
|
||||||
import { FileThumbnail } from '../components/FileThumbnail';
|
import { FileThumbnail } from '../components/FileThumbnail';
|
||||||
@@ -176,7 +176,7 @@ export function HomeScreen() {
|
|||||||
const [tagModalMode, setTagModalMode] = useState<'tag' | 'folder'>('tag');
|
const [tagModalMode, setTagModalMode] = useState<'tag' | 'folder'>('tag');
|
||||||
const [tagInput, setTagInput] = useState('');
|
const [tagInput, setTagInput] = useState('');
|
||||||
const addTags = useAddTags();
|
const addTags = useAddTags();
|
||||||
const moveFiles = useMoveFiles();
|
const moveFiles = useMoveResources();
|
||||||
const { data: foldersData } = useFolders();
|
const { data: foldersData } = useFolders();
|
||||||
const [moveModalVisible, setMoveModalVisible] = useState(false);
|
const [moveModalVisible, setMoveModalVisible] = useState(false);
|
||||||
const [settingsModalVisible, setSettingsModalVisible] = useState(false);
|
const [settingsModalVisible, setSettingsModalVisible] = useState(false);
|
||||||
@@ -271,9 +271,9 @@ export function HomeScreen() {
|
|||||||
onPress: async () => {
|
onPress: async () => {
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const f = files.find((fi) => fi.id === id);
|
const f = files.find((fi) => fi.id === id);
|
||||||
if (f?.backendFileId) {
|
if (f?.backendResourceId) {
|
||||||
await apiClient.delete(`${ENDPOINTS.FILES}/${f.backendFileId}`);
|
await apiClient.delete(`${ENDPOINTS.RESOURCES}/${f.backendResourceId}`);
|
||||||
fileStore.deleteByBackendId(f.backendFileId);
|
fileStore.deleteByBackendId(f.backendResourceId);
|
||||||
} else {
|
} else {
|
||||||
fileStore.deleteById(id);
|
fileStore.deleteById(id);
|
||||||
}
|
}
|
||||||
@@ -306,9 +306,8 @@ export function HomeScreen() {
|
|||||||
const name = tagInput.trim();
|
const name = tagInput.trim();
|
||||||
if (!name) return;
|
if (!name) return;
|
||||||
const ids = Array.from(selectedIds);
|
const ids = Array.from(selectedIds);
|
||||||
const tagType = tagModalMode === 'folder' ? 'folder' : 'none';
|
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
await addTags.mutateAsync({ fileId: id, tags: [name], tagType });
|
await addTags.mutateAsync({ fileId: id, tags: [name] });
|
||||||
}
|
}
|
||||||
setTagModalVisible(false);
|
setTagModalVisible(false);
|
||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
@@ -316,7 +315,7 @@ export function HomeScreen() {
|
|||||||
|
|
||||||
const handleMove = useCallback(async (folderId: string | null) => {
|
const handleMove = useCallback(async (folderId: string | null) => {
|
||||||
const ids = Array.from(selectedIds);
|
const ids = Array.from(selectedIds);
|
||||||
await moveFiles.mutateAsync({ fileIds: ids, parentFileId: folderId });
|
await moveFiles.mutateAsync({ resourceIds: ids, parentResourceId: folderId });
|
||||||
setMoveModalVisible(false);
|
setMoveModalVisible(false);
|
||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
}, [selectedIds, moveFiles]);
|
}, [selectedIds, moveFiles]);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function SearchScreen() {
|
|||||||
{isLoading && <Text style={styles.loading}>Recherche en cours...</Text>}
|
{isLoading && <Text style={styles.loading}>Recherche en cours...</Text>}
|
||||||
|
|
||||||
<FlatList
|
<FlatList
|
||||||
data={data?.data || []}
|
data={data || []}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
contentContainerStyle={styles.list}
|
contentContainerStyle={styles.list}
|
||||||
|
|||||||
@@ -2,29 +2,26 @@ import React from 'react';
|
|||||||
import { View, Text, Image, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
|
import { View, Text, Image, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
|
||||||
import { FileItem } from '../types';
|
import { FileItem } from '../types';
|
||||||
import { TagChip } from './TagChip';
|
import { TagChip } from './TagChip';
|
||||||
import { useFileImage } from '../hooks/useFileImage';
|
|
||||||
|
|
||||||
interface FileCardProps {
|
interface FileCardProps {
|
||||||
file: FileItem;
|
file: FileItem;
|
||||||
onPress?: (file: FileItem) => void;
|
onPress?: (file: FileItem) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileCard({ file, onPress }: FileCardProps) {
|
function formatSize(bytes: number) {
|
||||||
const { localUri, loading } = useFileImage(file.url, file.name);
|
|
||||||
const formatSize = (bytes: number) => {
|
|
||||||
if (bytes < 1024) return `${bytes} B`;
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export function FileCard({ file, onPress }: FileCardProps) {
|
||||||
|
const imageUri = file.thumbnailUrl || (file.url && file.mimeType?.startsWith('image/') ? file.url : undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity style={styles.container} onPress={() => onPress?.(file)}>
|
<TouchableOpacity style={styles.container} onPress={() => onPress?.(file)}>
|
||||||
{file.url && (
|
{imageUri && (
|
||||||
<View style={styles.imageContainer}>
|
<View style={styles.imageContainer}>
|
||||||
{loading && <ActivityIndicator style={styles.imageLoader} />}
|
<Image source={{ uri: imageUri }} style={styles.image} resizeMode="cover" />
|
||||||
{localUri && (
|
|
||||||
<Image source={{ uri: localUri }} style={styles.image} resizeMode="cover" />
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -54,58 +51,50 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: 16,
|
padding: 12,
|
||||||
marginBottom: 12,
|
marginBottom: 8,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 1 },
|
shadowOffset: { width: 0, height: 1 },
|
||||||
shadowOpacity: 0.1,
|
shadowOpacity: 0.05,
|
||||||
shadowRadius: 2,
|
shadowRadius: 4,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
},
|
},
|
||||||
imageContainer: {
|
imageContainer: {
|
||||||
width: '100%',
|
marginBottom: 8,
|
||||||
height: 200,
|
borderRadius: 4,
|
||||||
borderRadius: 8,
|
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
marginBottom: 12,
|
|
||||||
backgroundColor: '#f0f0f0',
|
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: 120,
|
||||||
},
|
borderRadius: 4,
|
||||||
imageLoader: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: '50%',
|
|
||||||
left: '50%',
|
|
||||||
marginTop: -10,
|
|
||||||
marginLeft: -10,
|
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 8,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
|
color: '#333',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
color: '#666',
|
color: '#999',
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
color: '#444',
|
color: '#666',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
lineHeight: 20,
|
lineHeight: 18,
|
||||||
},
|
},
|
||||||
tags: {
|
tags: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: 6,
|
gap: 4,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,23 +21,30 @@ export function TagChip({ name, onRemove }: TagChipProps) {
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
backgroundColor: '#E3F2FD',
|
backgroundColor: '#E3F2FD',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
flexDirection: 'row',
|
gap: 4,
|
||||||
alignItems: 'center',
|
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#1976D2',
|
color: '#1976D2',
|
||||||
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
removeButton: {
|
removeButton: {
|
||||||
marginLeft: 4,
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: '#BBDEFB',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
removeText: {
|
removeText: {
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
color: '#1976D2',
|
color: '#1976D2',
|
||||||
fontWeight: '600',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+12
-6
@@ -1,16 +1,22 @@
|
|||||||
export const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL || 'http://192.168.1.17:8080/api/v1';
|
export const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL || 'http://192.168.1.17:8080/api/v1';
|
||||||
|
|
||||||
export const ENDPOINTS = {
|
export const ENDPOINTS = {
|
||||||
FILES: '/files',
|
RESOURCES: '/resources',
|
||||||
FILE: '/files',
|
RESOURCE: '/resources',
|
||||||
UPLOAD: '/files/upload',
|
UPLOAD: '/resources/upload',
|
||||||
SEARCH: '/files/search',
|
MOVE: '/resources/move',
|
||||||
MOVE: '/files/move',
|
FOLDERS: '/resources/folders',
|
||||||
FOLDERS: '/files/folders',
|
VARIANT: '/variants',
|
||||||
|
DEDUP_CHECK: '/resources/dedup-check',
|
||||||
OCR_JOBS: '/ocr/jobs',
|
OCR_JOBS: '/ocr/jobs',
|
||||||
HEALTH: '/health',
|
HEALTH: '/health',
|
||||||
AUTH_LOGIN: '/auth/login',
|
AUTH_LOGIN: '/auth/login',
|
||||||
AUTH_REGISTER: '/auth/register',
|
AUTH_REGISTER: '/auth/register',
|
||||||
AUTH_REFRESH: '/auth/refresh',
|
AUTH_REFRESH: '/auth/refresh',
|
||||||
AUTH_LOGOUT: '/auth/logout',
|
AUTH_LOGOUT: '/auth/logout',
|
||||||
|
DEVICES: '/devices',
|
||||||
|
SYNC_PULL: '/sync/pull',
|
||||||
|
SYNC_PUSH: '/sync/push',
|
||||||
|
SHARE: '/resources/:id/share',
|
||||||
|
ACCESS: '/resources/:id/access',
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
import { useDeviceRegistration } from '../hooks/useDeviceRegistration';
|
||||||
|
import { useAuth } from './AuthContext';
|
||||||
|
|
||||||
|
interface DeviceContextType {
|
||||||
|
isRegistered: boolean | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
deviceName: string;
|
||||||
|
register: (name: string) => Promise<{ id: string; device_name: string; role: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeviceContext = createContext<DeviceContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function DeviceProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const registration = useDeviceRegistration();
|
||||||
|
const { isRegistered, isLoading, device, register } = registration;
|
||||||
|
|
||||||
|
const value: DeviceContextType = user
|
||||||
|
? {
|
||||||
|
isRegistered,
|
||||||
|
isLoading,
|
||||||
|
deviceName: device?.name ?? '',
|
||||||
|
register,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
isRegistered: null,
|
||||||
|
isLoading: false,
|
||||||
|
deviceName: '',
|
||||||
|
register: async () => { throw new Error('Not authenticated'); },
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DeviceContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</DeviceContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDevice() {
|
||||||
|
const context = useContext(DeviceContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useDevice must be used within a DeviceProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -77,7 +77,6 @@ export function useAutoSync() {
|
|||||||
|
|
||||||
if (globalMode === 'auto') {
|
if (globalMode === 'auto') {
|
||||||
// Mode auto global : tous les fichiers locaux sans backendId
|
// Mode auto global : tous les fichiers locaux sans backendId
|
||||||
// (pas de filtre par dossier)
|
|
||||||
} else {
|
} else {
|
||||||
// Mode manuel : uniquement les fichiers des dossiers en mode auto
|
// Mode manuel : uniquement les fichiers des dossiers en mode auto
|
||||||
const allFolders = safDirectory.getAll();
|
const allFolders = safDirectory.getAll();
|
||||||
@@ -85,7 +84,7 @@ export function useAutoSync() {
|
|||||||
allFolders.filter((f) => f.syncMode === 'auto').map((f) => f.id)
|
allFolders.filter((f) => f.syncMode === 'auto').map((f) => f.id)
|
||||||
);
|
);
|
||||||
pendingFiles = pendingFiles.filter(
|
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 {
|
} finally {
|
||||||
setIsSyncing(false);
|
setIsSyncing(false);
|
||||||
isRunning.current = 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;
|
if (!record) return null;
|
||||||
return {
|
return {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
backendFileId: record.backendId ?? undefined,
|
backendResourceId: record.backendId ?? undefined,
|
||||||
name: record.name,
|
name: record.name,
|
||||||
mimeType: record.mimeType,
|
mimeType: record.mimeType,
|
||||||
size: record.size,
|
size: record.size,
|
||||||
@@ -20,7 +20,8 @@ function recordToUnifiedItem(record: ReturnType<typeof fileStore.getById>): Unif
|
|||||||
ocrText: record.ocrText ?? undefined,
|
ocrText: record.ocrText ?? undefined,
|
||||||
tags: record.tags ?? [],
|
tags: record.tags ?? [],
|
||||||
isFolder: record.isFolder === 1,
|
isFolder: record.isFolder === 1,
|
||||||
parentFileId: record.parentFileId ?? undefined,
|
parentResourceId: record.parentResourceId ?? undefined,
|
||||||
|
ownerId: record.ownerId ?? undefined,
|
||||||
thumbnailUrl: record.thumbnailUrl ?? undefined,
|
thumbnailUrl: record.thumbnailUrl ?? undefined,
|
||||||
isDeviceFile: record.source === 'local' && !record.backendId,
|
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) {
|
export function useFiles(parentId?: string | null, page: number = 1, limit: number = 50) {
|
||||||
const queryKey = parentId
|
const queryKey = parentId
|
||||||
? ['files', parentId]
|
? ['resources', parentId]
|
||||||
: ['files', 'root', page, limit];
|
: ['resources', 'root', page, limit];
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey,
|
queryKey,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
const backendRes = await apiClient.get<{ data: FileItem[] }>(
|
const backendRes = await apiClient.get<{ data: FileItem[] }>(
|
||||||
`/files/folders/${parentId}/files?thumbnail=thumbnail`,
|
`${ENDPOINTS.RESOURCES}/folders/${parentId}/resources?thumbnail=thumbnail_small`,
|
||||||
);
|
);
|
||||||
fileStore.mergeFromBackend(
|
fileStore.mergeFromBackend(
|
||||||
backendRes.data.map((f) => ({
|
backendRes.data.map((f) => ({
|
||||||
@@ -59,7 +60,8 @@ export function useFiles(parentId?: string | null, page: number = 1, limit: numb
|
|||||||
ocrText: f.ocrText,
|
ocrText: f.ocrText,
|
||||||
tags: f.tags,
|
tags: f.tags,
|
||||||
isFolder: f.isFolder,
|
isFolder: f.isFolder,
|
||||||
parentFileId: f.parentFileId,
|
parentResourceId: f.parentResourceId,
|
||||||
|
ownerId: f.ownerId,
|
||||||
thumbnailUrl: f.thumbnailUrl,
|
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>>(
|
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(
|
fileStore.mergeFromBackend(
|
||||||
backendRes.data.map((f) => ({
|
backendRes.data.map((f) => ({
|
||||||
@@ -85,7 +87,8 @@ export function useFiles(parentId?: string | null, page: number = 1, limit: numb
|
|||||||
ocrText: f.ocrText,
|
ocrText: f.ocrText,
|
||||||
tags: f.tags,
|
tags: f.tags,
|
||||||
isFolder: f.isFolder,
|
isFolder: f.isFolder,
|
||||||
parentFileId: f.parentFileId,
|
parentResourceId: f.parentResourceId,
|
||||||
|
ownerId: f.ownerId,
|
||||||
thumbnailUrl: f.thumbnailUrl,
|
thumbnailUrl: f.thumbnailUrl,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
@@ -110,34 +113,23 @@ export function useFiles(parentId?: string | null, page: number = 1, limit: numb
|
|||||||
|
|
||||||
export function useFile(id: string) {
|
export function useFile(id: string) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['files', id],
|
queryKey: ['resources', id],
|
||||||
queryFn: () => apiClient.get<FileItem>(`${ENDPOINTS.FILES}/${id}?thumbnail=thumbnail`),
|
queryFn: () => apiClient.get<FileItem>(`${ENDPOINTS.RESOURCES}/${id}?thumbnail=thumbnail_small`),
|
||||||
enabled: !!id,
|
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() {
|
export function useDeleteFile() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (id: string) => {
|
mutationFn: async (id: string) => {
|
||||||
const result = await apiClient.delete(`${ENDPOINTS.FILES}/${id}`);
|
const result = await apiClient.delete(`${ENDPOINTS.RESOURCES}/${id}`);
|
||||||
fileStore.deleteByBackendId(id);
|
fileStore.deleteByBackendId(id);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -146,22 +138,22 @@ export function useAddTags() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ fileId, tags, tagType }: { fileId: string; tags: string[]; tagType?: string }) =>
|
mutationFn: ({ fileId, tags }: { fileId: string; tags: string[] }) =>
|
||||||
apiClient.post(`${ENDPOINTS.FILES}/${fileId}/tags`, { tags, tag_type: tagType }),
|
apiClient.post(`${ENDPOINTS.RESOURCES}/${fileId}/tags`, { tags }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMoveFiles() {
|
export function useMoveResources() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ fileIds, parentFileId }: { fileIds: string[]; parentFileId: string | null }) =>
|
mutationFn: ({ resourceIds, parentResourceId }: { resourceIds: string[]; parentResourceId: string | null }) =>
|
||||||
apiClient.post(ENDPOINTS.MOVE, { file_ids: fileIds, parent_file_id: parentFileId }),
|
apiClient.post(ENDPOINTS.MOVE, { resource_ids: resourceIds, parent_resource_id: parentResourceId }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -182,7 +174,8 @@ export function useFolders() {
|
|||||||
ocrText: f.ocrText,
|
ocrText: f.ocrText,
|
||||||
tags: f.tags,
|
tags: f.tags,
|
||||||
isFolder: f.isFolder,
|
isFolder: f.isFolder,
|
||||||
parentFileId: f.parentFileId,
|
parentResourceId: f.parentResourceId,
|
||||||
|
ownerId: f.ownerId,
|
||||||
thumbnailUrl: f.thumbnailUrl,
|
thumbnailUrl: f.thumbnailUrl,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
@@ -209,10 +202,9 @@ export function useDownloadFile() {
|
|||||||
return file.localUri ?? '';
|
return file.localUri ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await apiClient.get<{ data: { url: string; name: string } }>(
|
const res = await apiClient.get<{ url: string }>(
|
||||||
`/files/${file.backendFileId}`,
|
`${ENDPOINTS.RESOURCES}/${file.backendResourceId ?? file.id}`,
|
||||||
);
|
);
|
||||||
const downloadUrl = res.data.url;
|
|
||||||
|
|
||||||
const { downloadAsync, documentDirectory, makeDirectoryAsync } = await import('expo-file-system/legacy');
|
const { downloadAsync, documentDirectory, makeDirectoryAsync } = await import('expo-file-system/legacy');
|
||||||
const DOWNLOAD_DIR = `${documentDirectory}synced-files/`;
|
const DOWNLOAD_DIR = `${documentDirectory}synced-files/`;
|
||||||
@@ -227,28 +219,29 @@ export function useDownloadFile() {
|
|||||||
const ext = dot >= 0 ? file.name.slice(dot) : '';
|
const ext = dot >= 0 ? file.name.slice(dot) : '';
|
||||||
const fileUri = `${DOWNLOAD_DIR}${cacheKey}${ext}`;
|
const fileUri = `${DOWNLOAD_DIR}${cacheKey}${ext}`;
|
||||||
|
|
||||||
const result = await downloadAsync(downloadUrl, fileUri);
|
const result = await downloadAsync(res.url, fileUri);
|
||||||
|
|
||||||
fileStore.upsert({
|
fileStore.upsert({
|
||||||
id: file.backendFileId ?? file.id,
|
id: file.backendResourceId ?? file.id,
|
||||||
backendId: file.backendFileId ?? file.id,
|
backendId: file.backendResourceId ?? file.id,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
mimeType: file.mimeType,
|
mimeType: file.mimeType,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
source: 'synced',
|
source: 'synced',
|
||||||
localUri: result.uri,
|
localUri: result.uri,
|
||||||
syncStatus: 'synced',
|
syncStatus: 'synced',
|
||||||
parentFileId: file.parentFileId ?? null,
|
parentResourceId: file.parentResourceId ?? null,
|
||||||
isFolder: 0,
|
isFolder: 0,
|
||||||
ocrText: file.ocrText ?? null,
|
ocrText: file.ocrText ?? null,
|
||||||
thumbnailUrl: file.thumbnailUrl ?? null,
|
thumbnailUrl: file.thumbnailUrl ?? null,
|
||||||
|
ownerId: file.ownerId ?? null,
|
||||||
createdAt: file.createdAt,
|
createdAt: file.createdAt,
|
||||||
updatedAt: file.updatedAt ?? file.createdAt,
|
updatedAt: file.updatedAt ?? file.createdAt,
|
||||||
lastSyncedAt: new Date().toISOString(),
|
lastSyncedAt: new Date().toISOString(),
|
||||||
tags: file.tags,
|
tags: file.tags,
|
||||||
});
|
});
|
||||||
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||||
return result.uri;
|
return result.uri;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -274,7 +267,7 @@ export function useFreeLocalSpace() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function useLocalFiles() {
|
|||||||
for (const entry of registryEntries) {
|
for (const entry of registryEntries) {
|
||||||
merged.set(entry.id, {
|
merged.set(entry.id, {
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
backendFileId: entry.backendId ?? undefined,
|
backendResourceId: entry.backendId ?? undefined,
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
mimeType: entry.mimeType,
|
mimeType: entry.mimeType,
|
||||||
size: entry.size,
|
size: entry.size,
|
||||||
@@ -42,7 +42,7 @@ export function useLocalFiles() {
|
|||||||
localUri: entry.localUri ?? undefined,
|
localUri: entry.localUri ?? undefined,
|
||||||
tags: entry.tags ?? [],
|
tags: entry.tags ?? [],
|
||||||
isFolder: entry.isFolder === 1,
|
isFolder: entry.isFolder === 1,
|
||||||
parentFileId: entry.parentFileId ?? undefined,
|
parentResourceId: entry.parentResourceId ?? undefined,
|
||||||
isDeviceFile: entry.source === 'local' && !entry.backendId,
|
isDeviceFile: entry.source === 'local' && !entry.backendId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ export function useLocalFiles() {
|
|||||||
tags: [],
|
tags: [],
|
||||||
isFolder: false,
|
isFolder: false,
|
||||||
isDeviceFile: true,
|
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 { downloadAsync, documentDirectory, makeDirectoryAsync } from 'expo-file-system/legacy';
|
||||||
import { fileStore } from '../services/fileStore';
|
import { fileStore } from '../services/fileStore';
|
||||||
import { apiClient } from '../api/client';
|
import { apiClient } from '../api/client';
|
||||||
|
import { ENDPOINTS } from '../constants/api';
|
||||||
import { setIsSyncing } from './useSyncQueue';
|
import { setIsSyncing } from './useSyncQueue';
|
||||||
|
|
||||||
const SYNC_DIR = `${documentDirectory}synced-files/`;
|
const SYNC_DIR = `${documentDirectory}synced-files/`;
|
||||||
@@ -25,9 +26,11 @@ export function usePullSync() {
|
|||||||
size: number;
|
size: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
url?: 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 registry = fileStore.getAllSynced();
|
||||||
const existingBackendIds = new Set(
|
const existingBackendIds = new Set(
|
||||||
registry.filter((e) => e.backendId).map((e) => e.backendId)
|
registry.filter((e) => e.backendId).map((e) => e.backendId)
|
||||||
@@ -35,35 +38,36 @@ export function usePullSync() {
|
|||||||
|
|
||||||
let pulled = 0;
|
let pulled = 0;
|
||||||
|
|
||||||
for (const bf of backendFiles) {
|
for (const br of backendResources) {
|
||||||
if (existingBackendIds.has(bf.id)) continue;
|
if (existingBackendIds.has(br.id)) continue;
|
||||||
if (bf.size === 0) continue;
|
if (br.size === 0) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const detail = await apiClient.get<{ data: { url: string } }>(`/files/${bf.id}`);
|
const detail = await apiClient.get<{ url: string }>(`${ENDPOINTS.RESOURCES}/${br.id}`);
|
||||||
const downloadUrl = detail.data.url;
|
const downloadUrl = detail.url;
|
||||||
|
|
||||||
await makeDirectoryAsync(SYNC_DIR, { intermediates: true });
|
await makeDirectoryAsync(SYNC_DIR, { intermediates: true });
|
||||||
const safeName = bf.name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
const safeName = br.name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||||
const fileUri = `${SYNC_DIR}${bf.id}_${safeName}`;
|
const fileUri = `${SYNC_DIR}${br.id}_${safeName}`;
|
||||||
|
|
||||||
const result = await downloadAsync(downloadUrl, fileUri);
|
const result = await downloadAsync(downloadUrl, fileUri);
|
||||||
|
|
||||||
fileStore.upsert({
|
fileStore.upsert({
|
||||||
id: bf.id,
|
id: br.id,
|
||||||
backendId: bf.id,
|
backendId: br.id,
|
||||||
name: bf.name,
|
name: br.name,
|
||||||
mimeType: bf.mimeType,
|
mimeType: br.mimeType,
|
||||||
size: bf.size,
|
size: br.size,
|
||||||
source: 'synced',
|
source: 'synced',
|
||||||
localUri: result.uri,
|
localUri: result.uri,
|
||||||
syncStatus: 'synced',
|
syncStatus: 'synced',
|
||||||
parentFileId: null,
|
parentResourceId: null,
|
||||||
isFolder: 0,
|
isFolder: 0,
|
||||||
ocrText: null,
|
ocrText: null,
|
||||||
thumbnailUrl: null,
|
thumbnailUrl: br.thumbnailUrl ?? null,
|
||||||
createdAt: bf.createdAt,
|
ownerId: br.ownerId ?? null,
|
||||||
updatedAt: bf.createdAt,
|
createdAt: br.createdAt,
|
||||||
|
updatedAt: br.createdAt,
|
||||||
lastSyncedAt: new Date().toISOString(),
|
lastSyncedAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
pulled++;
|
pulled++;
|
||||||
@@ -73,7 +77,7 @@ export function usePullSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pulled > 0) {
|
if (pulled > 0) {
|
||||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
return { pulled };
|
return { pulled };
|
||||||
|
|||||||
+41
-13
@@ -1,15 +1,43 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useMemo } from 'react';
|
||||||
import { apiClient } from '../api/client';
|
import { fileStore } from '../services/fileStore';
|
||||||
import { ENDPOINTS } from '../constants/api';
|
import { UnifiedFileItem } from '../types';
|
||||||
import { FileItem, PaginatedResponse } from '../types';
|
|
||||||
|
|
||||||
export function useSearch(query: string, page: number = 1, limit: number = 20) {
|
function recordToUnifiedItem(record: ReturnType<typeof fileStore.getById>): UnifiedFileItem | null {
|
||||||
return useQuery({
|
if (!record) return null;
|
||||||
queryKey: ['search', query, page, limit],
|
return {
|
||||||
queryFn: () =>
|
id: record.id,
|
||||||
apiClient.get<PaginatedResponse<FileItem>>(
|
backendResourceId: record.backendId ?? undefined,
|
||||||
`${ENDPOINTS.FILES}/search?q=${encodeURIComponent(query)}&page=${page}&limit=${limit}`
|
name: record.name,
|
||||||
),
|
mimeType: record.mimeType,
|
||||||
enabled: query.length > 0,
|
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++;
|
count++;
|
||||||
} else {
|
} else {
|
||||||
// mode manuel : uniquement les fichiers des dossiers en mode auto
|
// mode manuel : uniquement les fichiers des dossiers en mode auto
|
||||||
if (!entry.parentFileId) continue;
|
if (!entry.parentResourceId) continue;
|
||||||
const folder = safDirectory.getAll().find((f) => f.id === entry.parentFileId);
|
const folder = safDirectory.getAll().find((f) => f.id === entry.parentResourceId);
|
||||||
if (folder && folder.syncMode === 'auto') {
|
if (folder && folder.syncMode === 'auto') {
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export function useUpload() {
|
|||||||
return { uploaded, errors };
|
return { uploaded, errors };
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
queryClient.invalidateQueries({ queryKey: ['resources'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { eq, like, or, and, desc, asc, sql, isNull } from 'drizzle-orm';
|
|||||||
import { files, fileTags, deletedFiles } from './schema';
|
import { files, fileTags, deletedFiles } from './schema';
|
||||||
import type { Tag } from '../../types';
|
import type { Tag } from '../../types';
|
||||||
|
|
||||||
const DB_NAME = 'vaultdrop.db';
|
const DB_NAME = 'vaultdrop-v3.db';
|
||||||
|
const SCHEMA_VERSION_KEY = 'schema_version';
|
||||||
|
const SCHEMA_VERSION = 3;
|
||||||
|
|
||||||
let _db: ReturnType<typeof drizzle> | null = null;
|
let _db: ReturnType<typeof drizzle> | null = null;
|
||||||
let _sqliteDb: SQLite.SQLiteDatabase | null = null;
|
let _sqliteDb: SQLite.SQLiteDatabase | null = null;
|
||||||
@@ -14,9 +16,41 @@ export function initDB() {
|
|||||||
_sqliteDb = SQLite.openDatabaseSync(DB_NAME);
|
_sqliteDb = SQLite.openDatabaseSync(DB_NAME);
|
||||||
_sqliteDb.execSync('PRAGMA journal_mode = WAL;');
|
_sqliteDb.execSync('PRAGMA journal_mode = WAL;');
|
||||||
_sqliteDb.execSync('PRAGMA foreign_keys = ON;');
|
_sqliteDb.execSync('PRAGMA foreign_keys = ON;');
|
||||||
_db = drizzle(_sqliteDb);
|
|
||||||
|
|
||||||
_sqliteDb.execSync(`
|
const existingVersion = _sqliteDb.getFirstSync<{ version: number }>(
|
||||||
|
`SELECT name as version FROM sqlite_master WHERE type='table' AND name='schema_version'`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingVersion) {
|
||||||
|
_sqliteDb.execSync(`CREATE TABLE schema_version (version INTEGER PRIMARY KEY);`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionRow = _sqliteDb.getFirstSync<{ version: number }>(
|
||||||
|
`SELECT version FROM schema_version ORDER BY version DESC LIMIT 1`
|
||||||
|
);
|
||||||
|
const currentVersion = versionRow?.version ?? 0;
|
||||||
|
|
||||||
|
if (currentVersion < SCHEMA_VERSION) {
|
||||||
|
dropAllTables(_sqliteDb);
|
||||||
|
createSchema(_sqliteDb);
|
||||||
|
_sqliteDb.execSync(`INSERT INTO schema_version (version) VALUES (${SCHEMA_VERSION});`);
|
||||||
|
}
|
||||||
|
|
||||||
|
_db = drizzle(_sqliteDb);
|
||||||
|
return _db;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dropAllTables(db: SQLite.SQLiteDatabase) {
|
||||||
|
db.execSync(`DROP TABLE IF EXISTS file_tags;`);
|
||||||
|
db.execSync(`DROP TABLE IF EXISTS files;`);
|
||||||
|
db.execSync(`DROP TABLE IF EXISTS deleted_files;`);
|
||||||
|
db.execSync(`DROP TABLE IF EXISTS device_info;`);
|
||||||
|
db.execSync(`DROP TABLE IF EXISTS resources_fts;`);
|
||||||
|
db.execSync(`DROP TABLE IF EXISTS schema_version;`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSchema(db: SQLite.SQLiteDatabase) {
|
||||||
|
db.execSync(`
|
||||||
CREATE TABLE IF NOT EXISTS files (
|
CREATE TABLE IF NOT EXISTS files (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
backend_id TEXT,
|
backend_id TEXT,
|
||||||
@@ -26,33 +60,75 @@ export function initDB() {
|
|||||||
source TEXT NOT NULL DEFAULT 'cloud',
|
source TEXT NOT NULL DEFAULT 'cloud',
|
||||||
local_uri TEXT,
|
local_uri TEXT,
|
||||||
sync_status TEXT NOT NULL DEFAULT 'cloud',
|
sync_status TEXT NOT NULL DEFAULT 'cloud',
|
||||||
parent_file_id TEXT,
|
parent_resource_id TEXT,
|
||||||
is_folder INTEGER NOT NULL DEFAULT 0,
|
is_folder INTEGER NOT NULL DEFAULT 0,
|
||||||
ocr_text TEXT,
|
ocr_text TEXT,
|
||||||
thumbnail_url TEXT,
|
thumbnail_url TEXT,
|
||||||
|
owner_id TEXT,
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
updated_at TEXT NOT NULL,
|
updated_at TEXT NOT NULL,
|
||||||
last_synced_at TEXT
|
last_synced_at TEXT
|
||||||
);
|
);
|
||||||
|
`);
|
||||||
|
db.execSync(`CREATE INDEX IF NOT EXISTS idx_files_backend_id ON files(backend_id);`);
|
||||||
|
db.execSync(`CREATE INDEX IF NOT EXISTS idx_files_parent_resource_id ON files(parent_resource_id);`);
|
||||||
|
db.execSync(`CREATE INDEX IF NOT EXISTS idx_files_source ON files(source);`);
|
||||||
|
db.execSync(`CREATE INDEX IF NOT EXISTS idx_files_is_folder ON files(is_folder);`);
|
||||||
|
db.execSync(`CREATE INDEX IF NOT EXISTS idx_files_sync_status ON files(sync_status);`);
|
||||||
|
db.execSync(`CREATE INDEX IF NOT EXISTS idx_files_owner_id ON files(owner_id);`);
|
||||||
|
db.execSync(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY);`);
|
||||||
|
|
||||||
|
db.execSync(`
|
||||||
CREATE TABLE IF NOT EXISTS file_tags (
|
CREATE TABLE IF NOT EXISTS file_tags (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
||||||
tag_name TEXT NOT NULL,
|
tag_name TEXT NOT NULL
|
||||||
tag_type TEXT NOT NULL DEFAULT 'none'
|
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_files_backend_id ON files(backend_id);
|
`);
|
||||||
CREATE INDEX IF NOT EXISTS idx_files_parent_id ON files(parent_file_id);
|
db.execSync(`CREATE INDEX IF NOT EXISTS idx_file_tags_file_id ON file_tags(file_id);`);
|
||||||
CREATE INDEX IF NOT EXISTS idx_files_source ON files(source);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_files_is_folder ON files(is_folder);
|
db.execSync(`
|
||||||
CREATE INDEX IF NOT EXISTS idx_files_sync_status ON files(sync_status);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_file_tags_file_id ON file_tags(file_id);
|
|
||||||
CREATE TABLE IF NOT EXISTS deleted_files (
|
CREATE TABLE IF NOT EXISTS deleted_files (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
deleted_at TEXT NOT NULL
|
deleted_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
return _db;
|
db.execSync(`
|
||||||
|
CREATE TABLE IF NOT EXISTS device_info (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
server_id TEXT,
|
||||||
|
device_name TEXT NOT NULL DEFAULT '',
|
||||||
|
platform TEXT NOT NULL DEFAULT '',
|
||||||
|
registered_at TEXT
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
db.execSync(`
|
||||||
|
CREATE VIRTUAL TABLE IF NOT EXISTS resources_fts USING fts5(
|
||||||
|
name,
|
||||||
|
ocr_text,
|
||||||
|
content='files',
|
||||||
|
content_rowid='rowid'
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
db.execSync(`
|
||||||
|
CREATE TRIGGER IF NOT EXISTS resources_fts_insert AFTER INSERT ON files BEGIN
|
||||||
|
INSERT INTO resources_fts(rowid, name, ocr_text) VALUES (new.rowid, new.name, new.ocr_text);
|
||||||
|
END;
|
||||||
|
`);
|
||||||
|
db.execSync(`
|
||||||
|
CREATE TRIGGER IF NOT EXISTS resources_fts_delete AFTER DELETE ON files BEGIN
|
||||||
|
INSERT INTO resources_fts(resources_fts, rowid, name, ocr_text) VALUES('delete', old.rowid, old.name, old.ocr_text);
|
||||||
|
END;
|
||||||
|
`);
|
||||||
|
db.execSync(`
|
||||||
|
CREATE TRIGGER IF NOT EXISTS resources_fts_update AFTER UPDATE ON files BEGIN
|
||||||
|
INSERT INTO resources_fts(resources_fts, rowid, name, ocr_text) VALUES('delete', old.rowid, old.name, old.ocr_text);
|
||||||
|
INSERT INTO resources_fts(rowid, name, ocr_text) VALUES (new.rowid, new.name, new.ocr_text);
|
||||||
|
END;
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDb() {
|
function getDb() {
|
||||||
@@ -69,18 +145,35 @@ export type FileRecord = {
|
|||||||
source: string;
|
source: string;
|
||||||
localUri: string | null;
|
localUri: string | null;
|
||||||
syncStatus: string;
|
syncStatus: string;
|
||||||
parentFileId: string | null;
|
parentResourceId: string | null;
|
||||||
isFolder: number;
|
isFolder: number;
|
||||||
ocrText: string | null;
|
ocrText: string | null;
|
||||||
thumbnailUrl: string | null;
|
thumbnailUrl: string | null;
|
||||||
|
ownerId: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
lastSyncedAt: string | null;
|
lastSyncedAt: string | null;
|
||||||
tags?: Tag[];
|
tags?: Tag[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type FileRow = typeof files.$inferSelect;
|
type FileRow = {
|
||||||
type TagRow = typeof fileTags.$inferSelect;
|
id: string;
|
||||||
|
backendId: string | null;
|
||||||
|
name: string;
|
||||||
|
mimeType: string;
|
||||||
|
size: number;
|
||||||
|
source: string;
|
||||||
|
localUri: string | null;
|
||||||
|
syncStatus: string;
|
||||||
|
parentResourceId: string | null;
|
||||||
|
isFolder: number;
|
||||||
|
ocrText: string | null;
|
||||||
|
thumbnailUrl: string | null;
|
||||||
|
ownerId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
lastSyncedAt: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
function rowToRecord(row: FileRow, tags?: Tag[]): FileRecord {
|
function rowToRecord(row: FileRow, tags?: Tag[]): FileRecord {
|
||||||
return {
|
return {
|
||||||
@@ -92,10 +185,11 @@ function rowToRecord(row: FileRow, tags?: Tag[]): FileRecord {
|
|||||||
source: row.source,
|
source: row.source,
|
||||||
localUri: row.localUri,
|
localUri: row.localUri,
|
||||||
syncStatus: row.syncStatus,
|
syncStatus: row.syncStatus,
|
||||||
parentFileId: row.parentFileId,
|
parentResourceId: row.parentResourceId,
|
||||||
isFolder: row.isFolder,
|
isFolder: row.isFolder,
|
||||||
ocrText: row.ocrText,
|
ocrText: row.ocrText,
|
||||||
thumbnailUrl: row.thumbnailUrl,
|
thumbnailUrl: row.thumbnailUrl,
|
||||||
|
ownerId: row.ownerId,
|
||||||
createdAt: row.createdAt,
|
createdAt: row.createdAt,
|
||||||
updatedAt: row.updatedAt,
|
updatedAt: row.updatedAt,
|
||||||
lastSyncedAt: row.lastSyncedAt,
|
lastSyncedAt: row.lastSyncedAt,
|
||||||
@@ -106,7 +200,7 @@ function rowToRecord(row: FileRow, tags?: Tag[]): FileRecord {
|
|||||||
function getTagsForFile(fileId: string): Tag[] {
|
function getTagsForFile(fileId: string): Tag[] {
|
||||||
const d = getDb();
|
const d = getDb();
|
||||||
const rows = d.select().from(fileTags).where(eq(fileTags.fileId, fileId)).all();
|
const rows = d.select().from(fileTags).where(eq(fileTags.fileId, fileId)).all();
|
||||||
return rows.map((r) => ({ id: r.tagName, tag_name: r.tagName, tag_type: r.tagType }));
|
return rows.map((r) => ({ id: r.tagName, tag_name: r.tagName }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTagsForFile(fileId: string, tags: Tag[]) {
|
function setTagsForFile(fileId: string, tags: Tag[]) {
|
||||||
@@ -118,7 +212,6 @@ function setTagsForFile(fileId: string, tags: Tag[]) {
|
|||||||
id: `${fileId}_${t.id || t.tag_name}`,
|
id: `${fileId}_${t.id || t.tag_name}`,
|
||||||
fileId,
|
fileId,
|
||||||
tagName: t.tag_name,
|
tagName: t.tag_name,
|
||||||
tagType: t.tag_type,
|
|
||||||
})),
|
})),
|
||||||
).run();
|
).run();
|
||||||
}
|
}
|
||||||
@@ -134,10 +227,11 @@ function upsertRow(file: FileRecord) {
|
|||||||
source: file.source,
|
source: file.source,
|
||||||
localUri: file.localUri,
|
localUri: file.localUri,
|
||||||
syncStatus: file.syncStatus,
|
syncStatus: file.syncStatus,
|
||||||
parentFileId: file.parentFileId,
|
parentResourceId: file.parentResourceId,
|
||||||
isFolder: file.isFolder,
|
isFolder: file.isFolder,
|
||||||
ocrText: file.ocrText,
|
ocrText: file.ocrText,
|
||||||
thumbnailUrl: file.thumbnailUrl,
|
thumbnailUrl: file.thumbnailUrl,
|
||||||
|
ownerId: file.ownerId,
|
||||||
createdAt: file.createdAt,
|
createdAt: file.createdAt,
|
||||||
updatedAt: file.updatedAt,
|
updatedAt: file.updatedAt,
|
||||||
lastSyncedAt: file.lastSyncedAt,
|
lastSyncedAt: file.lastSyncedAt,
|
||||||
@@ -151,10 +245,11 @@ function upsertRow(file: FileRecord) {
|
|||||||
source: file.source,
|
source: file.source,
|
||||||
localUri: file.localUri,
|
localUri: file.localUri,
|
||||||
syncStatus: file.syncStatus,
|
syncStatus: file.syncStatus,
|
||||||
parentFileId: file.parentFileId,
|
parentResourceId: file.parentResourceId,
|
||||||
isFolder: file.isFolder,
|
isFolder: file.isFolder,
|
||||||
ocrText: file.ocrText,
|
ocrText: file.ocrText,
|
||||||
thumbnailUrl: file.thumbnailUrl,
|
thumbnailUrl: file.thumbnailUrl,
|
||||||
|
ownerId: file.ownerId,
|
||||||
updatedAt: file.updatedAt,
|
updatedAt: file.updatedAt,
|
||||||
lastSyncedAt: file.lastSyncedAt,
|
lastSyncedAt: file.lastSyncedAt,
|
||||||
},
|
},
|
||||||
@@ -179,14 +274,14 @@ export const fileStore = {
|
|||||||
|
|
||||||
getById(id: string): FileRecord | null {
|
getById(id: string): FileRecord | null {
|
||||||
const d = getDb();
|
const d = getDb();
|
||||||
const row = d.select().from(files).where(eq(files.id, id)).get();
|
const row = d.select().from(files).where(eq(files.id, id)).get() as FileRow | undefined;
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
return rowToRecord(row, getTagsForFile(id));
|
return rowToRecord(row, getTagsForFile(id));
|
||||||
},
|
},
|
||||||
|
|
||||||
getByBackendId(backendId: string): FileRecord | null {
|
getByBackendId(backendId: string): FileRecord | null {
|
||||||
const d = getDb();
|
const d = getDb();
|
||||||
const row = d.select().from(files).where(eq(files.backendId, backendId)).get();
|
const row = d.select().from(files).where(eq(files.backendId, backendId)).get() as FileRow | undefined;
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
return rowToRecord(row, getTagsForFile(row.id));
|
return rowToRecord(row, getTagsForFile(row.id));
|
||||||
},
|
},
|
||||||
@@ -194,18 +289,18 @@ export const fileStore = {
|
|||||||
getRootFolders(): FileRecord[] {
|
getRootFolders(): FileRecord[] {
|
||||||
const d = getDb();
|
const d = getDb();
|
||||||
const rows = d.select().from(files)
|
const rows = d.select().from(files)
|
||||||
.where(and(eq(files.isFolder, 1), isNull(files.parentFileId)))
|
.where(and(eq(files.isFolder, 1), isNull(files.parentResourceId)))
|
||||||
.orderBy(asc(files.name))
|
.orderBy(asc(files.name))
|
||||||
.all();
|
.all() as FileRow[];
|
||||||
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
||||||
},
|
},
|
||||||
|
|
||||||
getChildrenByParent(parentId: string): FileRecord[] {
|
getChildrenByParent(parentId: string): FileRecord[] {
|
||||||
const d = getDb();
|
const d = getDb();
|
||||||
const rows = d.select().from(files)
|
const rows = d.select().from(files)
|
||||||
.where(eq(files.parentFileId, parentId))
|
.where(eq(files.parentResourceId, parentId))
|
||||||
.orderBy(desc(files.isFolder), desc(files.createdAt))
|
.orderBy(desc(files.isFolder), desc(files.createdAt))
|
||||||
.all();
|
.all() as FileRow[];
|
||||||
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -215,16 +310,16 @@ export const fileStore = {
|
|||||||
|
|
||||||
const countRow = d.select({ count: sql<number>`count(*)` })
|
const countRow = d.select({ count: sql<number>`count(*)` })
|
||||||
.from(files)
|
.from(files)
|
||||||
.where(and(isNull(files.parentFileId), eq(files.isFolder, 0)))
|
.where(and(isNull(files.parentResourceId), eq(files.isFolder, 0)))
|
||||||
.get();
|
.get();
|
||||||
const total = countRow?.count ?? 0;
|
const total = countRow?.count ?? 0;
|
||||||
|
|
||||||
const rows = d.select().from(files)
|
const rows = d.select().from(files)
|
||||||
.where(and(isNull(files.parentFileId), eq(files.isFolder, 0)))
|
.where(and(isNull(files.parentResourceId), eq(files.isFolder, 0)))
|
||||||
.orderBy(desc(files.createdAt))
|
.orderBy(desc(files.createdAt))
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.all();
|
.all() as FileRow[];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
files: rows.map((r) => rowToRecord(r, getTagsForFile(r.id))),
|
files: rows.map((r) => rowToRecord(r, getTagsForFile(r.id))),
|
||||||
@@ -237,7 +332,29 @@ export const fileStore = {
|
|||||||
const rows = d.select().from(files)
|
const rows = d.select().from(files)
|
||||||
.where(eq(files.isFolder, 1))
|
.where(eq(files.isFolder, 1))
|
||||||
.orderBy(asc(files.name))
|
.orderBy(asc(files.name))
|
||||||
.all();
|
.all() as FileRow[];
|
||||||
|
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
||||||
|
},
|
||||||
|
|
||||||
|
searchFts(query: string): FileRecord[] {
|
||||||
|
const d = getDb();
|
||||||
|
const sanitized = query.replace(/['"]/g, '').trim();
|
||||||
|
if (!sanitized) return [];
|
||||||
|
const ftsPattern = sanitized.split(/\s+/).map(w => `"${w}"`).join(' OR ');
|
||||||
|
const sqlQuery = `
|
||||||
|
SELECT f.* FROM files f
|
||||||
|
JOIN resources_fts r ON r.rowid = f.rowid
|
||||||
|
WHERE resources_fts MATCH ?
|
||||||
|
ORDER BY rank
|
||||||
|
LIMIT 100
|
||||||
|
`;
|
||||||
|
const sqliteDb = _sqliteDb!;
|
||||||
|
const stmt = sqliteDb.prepareSync(sqlQuery);
|
||||||
|
const result = stmt.executeSync<FileRow>(ftsPattern);
|
||||||
|
const rows: FileRow[] = [];
|
||||||
|
for (const r of result) {
|
||||||
|
rows.push(r as unknown as FileRow);
|
||||||
|
}
|
||||||
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -248,7 +365,7 @@ export const fileStore = {
|
|||||||
.where(or(like(files.name, pattern), like(files.ocrText, pattern)))
|
.where(or(like(files.name, pattern), like(files.ocrText, pattern)))
|
||||||
.orderBy(desc(files.createdAt))
|
.orderBy(desc(files.createdAt))
|
||||||
.limit(100)
|
.limit(100)
|
||||||
.all();
|
.all() as FileRow[];
|
||||||
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -262,15 +379,16 @@ export const fileStore = {
|
|||||||
ocrText?: string;
|
ocrText?: string;
|
||||||
tags?: Tag[];
|
tags?: Tag[];
|
||||||
isFolder: boolean;
|
isFolder: boolean;
|
||||||
parentFileId?: string;
|
parentResourceId?: string;
|
||||||
thumbnailUrl?: string;
|
thumbnailUrl?: string;
|
||||||
|
ownerId?: string;
|
||||||
}>) {
|
}>) {
|
||||||
const d = getDb();
|
const d = getDb();
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
d.transaction(() => {
|
d.transaction(() => {
|
||||||
for (const bf of backendFiles) {
|
for (const bf of backendFiles) {
|
||||||
const existing = d.select().from(files).where(eq(files.backendId, bf.id)).get();
|
const existing = d.select().from(files).where(eq(files.backendId, bf.id)).get() as FileRow | undefined;
|
||||||
|
|
||||||
const source = existing && existing.localUri ? 'synced' : 'cloud';
|
const source = existing && existing.localUri ? 'synced' : 'cloud';
|
||||||
const syncStatus = existing && existing.localUri
|
const syncStatus = existing && existing.localUri
|
||||||
@@ -286,10 +404,11 @@ export const fileStore = {
|
|||||||
source,
|
source,
|
||||||
localUri: existing?.localUri ?? null,
|
localUri: existing?.localUri ?? null,
|
||||||
syncStatus,
|
syncStatus,
|
||||||
parentFileId: bf.parentFileId ?? null,
|
parentResourceId: bf.parentResourceId ?? null,
|
||||||
isFolder: bf.isFolder ? 1 : 0,
|
isFolder: bf.isFolder ? 1 : 0,
|
||||||
ocrText: bf.ocrText ?? null,
|
ocrText: bf.ocrText ?? null,
|
||||||
thumbnailUrl: bf.thumbnailUrl ?? null,
|
thumbnailUrl: bf.thumbnailUrl ?? null,
|
||||||
|
ownerId: bf.ownerId ?? null,
|
||||||
createdAt: bf.createdAt,
|
createdAt: bf.createdAt,
|
||||||
updatedAt: bf.updatedAt ?? now,
|
updatedAt: bf.updatedAt ?? now,
|
||||||
lastSyncedAt: now,
|
lastSyncedAt: now,
|
||||||
@@ -327,10 +446,11 @@ export const fileStore = {
|
|||||||
source: 'local',
|
source: 'local',
|
||||||
localUri: df.uri,
|
localUri: df.uri,
|
||||||
syncStatus: 'local',
|
syncStatus: 'local',
|
||||||
parentFileId: df.folderId ?? null,
|
parentResourceId: df.folderId ?? null,
|
||||||
isFolder: 0,
|
isFolder: 0,
|
||||||
ocrText: null,
|
ocrText: null,
|
||||||
thumbnailUrl: null,
|
thumbnailUrl: null,
|
||||||
|
ownerId: null,
|
||||||
createdAt: df.createdAt,
|
createdAt: df.createdAt,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
lastSyncedAt: null,
|
lastSyncedAt: null,
|
||||||
@@ -348,8 +468,9 @@ export const fileStore = {
|
|||||||
if (updates.source !== undefined) setFields.source = updates.source;
|
if (updates.source !== undefined) setFields.source = updates.source;
|
||||||
if (updates.thumbnailUrl !== undefined) setFields.thumbnailUrl = updates.thumbnailUrl;
|
if (updates.thumbnailUrl !== undefined) setFields.thumbnailUrl = updates.thumbnailUrl;
|
||||||
if (updates.ocrText !== undefined) setFields.ocrText = updates.ocrText;
|
if (updates.ocrText !== undefined) setFields.ocrText = updates.ocrText;
|
||||||
if (updates.parentFileId !== undefined) setFields.parentFileId = updates.parentFileId;
|
if (updates.parentResourceId !== undefined) setFields.parentResourceId = updates.parentResourceId;
|
||||||
if (updates.name !== undefined) setFields.name = updates.name;
|
if (updates.name !== undefined) setFields.name = updates.name;
|
||||||
|
if (updates.ownerId !== undefined) setFields.ownerId = updates.ownerId;
|
||||||
setFields.updatedAt = new Date().toISOString();
|
setFields.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
d.update(files).set(setFields).where(eq(files.id, id)).run();
|
d.update(files).set(setFields).where(eq(files.id, id)).run();
|
||||||
@@ -390,7 +511,7 @@ export const fileStore = {
|
|||||||
|
|
||||||
deleteByBackendId(backendId: string) {
|
deleteByBackendId(backendId: string) {
|
||||||
const d = getDb();
|
const d = getDb();
|
||||||
const row = d.select().from(files).where(eq(files.backendId, backendId)).get();
|
const row = d.select().from(files).where(eq(files.backendId, backendId)).get() as FileRow | undefined;
|
||||||
if (row) this.markDeleted(row.id);
|
if (row) this.markDeleted(row.id);
|
||||||
d.delete(files).where(eq(files.backendId, backendId)).run();
|
d.delete(files).where(eq(files.backendId, backendId)).run();
|
||||||
},
|
},
|
||||||
@@ -411,7 +532,7 @@ export const fileStore = {
|
|||||||
const d = getDb();
|
const d = getDb();
|
||||||
const rows = d.select().from(files)
|
const rows = d.select().from(files)
|
||||||
.where(or(eq(files.source, 'local'), eq(files.source, 'synced')))
|
.where(or(eq(files.source, 'local'), eq(files.source, 'synced')))
|
||||||
.all();
|
.all() as FileRow[];
|
||||||
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -419,7 +540,7 @@ export const fileStore = {
|
|||||||
const d = getDb();
|
const d = getDb();
|
||||||
const rows = d.select().from(files)
|
const rows = d.select().from(files)
|
||||||
.where(eq(files.source, 'synced'))
|
.where(eq(files.source, 'synced'))
|
||||||
.all();
|
.all() as FileRow[];
|
||||||
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -427,7 +548,7 @@ export const fileStore = {
|
|||||||
const d = getDb();
|
const d = getDb();
|
||||||
const rows = d.select().from(files)
|
const rows = d.select().from(files)
|
||||||
.where(and(eq(files.syncStatus, 'local'), sql`${files.backendId} IS NULL`))
|
.where(and(eq(files.syncStatus, 'local'), sql`${files.backendId} IS NULL`))
|
||||||
.all();
|
.all() as FileRow[];
|
||||||
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
return rows.map((r) => rowToRecord(r, getTagsForFile(r.id)));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,112 +1,5 @@
|
|||||||
import { createMMKV } from 'react-native-mmkv';
|
import { initDB } from './index';
|
||||||
import { fileStore, FileRecord } from './index';
|
|
||||||
import type { Tag, SyncStatus } from '../../types';
|
|
||||||
|
|
||||||
const metadataStorage = createMMKV({ id: 'vaultdrop-metadata' });
|
|
||||||
const localFilesStorage = createMMKV({ id: 'vaultdrop-local-files' });
|
|
||||||
|
|
||||||
interface LegacyCachedFiles {
|
|
||||||
files: Array<{
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
mimeType: string;
|
|
||||||
size: number;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
ocrText?: string;
|
|
||||||
tags?: Tag[];
|
|
||||||
isFolder: boolean;
|
|
||||||
parentFileId?: string;
|
|
||||||
url?: string;
|
|
||||||
thumbnailUrl?: string;
|
|
||||||
}>;
|
|
||||||
page: number;
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LegacyRegistryBlob {
|
|
||||||
entries: Record<string, {
|
|
||||||
id: string;
|
|
||||||
backendFileId?: string;
|
|
||||||
localUri: string;
|
|
||||||
name: string;
|
|
||||||
mimeType: string;
|
|
||||||
size: number;
|
|
||||||
syncStatus: SyncStatus;
|
|
||||||
createdAt: string;
|
|
||||||
tags?: Tag[];
|
|
||||||
folderId?: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function migrateFromLegacy() {
|
export function migrateFromLegacy() {
|
||||||
const count = fileStore.count();
|
initDB();
|
||||||
if (count > 0) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rawMetadata = metadataStorage.getString('backend_files_cache');
|
|
||||||
if (rawMetadata) {
|
|
||||||
const cached: LegacyCachedFiles = JSON.parse(rawMetadata);
|
|
||||||
for (const f of cached.files) {
|
|
||||||
fileStore.upsert({
|
|
||||||
id: f.id,
|
|
||||||
backendId: f.id,
|
|
||||||
name: f.name,
|
|
||||||
mimeType: f.mimeType,
|
|
||||||
size: f.size,
|
|
||||||
source: 'cloud',
|
|
||||||
localUri: null,
|
|
||||||
syncStatus: 'cloud',
|
|
||||||
parentFileId: f.parentFileId ?? null,
|
|
||||||
isFolder: f.isFolder ? 1 : 0,
|
|
||||||
ocrText: f.ocrText ?? null,
|
|
||||||
thumbnailUrl: f.thumbnailUrl ?? null,
|
|
||||||
createdAt: f.createdAt,
|
|
||||||
updatedAt: f.updatedAt,
|
|
||||||
lastSyncedAt: new Date().toISOString(),
|
|
||||||
tags: f.tags,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rawRegistry = localFilesStorage.getString('local_files_v2');
|
|
||||||
if (rawRegistry) {
|
|
||||||
const blob: LegacyRegistryBlob = JSON.parse(rawRegistry);
|
|
||||||
for (const entry of Object.values(blob.entries)) {
|
|
||||||
const existing = fileStore.getByBackendId(entry.backendFileId ?? '');
|
|
||||||
const source = entry.backendFileId
|
|
||||||
? (entry.syncStatus === 'synced' ? 'synced' : 'cloud')
|
|
||||||
: 'local';
|
|
||||||
|
|
||||||
fileStore.upsert({
|
|
||||||
id: entry.id,
|
|
||||||
backendId: entry.backendFileId ?? null,
|
|
||||||
name: entry.name,
|
|
||||||
mimeType: entry.mimeType,
|
|
||||||
size: entry.size,
|
|
||||||
source,
|
|
||||||
localUri: entry.localUri,
|
|
||||||
syncStatus: entry.syncStatus,
|
|
||||||
parentFileId: entry.folderId ?? null,
|
|
||||||
isFolder: 0,
|
|
||||||
ocrText: null,
|
|
||||||
thumbnailUrl: null,
|
|
||||||
createdAt: entry.createdAt,
|
|
||||||
updatedAt: entry.createdAt,
|
|
||||||
lastSyncedAt: entry.backendFileId ? new Date().toISOString() : null,
|
|
||||||
tags: entry.tags,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existing && entry.localUri) {
|
|
||||||
fileStore.updatePartial(entry.id, {
|
|
||||||
localUri: entry.localUri,
|
|
||||||
source: 'synced',
|
|
||||||
syncStatus: 'synced',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,20 +11,22 @@ export const files = sqliteTable(
|
|||||||
source: text('source').notNull().default('cloud'),
|
source: text('source').notNull().default('cloud'),
|
||||||
localUri: text('local_uri'),
|
localUri: text('local_uri'),
|
||||||
syncStatus: text('sync_status').notNull().default('cloud'),
|
syncStatus: text('sync_status').notNull().default('cloud'),
|
||||||
parentFileId: text('parent_file_id'),
|
parentResourceId: text('parent_resource_id'),
|
||||||
isFolder: integer('is_folder').notNull().default(0),
|
isFolder: integer('is_folder').notNull().default(0),
|
||||||
ocrText: text('ocr_text'),
|
ocrText: text('ocr_text'),
|
||||||
thumbnailUrl: text('thumbnail_url'),
|
thumbnailUrl: text('thumbnail_url'),
|
||||||
|
ownerId: text('owner_id'),
|
||||||
createdAt: text('created_at').notNull(),
|
createdAt: text('created_at').notNull(),
|
||||||
updatedAt: text('updated_at').notNull(),
|
updatedAt: text('updated_at').notNull(),
|
||||||
lastSyncedAt: text('last_synced_at'),
|
lastSyncedAt: text('last_synced_at'),
|
||||||
},
|
},
|
||||||
(t) => [
|
(t) => [
|
||||||
index('idx_files_backend_id').on(t.backendId),
|
index('idx_files_backend_id').on(t.backendId),
|
||||||
index('idx_files_parent_id').on(t.parentFileId),
|
index('idx_files_parent_resource_id').on(t.parentResourceId),
|
||||||
index('idx_files_source').on(t.source),
|
index('idx_files_source').on(t.source),
|
||||||
index('idx_files_is_folder').on(t.isFolder),
|
index('idx_files_is_folder').on(t.isFolder),
|
||||||
index('idx_files_sync_status').on(t.syncStatus),
|
index('idx_files_sync_status').on(t.syncStatus),
|
||||||
|
index('idx_files_owner_id').on(t.ownerId),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -36,7 +38,6 @@ export const fileTags = sqliteTable(
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => files.id, { onDelete: 'cascade' }),
|
.references(() => files.id, { onDelete: 'cascade' }),
|
||||||
tagName: text('tag_name').notNull(),
|
tagName: text('tag_name').notNull(),
|
||||||
tagType: text('tag_type').notNull().default('none'),
|
|
||||||
},
|
},
|
||||||
(t) => [index('idx_file_tags_file_id').on(t.fileId)],
|
(t) => [index('idx_file_tags_file_id').on(t.fileId)],
|
||||||
);
|
);
|
||||||
@@ -45,3 +46,11 @@ export const deletedFiles = sqliteTable('deleted_files', {
|
|||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
deletedAt: text('deleted_at').notNull(),
|
deletedAt: text('deleted_at').notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const deviceInfo = sqliteTable('device_info', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
serverId: text('server_id'),
|
||||||
|
deviceName: text('device_name').notNull().default(''),
|
||||||
|
platform: text('platform').notNull().default(''),
|
||||||
|
registeredAt: text('registered_at'),
|
||||||
|
});
|
||||||
|
|||||||
+37
-6
@@ -1,7 +1,7 @@
|
|||||||
export interface Thumbnail {
|
export interface Variant {
|
||||||
id: string;
|
id: string;
|
||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
resolutionLabel: string;
|
variantType: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
url: string;
|
url: string;
|
||||||
@@ -10,7 +10,7 @@ export interface Thumbnail {
|
|||||||
|
|
||||||
export interface UnifiedFileItem {
|
export interface UnifiedFileItem {
|
||||||
id: string;
|
id: string;
|
||||||
backendFileId?: string;
|
backendResourceId?: string;
|
||||||
name: string;
|
name: string;
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
size: number;
|
size: number;
|
||||||
@@ -22,10 +22,11 @@ export interface UnifiedFileItem {
|
|||||||
ocrText?: string;
|
ocrText?: string;
|
||||||
tags: Tag[];
|
tags: Tag[];
|
||||||
isFolder: boolean;
|
isFolder: boolean;
|
||||||
parentFileId?: string;
|
parentResourceId?: string;
|
||||||
|
ownerId?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
thumbnailUrl?: string;
|
thumbnailUrl?: string;
|
||||||
thumbnails?: Thumbnail[];
|
variants?: Variant[];
|
||||||
isDeviceFile?: boolean;
|
isDeviceFile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +39,6 @@ export function isFolder(file: UnifiedFileItem | { isFolder: boolean }): boolean
|
|||||||
export interface Tag {
|
export interface Tag {
|
||||||
id: string;
|
id: string;
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
tag_type: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OcrJob {
|
export interface OcrJob {
|
||||||
@@ -125,3 +125,34 @@ export interface RefreshResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SyncStatus = 'local' | 'syncing' | 'synced' | 'cloud' | 'conflict';
|
export type SyncStatus = 'local' | 'syncing' | 'synced' | 'cloud' | 'conflict';
|
||||||
|
|
||||||
|
export interface Device {
|
||||||
|
id: string;
|
||||||
|
device_name: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncQueueItem {
|
||||||
|
id: string;
|
||||||
|
resourceId: string;
|
||||||
|
storageLocationId: string;
|
||||||
|
operation: string;
|
||||||
|
status: string;
|
||||||
|
attempts: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShareEntry {
|
||||||
|
user_id: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourcePlacement {
|
||||||
|
id: string;
|
||||||
|
resourceId: string;
|
||||||
|
storageLocationId: string;
|
||||||
|
status: string;
|
||||||
|
storageKey: string | null;
|
||||||
|
syncedAt: string | null;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user