import React, { useMemo, useState, useCallback, useEffect } from 'react'; import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, Modal, TextInput } 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 { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useDeleteFile, useFiles, useFreeLocalSpace, useAddTags, useMoveResources, useFolders, useCreateFolder, } from '../hooks/useFiles'; import { SelectionPanel } from '../components/SelectionPanel'; import { ConfirmModal, type ConfirmOption } from '../components/ConfirmModal'; 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; const ITEM_SIZE = (SCREEN_WIDTH - 16 * 2 - (NUM_COLUMNS - 1) * 6) / NUM_COLUMNS; const PAGE_SIZE = 100; type RootStackParamList = { Folder: { folderId: string; folderName: string }; FileDetail: { fileIds: string[]; initialIndex: number }; FileEdit: { fileIds: string[] }; }; type NavigationProp = NativeStackNavigationProp; type FolderRouteProp = RouteProp; function FolderGridItem({ file, onPress, onLongPress, selected, onFolderPress }: { file: UnifiedFileItem; onPress?: () => void; onLongPress?: () => void; selected?: boolean; onFolderPress?: () => void; }) { const folder = isFolder(file); return ( {selected && ( )} {file.name} ); } export function FolderScreen() { const route = useRoute(); const navigation = useNavigation(); const { folderId, folderName } = route.params; const [page, setPage] = useState(1); const { data, isLoading, isFetching } = useFiles(folderId, page, PAGE_SIZE); const deleteFile = useDeleteFile(); const freeLocalSpace = useFreeLocalSpace(); const queryClient = useQueryClient(); const [selectedIds, setSelectedIds] = useState>(new Set()); const addTags = useAddTags(); const moveFiles = useMoveResources(); const createFolder = useCreateFolder(); const { data: foldersData } = useFolders(); const insets = useSafeAreaInsets(); const loadMore = useCallback(() => { if (isFetching) return; const total = data?.meta?.total ?? 0; const loaded = data?.data?.length ?? 0; if (loaded < total) { setPage((p) => p + 1); } }, [isFetching, data?.meta?.total, data?.data?.length]); const hasMore = (data?.data?.length ?? 0) > 0 && (data?.data?.length ?? 0) < (data?.meta?.total ?? 0); const [tagModalVisible, setTagModalVisible] = useState(false); const [tagModalMode, setTagModalMode] = useState<'tag' | 'folder'>('tag'); const [tagInput, setTagInput] = useState(''); const [moveModalVisible, setMoveModalVisible] = useState(false); const [confirmDeleteState, setConfirmDeleteState] = useState<{ message: string; options: ConfirmOption[] } | null>(null); const selectionMode = selectedIds.size > 0; useEffect(() => { navigation.setOptions({ title: folderName }); }, [navigation, folderName]); const files = useMemo(() => { return data?.data ?? []; }, [data]); 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 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: ConfirmOption[] = []; if (hasSynced) { options.push({ label: '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()); }, }); } options.push({ label: 'Du device + serveur', destructive: true, onPress: async () => { for (const id of ids) { const f = files.find((fi) => fi.id === id); if (f?.backendResourceId) { await apiClient.delete(`${ENDPOINTS.RESOURCES}/${f.backendResourceId}`); fileStore.deleteByBackendId(f.backendResourceId); } else { fileStore.deleteById(id); } if (f?.localUri) { try { await deleteAsync(f.localUri, { idempotent: true }); } catch {} } downloadRegistry.remove(id); } await queryClient.invalidateQueries({ queryKey: ['resources'] }); setSelectedIds(new Set()); }, }); options.push({ label: 'Annuler' }); setConfirmDeleteState({ message: `Supprimer ${label} ?`, options }); }, [selectedIds, files, deleteFile, freeLocalSpace, queryClient]); const handleEdit = useCallback(() => { const ids = Array.from(selectedIds); if (ids.length === 0) return; navigation.navigate('FileEdit', { fileIds: ids }); setSelectedIds(new Set()); }, [selectedIds, navigation]); const openTagModal = useCallback((mode: 'tag' | 'folder') => { setTagModalMode(mode); setTagInput(''); setTagModalVisible(true); }, []); const handleAddTag = useCallback(async () => { const name = tagInput.trim(); if (!name) return; const ids = Array.from(selectedIds); for (const id of ids) { await addTags.mutateAsync({ fileId: id, tags: [name] }); } setTagModalVisible(false); setSelectedIds(new Set()); }, [tagInput, selectedIds, addTags]); const handleCreateFolderFromModal = useCallback(async () => { const name = tagInput.trim(); if (!name) return; const ids = Array.from(selectedIds); try { const newFolder = await createFolder.mutateAsync({ name, parentResourceId: folderId }); await moveFiles.mutateAsync({ resourceIds: ids, parentResourceId: newFolder.id }); setTagModalVisible(false); setSelectedIds(new Set()); navigation.navigate('Folder', { folderId: newFolder.id, folderName: newFolder.name }); } catch {} }, [tagInput, selectedIds, createFolder, moveFiles, navigation, folderId]); const handleMove = useCallback(async (folderId: string | null) => { const ids = Array.from(selectedIds); await moveFiles.mutateAsync({ resourceIds: ids, parentResourceId: folderId }); setMoveModalVisible(false); setSelectedIds(new Set()); }, [selectedIds, moveFiles]); const handleItemPress = useCallback((file: UnifiedFileItem) => { 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: UnifiedFileItem) => { if (!selectionMode) toggleSelection(file.id); }, [selectionMode, toggleSelection]); if (isLoading) { return ( Chargement... ); } return ( item.id} contentContainerStyle={styles.list} onEndReached={loadMore} onEndReachedThreshold={0.5} ListEmptyComponent={ Dossier vide } ListFooterComponent={ isFetching ? ( Chargement... ) : hasMore ? ( Charger plus ) : null } renderItem={({ item: file }) => ( handleItemPress(file)} onLongPress={() => handleItemLongPress(file)} onFolderPress={() => navigation.push('Folder', { folderId: file.id, folderName: file.name })} /> )} /> {selectionMode && ( openTagModal('tag')} onFolder={() => openTagModal('folder')} onMove={() => setMoveModalVisible(true)} insetsBottom={insets.bottom} /> )} setTagModalVisible(false)}> setTagModalVisible(false)}> {}}> {tagModalMode === 'folder' ? 'Créer un dossier' : 'Ajouter un tag'} setTagModalVisible(false)}> Annuler {tagModalMode === 'folder' ? 'Créer' : 'Ajouter'} setMoveModalVisible(false)}> setMoveModalVisible(false)}> {}}> Déplacer vers... handleMove(null)} > Racine {(foldersData ?? []).map((folder) => ( handleMove(folder.id)} > {folder.name} ))} setConfirmDeleteState(null)} /> ); } 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', }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'center', alignItems: 'center', }, modalContent: { backgroundColor: '#fff', borderRadius: 12, padding: 20, width: '80%', }, modalTitle: { fontSize: 18, fontWeight: '700', color: '#333', marginBottom: 16, }, modalInput: { borderWidth: 1, borderColor: '#ddd', borderRadius: 8, paddingHorizontal: 12, paddingVertical: 10, fontSize: 16, color: '#333', marginBottom: 16, }, modalActions: { flexDirection: 'row', justifyContent: 'flex-end', gap: 12, }, modalCancelBtn: { paddingHorizontal: 16, paddingVertical: 8, }, modalCancelText: { fontSize: 15, color: '#666', }, modalConfirmBtn: { paddingHorizontal: 16, paddingVertical: 8, backgroundColor: '#1976D2', borderRadius: 8, }, modalConfirmDisabled: { backgroundColor: '#ccc', }, modalConfirmText: { fontSize: 15, color: '#fff', fontWeight: '600', }, folderOption: { flexDirection: 'row', alignItems: 'center', paddingVertical: 12, paddingHorizontal: 8, gap: 10, borderBottomWidth: 1, borderBottomColor: '#f0f0f0', }, folderOptionText: { fontSize: 16, color: '#333', }, footer: { paddingVertical: 20, alignItems: 'center', }, footerText: { fontSize: 14, color: '#999', }, footerLink: { fontSize: 14, color: '#1976D2', fontWeight: '600', }, });