From c8f5935469b7539df02dece4d49a1adc73690b84 Mon Sep 17 00:00:00 2001 From: m Date: Mon, 13 Jul 2026 16:47:55 +0200 Subject: [PATCH] folder new interface --- mobile/App.tsx | 2 + mobile/app/folder.tsx | 288 +++++++++++++++++++++++++++++++++ mobile/app/index.tsx | 9 +- mobile/components/FileCard.tsx | 2 +- mobile/types/index.ts | 5 + 5 files changed, 302 insertions(+), 4 deletions(-) create mode 100644 mobile/app/folder.tsx diff --git a/mobile/App.tsx b/mobile/App.tsx index bcc143a..281df21 100644 --- a/mobile/App.tsx +++ b/mobile/App.tsx @@ -11,6 +11,7 @@ import { BatchReviewScreen } from './app/batch-review'; import { PendingReviewScreen } from './app/pending-review'; import { FileDetailScreen } from './app/file-detail'; import { FileEditScreen } from './app/file-edit'; +import { FolderScreen } from './app/folder'; const Stack = createNativeStackNavigator(); const queryClient = new QueryClient(); @@ -37,6 +38,7 @@ export default function App() { options={{ title: 'Détails', headerTintColor: '#fff', headerStyle: { backgroundColor: '#000' } }} /> + diff --git a/mobile/app/folder.tsx b/mobile/app/folder.tsx new file mode 100644 index 0000000..fd0ea22 --- /dev/null +++ b/mobile/app/folder.tsx @@ -0,0 +1,288 @@ +import React, { useMemo, useState, useCallback, useEffect } from 'react'; +import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, Alert } from 'react-native'; +import { useRoute, useNavigation, RouteProp } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { MaterialIcons } from '@expo/vector-icons'; +import { useFiles, useFileImage, useDeleteFile } from '../hooks/useFiles'; +import { FileItem, isFolder } from '../types'; +import { FileThumbnail } from '../components/FileThumbnail'; + +const NUM_COLUMNS = 3; +const SCREEN_WIDTH = Dimensions.get('window').width; +const ITEM_SIZE = (SCREEN_WIDTH - 16 * 2 - (NUM_COLUMNS - 1) * 6) / NUM_COLUMNS; + +type RootStackParamList = { + Folder: { folderId: string; folderName: string }; + FileDetail: { fileIds: string[]; initialIndex: number }; +}; + +type NavigationProp = NativeStackNavigationProp; + +type FolderRouteProp = RouteProp; + +function FolderGridItem({ file, onPress, onLongPress, selected, onFolderPress }: { + file: FileItem; + onPress?: () => void; + onLongPress?: () => void; + selected?: boolean; + onFolderPress?: () => void; +}) { + const { data, isLoading } = useFileImage(file.id); + const folder = isFolder(file); + + return ( + + + {selected && ( + + + + + + )} + {file.name} + + ); +} + +export function FolderScreen() { + const route = useRoute(); + const navigation = useNavigation(); + const { folderId, folderName } = route.params; + const { data, isLoading, error } = useFiles(); + const deleteFile = useDeleteFile(); + const [selectedIds, setSelectedIds] = useState>(new Set()); + + const selectionMode = selectedIds.size > 0; + + useEffect(() => { + navigation.setOptions({ title: folderName }); + }, [navigation, folderName]); + + const files = useMemo(() => { + const all = data?.data ?? []; + return all.filter((f) => f.parentFileId === folderId); + }, [data, folderId]); + + const fileIdToIndex = useMemo(() => { + const map = new Map(); + files.forEach((f, i) => map.set(f.id, i)); + return map; + }, [files]); + + const toggleSelection = useCallback((id: string) => { + setSelectedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }, []); + + const clearSelection = useCallback(() => { + setSelectedIds(new Set()); + }, []); + + 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()); + }, + }, + ] + ); + }, [selectedIds, deleteFile]); + + const handleItemPress = useCallback((file: FileItem) => { + if (selectionMode) { + toggleSelection(file.id); + } else if (isFolder(file)) { + navigation.push('Folder', { folderId: file.id, folderName: file.name }); + } else { + navigation.navigate('FileDetail', { + fileIds: files.map((f) => f.id), + initialIndex: fileIdToIndex.get(file.id) ?? 0, + }); + } + }, [selectionMode, toggleSelection, navigation, files, fileIdToIndex]); + + const handleItemLongPress = useCallback((file: FileItem) => { + if (!selectionMode) toggleSelection(file.id); + }, [selectionMode, toggleSelection]); + + if (isLoading) { + return ( + + Chargement... + + ); + } + + if (error) { + return ( + + Erreur de chargement + + ); + } + + return ( + + item.id} + contentContainerStyle={styles.list} + ListEmptyComponent={ + + + Dossier vide + + } + renderItem={({ item: file }) => ( + handleItemPress(file)} + onLongPress={() => handleItemLongPress(file)} + onFolderPress={() => navigation.push('Folder', { folderId: file.id, folderName: file.name })} + /> + )} + /> + + {selectionMode && ( + + + + + + + {selectedIds.size} sélectionnée{selectedIds.size > 1 ? 's' : ''} + + + + + Supprimer + + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, + center: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + infoText: { + fontSize: 16, + color: '#666', + }, + list: { + padding: 15, + }, + empty: { + paddingVertical: 60, + alignItems: 'center', + gap: 12, + }, + emptyText: { + fontSize: 16, + color: '#999', + }, + gridItem: { + width: ITEM_SIZE, + }, + gridItemSelected: { + opacity: 0.85, + }, + selectedOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 18, + justifyContent: 'flex-start', + alignItems: 'flex-end', + padding: 4, + }, + checkCircle: { + width: 24, + height: 24, + borderRadius: 12, + backgroundColor: '#1976D2', + justifyContent: 'center', + alignItems: 'center', + }, + fileName: { + fontSize: 11, + color: '#666', + marginTop: 4, + textAlign: 'center', + }, + selectionBar: { + backgroundColor: '#fff', + borderTopWidth: 1, + borderTopColor: '#e0e0e0', + paddingHorizontal: 16, + paddingVertical: 12, + }, + selectionHeader: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 12, + }, + cancelBtn: { + padding: 4, + }, + selectionCount: { + fontSize: 16, + fontWeight: '600', + color: '#333', + }, + deleteBtn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 10, + borderRadius: 8, + borderWidth: 1.5, + borderColor: '#F44336', + gap: 6, + }, + deleteText: { + fontSize: 15, + fontWeight: '600', + color: '#F44336', + }, +}); diff --git a/mobile/app/index.tsx b/mobile/app/index.tsx index 52a8dfe..54221c3 100644 --- a/mobile/app/index.tsx +++ b/mobile/app/index.tsx @@ -5,7 +5,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { MaterialIcons } from '@expo/vector-icons'; import { useFiles, useFileImage, useDeleteFile, useAddTags } from '../hooks/useFiles'; -import { FileItem } from '../types'; +import { FileItem, isFolder } from '../types'; import { SearchBar, SearchFilters } from '../components/SearchBar'; import { FileThumbnail } from '../components/FileThumbnail'; @@ -19,6 +19,7 @@ type RootStackParamList = { Scan: undefined; FileDetail: { fileIds: string[]; initialIndex: number }; FileEdit: { fileIds: string[] }; + Folder: { folderId: string; folderName: string }; }; type NavigationProp = NativeStackNavigationProp; @@ -111,14 +112,14 @@ function matchesQuery(file: FileItem, query: string, filters: SearchFilters): bo if (filters.name && file.name.toLowerCase().includes(q)) return true; if (filters.ocrText && file.ocrText?.toLowerCase().includes(q)) return true; if (filters.tags && file.tags?.some((t) => { - const tagName = typeof t === 'string' ? t : t.name; + const tagName = typeof t === 'string' ? t : t.tag_name; return tagName?.toLowerCase().includes(q); })) return true; if (!filters.name && !filters.ocrText && !filters.tags) { if (file.name.toLowerCase().includes(q)) return true; if (file.ocrText?.toLowerCase().includes(q)) return true; if (file.tags?.some((t) => { - const tagName = typeof t === 'string' ? t : t.name; + const tagName = typeof t === 'string' ? t : t.tag_name; return tagName?.toLowerCase().includes(q); })) return true; } @@ -231,6 +232,8 @@ export function HomeScreen() { const handleItemPress = useCallback((file: FileItem) => { if (selectionMode) { toggleSelection(file.id); + } else if (isFolder(file)) { + navigation.navigate('Folder', { folderId: file.id, folderName: file.name }); } else { navigation.navigate('FileDetail', { fileIds: filteredFiles.map((f) => f.id), diff --git a/mobile/components/FileCard.tsx b/mobile/components/FileCard.tsx index aad32f9..2ed5d11 100644 --- a/mobile/components/FileCard.tsx +++ b/mobile/components/FileCard.tsx @@ -43,7 +43,7 @@ export function FileCard({ file, onPress }: FileCardProps) { {file.tags.map((tag) => ( - + ))} diff --git a/mobile/types/index.ts b/mobile/types/index.ts index 450db5c..f3e1619 100644 --- a/mobile/types/index.ts +++ b/mobile/types/index.ts @@ -7,9 +7,14 @@ export interface FileItem { updatedAt: string; ocrText?: string; tags: Tag[]; + parentFileId?: string; url?: string; } +export function isFolder(file: FileItem): boolean { + return file.parentFileId != null; +} + export interface Tag { id: string; tag_name: string;