sqlite database
This commit is contained in:
@@ -14,9 +14,8 @@ import {
|
||||
} from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { RouteProp, useRoute } from '@react-navigation/native';
|
||||
import { useFile, useFileImage } from '../hooks/useFiles';
|
||||
import { useDownloadFile } from '../hooks/useUnifiedFiles';
|
||||
import { localFileRegistry } from '../services/localFileRegistry';
|
||||
import { useFile, useFileImage, useDownloadFile } from '../hooks/useFiles';
|
||||
import { fileStore } from '../services/fileStore';
|
||||
import { TagChip } from '../components/TagChip';
|
||||
import { FileThumbnail } from '../components/FileThumbnail';
|
||||
import { SyncStatusBadge } from '../components/SyncStatusBadge';
|
||||
@@ -53,7 +52,7 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
|
||||
const uri = isDevice ? deviceFile.localUri : (imageData?.data?.url);
|
||||
const file = fileData as any;
|
||||
|
||||
const localEntry = localFileRegistry.getByBackendId(fileId);
|
||||
const localEntry = fileStore.getByBackendId(fileId);
|
||||
const syncStatus: SyncStatus = isDevice
|
||||
? 'local'
|
||||
: localEntry
|
||||
@@ -77,6 +76,7 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
|
||||
mimeType: file.mimeType ?? 'application/octet-stream',
|
||||
size: file.data?.size ?? 0,
|
||||
createdAt: file.createdAt ?? new Date().toISOString(),
|
||||
source: 'cloud',
|
||||
syncStatus: 'cloud',
|
||||
tags: file.data?.tags ?? [],
|
||||
isFolder: false,
|
||||
|
||||
+59
-24
@@ -3,11 +3,16 @@ import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, Alert }
|
||||
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { useDeleteFile, useFileImage } from '../hooks/useFiles';
|
||||
import { useUnifiedFilesByParent } from '../hooks/useUnifiedFiles';
|
||||
import { UnifiedFileItem } from '../hooks/useUnifiedFiles';
|
||||
import { useDeleteFile, useFileImage, useFiles, useFreeLocalSpace } from '../hooks/useFiles';
|
||||
import { UnifiedFileItem } from '../types';
|
||||
import { isFolder } from '../types';
|
||||
import { FileThumbnail } from '../components/FileThumbnail';
|
||||
import { fileStore } from '../services/fileStore';
|
||||
import { downloadRegistry } from '../services/downloadRegistry';
|
||||
import { apiClient } from '../api/client';
|
||||
import { ENDPOINTS } from '../constants/api';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { deleteAsync } from 'expo-file-system/legacy';
|
||||
|
||||
const NUM_COLUMNS = 3;
|
||||
const SCREEN_WIDTH = Dimensions.get('window').width;
|
||||
@@ -65,8 +70,10 @@ export function FolderScreen() {
|
||||
const route = useRoute<FolderRouteProp>();
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const { folderId, folderName } = route.params;
|
||||
const { data, isLoading } = useUnifiedFilesByParent(folderId);
|
||||
const { data, isLoading } = useFiles(folderId);
|
||||
const deleteFile = useDeleteFile();
|
||||
const freeLocalSpace = useFreeLocalSpace();
|
||||
const queryClient = useQueryClient();
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
const selectionMode = selectedIds.size > 0;
|
||||
@@ -76,7 +83,7 @@ export function FolderScreen() {
|
||||
}, [navigation, folderName]);
|
||||
|
||||
const files = useMemo(() => {
|
||||
return data ?? [];
|
||||
return data?.data ?? [];
|
||||
}, [data]);
|
||||
|
||||
const fileIdToIndex = useMemo(() => {
|
||||
@@ -99,26 +106,54 @@ export function FolderScreen() {
|
||||
}, []);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
const count = selectedIds.size;
|
||||
if (count === 0) return;
|
||||
Alert.alert(
|
||||
'Supprimer',
|
||||
`Supprimer ${count === 1 ? 'ce fichier' : `ces ${count} fichiers`} ?`,
|
||||
[
|
||||
{ text: 'Annuler', style: 'cancel' },
|
||||
{
|
||||
text: 'Supprimer',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
for (const id of selectedIds) {
|
||||
await deleteFile.mutateAsync(id);
|
||||
}
|
||||
setSelectedIds(new Set());
|
||||
},
|
||||
const ids = Array.from(selectedIds);
|
||||
if (ids.length === 0) return;
|
||||
const hasSynced = ids.some((id) => {
|
||||
const f = files.find((fi) => fi.id === id);
|
||||
return f?.syncStatus === 'synced';
|
||||
});
|
||||
const label = ids.length === 1 ? 'ce fichier' : `ces ${ids.length} fichiers`;
|
||||
const options: Array<{ text: string; style?: 'default' | 'cancel' | 'destructive'; onPress?: () => void }> = [
|
||||
{ text: 'Annuler', style: 'cancel' },
|
||||
];
|
||||
if (hasSynced) {
|
||||
options.push({
|
||||
text: 'Du device uniquement',
|
||||
onPress: async () => {
|
||||
const syncedIds = ids.filter((id) => {
|
||||
const f = files.find((fi) => fi.id === id);
|
||||
return f?.syncStatus === 'synced';
|
||||
});
|
||||
if (syncedIds.length > 0) {
|
||||
await freeLocalSpace.mutateAsync(syncedIds);
|
||||
}
|
||||
setSelectedIds(new Set());
|
||||
},
|
||||
]
|
||||
);
|
||||
}, [selectedIds, deleteFile]);
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
text: 'Du device + serveur',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
for (const id of ids) {
|
||||
const f = files.find((fi) => fi.id === id);
|
||||
if (f?.backendFileId) {
|
||||
await apiClient.delete(`${ENDPOINTS.FILES}/${f.backendFileId}`);
|
||||
fileStore.deleteByBackendId(f.backendFileId);
|
||||
} else {
|
||||
fileStore.deleteById(id);
|
||||
}
|
||||
if (f?.localUri) {
|
||||
try { await deleteAsync(f.localUri, { idempotent: true }); } catch {}
|
||||
}
|
||||
downloadRegistry.remove(id);
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||
setSelectedIds(new Set());
|
||||
},
|
||||
});
|
||||
Alert.alert('Supprimer', `Supprimer ${label} ?`, options);
|
||||
}, [selectedIds, files, deleteFile, freeLocalSpace, queryClient]);
|
||||
|
||||
const handleItemPress = useCallback((file: UnifiedFileItem) => {
|
||||
if (selectionMode) {
|
||||
|
||||
+56
-58
@@ -4,10 +4,8 @@ import { useNavigation } from '@react-navigation/native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { useDeleteFile, useAddTags, useMoveFiles, useFolders } from '../hooks/useFiles';
|
||||
import { useUnifiedFiles, useFreeLocalSpace } from '../hooks/useUnifiedFiles';
|
||||
import { UnifiedFileItem } from '../hooks/useUnifiedFiles';
|
||||
import { FileItem, isFolder } from '../types';
|
||||
import { useDeleteFile, useAddTags, useMoveFiles, useFolders, useFiles, useFreeLocalSpace } from '../hooks/useFiles';
|
||||
import { UnifiedFileItem, isFolder } from '../types';
|
||||
import { SearchBar, SearchFilters } from '../components/SearchBar';
|
||||
import { FileThumbnail } from '../components/FileThumbnail';
|
||||
import { SettingsModal } from '../components/SettingsModal';
|
||||
@@ -15,6 +13,13 @@ import { SyncStatusIcon } from '../components/SyncStatusIcon';
|
||||
import { useSyncQueue } from '../hooks/useSyncQueue';
|
||||
import { useAutoSync } from '../hooks/useAutoSync';
|
||||
import { safDirectory, StoredFolder, SyncMode, SyncGlobalMode } from '../services/safDirectory';
|
||||
import { fileStore } from '../services/fileStore';
|
||||
import { downloadRegistry } from '../services/downloadRegistry';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '../api/client';
|
||||
import { ENDPOINTS } from '../constants/api';
|
||||
import { useLocalFiles } from '../hooks/useLocalFiles';
|
||||
import { deleteAsync } from 'expo-file-system/legacy';
|
||||
|
||||
const NUM_COLUMNS = 3;
|
||||
const SCREEN_WIDTH = Dimensions.get('window').width;
|
||||
@@ -157,9 +162,11 @@ function matchesQuery(file: UnifiedFileItem, query: string, filters: SearchFilte
|
||||
export function HomeScreen() {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { data, isLoading, error, hasPermission, requestPermission, pickDirectory, folders, refreshFolders } = useUnifiedFiles();
|
||||
const { data, isLoading, error } = useFiles();
|
||||
const { hasPermission, requestPermission, pickDirectory, folders, refreshFolders } = useLocalFiles();
|
||||
const deleteFile = useDeleteFile();
|
||||
const freeLocalSpace = useFreeLocalSpace();
|
||||
const queryClient = useQueryClient();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filters, setFilters] = useState<SearchFilters>({ name: true, ocrText: true, tags: true });
|
||||
const [keyboardOpen, setKeyboardOpen] = useState(false);
|
||||
@@ -203,7 +210,7 @@ export function HomeScreen() {
|
||||
|
||||
const selectionMode = selectedIds.size > 0;
|
||||
|
||||
const files = data ?? [];
|
||||
const files = data?.data ?? [];
|
||||
|
||||
const filteredFiles = useMemo(
|
||||
() => searchQuery.trim() ? files.filter((f) => matchesQuery(f, searchQuery, filters)) : files,
|
||||
@@ -233,55 +240,54 @@ export function HomeScreen() {
|
||||
}, []);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
const count = selectedIds.size;
|
||||
if (count === 0) return;
|
||||
const label = count === 1 ? 'ce fichier' : `ces ${count} fichiers`;
|
||||
Alert.alert(
|
||||
'Supprimer',
|
||||
`Supprimer ${label} ?`,
|
||||
[
|
||||
{ text: 'Annuler', style: 'cancel' },
|
||||
{
|
||||
text: 'Supprimer',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
const ids = Array.from(selectedIds);
|
||||
for (const id of ids) {
|
||||
await deleteFile.mutateAsync(id);
|
||||
}
|
||||
setSelectedIds(new Set());
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
}, [selectedIds, deleteFile]);
|
||||
|
||||
const handleFreeSpace = useCallback(() => {
|
||||
const ids = Array.from(selectedIds);
|
||||
const syncedIds = ids.filter((id) => {
|
||||
if (ids.length === 0) return;
|
||||
const hasSynced = ids.some((id) => {
|
||||
const f = files.find((fi) => fi.id === id);
|
||||
return f?.syncStatus === 'synced';
|
||||
});
|
||||
if (syncedIds.length === 0) {
|
||||
Alert.alert('Info', 'Aucun fichier avec copie locale sélectionné.');
|
||||
return;
|
||||
}
|
||||
const label = syncedIds.length === 1 ? 'ce fichier' : `ces ${syncedIds.length} fichiers`;
|
||||
Alert.alert(
|
||||
'Libérer l\'espace',
|
||||
`${label} sera supprimé de votre appareil mais restera disponible sur le serveur. Vous pourrez le re-télécharger ultérieurement.`,
|
||||
[
|
||||
{ text: 'Annuler', style: 'cancel' },
|
||||
{
|
||||
text: 'Libérer',
|
||||
onPress: async () => {
|
||||
const label = ids.length === 1 ? 'ce fichier' : `ces ${ids.length} fichiers`;
|
||||
const options: Array<{ text: string; style?: 'default' | 'cancel' | 'destructive'; onPress?: () => void }> = [
|
||||
{ text: 'Annuler', style: 'cancel' },
|
||||
];
|
||||
if (hasSynced) {
|
||||
options.push({
|
||||
text: 'Du device uniquement',
|
||||
onPress: async () => {
|
||||
const syncedIds = ids.filter((id) => {
|
||||
const f = files.find((fi) => fi.id === id);
|
||||
return f?.syncStatus === 'synced';
|
||||
});
|
||||
if (syncedIds.length > 0) {
|
||||
await freeLocalSpace.mutateAsync(syncedIds);
|
||||
setSelectedIds(new Set());
|
||||
},
|
||||
}
|
||||
setSelectedIds(new Set());
|
||||
},
|
||||
]
|
||||
);
|
||||
}, [selectedIds, files, freeLocalSpace]);
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
text: 'Du device + serveur',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
for (const id of ids) {
|
||||
const f = files.find((fi) => fi.id === id);
|
||||
if (f?.backendFileId) {
|
||||
await apiClient.delete(`${ENDPOINTS.FILES}/${f.backendFileId}`);
|
||||
fileStore.deleteByBackendId(f.backendFileId);
|
||||
} else {
|
||||
fileStore.deleteById(id);
|
||||
}
|
||||
if (f?.localUri) {
|
||||
try { await deleteAsync(f.localUri, { idempotent: true }); } catch {}
|
||||
}
|
||||
downloadRegistry.remove(id);
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||
setSelectedIds(new Set());
|
||||
},
|
||||
});
|
||||
Alert.alert('Supprimer', `Supprimer ${label} ?`, options);
|
||||
}, [selectedIds, files, deleteFile, freeLocalSpace, queryClient]);
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
const ids = Array.from(selectedIds);
|
||||
@@ -542,13 +548,6 @@ export function HomeScreen() {
|
||||
<MaterialIcons name="drive-file-move" size={18} color="#00897B" />
|
||||
<Text style={[styles.chipText, { color: '#00897B' }]}>Déplacer</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.selectionChip, styles.freeSpaceChip]}
|
||||
onPress={handleFreeSpace}
|
||||
>
|
||||
<MaterialIcons name="free-cancellation" size={18} color="#FF8F00" />
|
||||
<Text style={[styles.chipText, { color: '#FF8F00' }]}>Libérer</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
) : (
|
||||
@@ -619,7 +618,7 @@ export function HomeScreen() {
|
||||
<MaterialIcons name="home" size={20} color="#666" />
|
||||
<Text style={styles.folderOptionText}>Racine</Text>
|
||||
</TouchableOpacity>
|
||||
{(foldersData?.data ?? []).map((folder) => (
|
||||
{(foldersData ?? []).map((folder) => (
|
||||
<TouchableOpacity
|
||||
key={folder.id}
|
||||
style={styles.folderOption}
|
||||
@@ -840,7 +839,6 @@ const styles = StyleSheet.create({
|
||||
tagChip: { backgroundColor: '#F3E5F5' },
|
||||
folderChip: { backgroundColor: '#FFF3E0' },
|
||||
moveChip: { backgroundColor: '#E0F2F1' },
|
||||
freeSpaceChip: { backgroundColor: '#FFF8E1' },
|
||||
folderOption: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -9,10 +9,9 @@ import {
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { localFileRegistry } from '../services/localFileRegistry';
|
||||
import { fileStore, FileRecord } from '../services/fileStore';
|
||||
import { safDirectory } from '../services/safDirectory';
|
||||
import { useSyncQueue } from '../hooks/useSyncQueue';
|
||||
import { LocalFileEntry } from '../types';
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes === 0) return '';
|
||||
@@ -26,9 +25,7 @@ export function SyncDetailScreen() {
|
||||
const { pendingCount, isSyncing, refresh } = useSyncQueue();
|
||||
|
||||
const pendingFiles = useMemo(() => {
|
||||
return localFileRegistry.getAll().filter(
|
||||
(entry) => !entry.backendFileId && entry.syncStatus === 'local'
|
||||
);
|
||||
return fileStore.getPendingSync();
|
||||
}, []);
|
||||
|
||||
const handleSyncAll = useCallback(() => {
|
||||
@@ -36,7 +33,7 @@ export function SyncDetailScreen() {
|
||||
console.log(`[SyncDetail] sync demandé pour ${pendingFiles.length} fichiers`);
|
||||
}, [pendingFiles.length]);
|
||||
|
||||
const renderItem = useCallback(({ item }: { item: LocalFileEntry }) => (
|
||||
const renderItem = useCallback(({ item }: { item: FileRecord }) => (
|
||||
<View style={styles.fileRow}>
|
||||
<MaterialIcons name="insert-drive-file" size={20} color="#999" />
|
||||
<View style={styles.fileInfo}>
|
||||
|
||||
Reference in New Issue
Block a user