From 4119e84f3326e286cc39dabba82c16a342478cb1 Mon Sep 17 00:00:00 2001 From: m Date: Wed, 29 Jul 2026 23:05:18 +0200 Subject: [PATCH] fix upload edit --- mobile/app/file-edit.tsx | 312 +++++++++++++++++++++++++++------------ 1 file changed, 220 insertions(+), 92 deletions(-) diff --git a/mobile/app/file-edit.tsx b/mobile/app/file-edit.tsx index efa069b..daeb9d3 100644 --- a/mobile/app/file-edit.tsx +++ b/mobile/app/file-edit.tsx @@ -1,77 +1,100 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useMemo, useRef } from 'react'; import { View, Text, StyleSheet, FlatList, TouchableOpacity, - Image, TextInput, Alert, ActivityIndicator, Dimensions, + Modal, } from 'react-native'; -import { RouteProp, useRoute, useNavigation } from '@react-navigation/native'; +import { Image } from 'expo-image'; import { MaterialIcons } from '@expo/vector-icons'; -import { useFile, useAddTags } from '../hooks/useFiles'; +import { RouteProp, useRoute, useNavigation } from '@react-navigation/native'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { useAddTags } from '../hooks/useFiles'; import { usePdfGeneration } from '../hooks/usePdfGeneration'; import { useUpload } from '../hooks/useUpload'; import { TagChip } from '../components/TagChip'; import { FileThumbnail } from '../components/FileThumbnail'; -import { FileItem } from '../types'; +import { ZoomableImage } from '../components/ZoomableImage'; +import { fileStore } from '../services/fileStore'; import { apiClient } from '../api/client'; import { ENDPOINTS } from '../constants/api'; +import type { SyncStatus, Variant } from '../types'; 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 PADDING = 16; +const ITEM_GAP = 6; +const ITEM_SIZE = (SCREEN_WIDTH - PADDING * 2 - (NUM_COLUMNS - 1) * ITEM_GAP) / NUM_COLUMNS; type FileEditRouteParams = { FileEdit: { fileIds: string[] }; }; +type PreviewFile = { uri: string; width: number; height: number }; + interface FileEditItemProps { fileId: string; selected: boolean; - onPress: () => void; + onSelect: (id: string) => void; + onPreview: (id: string) => void; + size: number; } -function FileEditItem({ fileId, selected, onPress }: FileEditItemProps) { - const { data: fileData, isLoading: fileLoading } = useFile(fileId); - const file = fileData as any; - const uri = file?.url; - const isLoading = fileLoading; +const FileEditItem = React.memo(function FileEditItem({ fileId, selected, onSelect, onPreview, size }: FileEditItemProps) { + const record = fileStore.getByBackendId(fileId) ?? fileStore.getById(fileId); - if (isLoading) { - return ( - - - - ); - } + const uri = record?.localUri ?? undefined; + const thumbnailUrl = record?.thumbnailUrl ?? undefined; + const mimeType = record?.mimeType ?? 'application/octet-stream'; + const fileName = record?.name ?? fileId; + const syncStatus = (record?.syncStatus ?? 'cloud') as SyncStatus; + const isViewable = mimeType.startsWith('image/') || mimeType === 'application/pdf'; return ( - - {uri && file?.mimeType?.startsWith('image/') ? ( - - ) : ( + + { if (isViewable) onPreview(fileId); else onSelect(fileId); }} + > + + + {isViewable && ( + onPreview(fileId)} + hitSlop={6} + > + + )} - {selected && ( - - - - + + onSelect(fileId)} + hitSlop={8} + > + + {selected && } - )} - + + ); -} +}); export function FileEditScreen() { const route = useRoute>(); @@ -86,6 +109,9 @@ export function FileEditScreen() { const [pendingTags, setPendingTags] = useState([]); const [uploading, setUploading] = useState(false); const [selectedIds, setSelectedIds] = useState>(new Set()); + const [previewFiles, setPreviewFiles] = useState(null); + const [previewIndex, setPreviewIndex] = useState(0); + const [previewLoading, setPreviewLoading] = useState(false); const hasSelection = selectedIds.size > 0; const targetIds = hasSelection ? Array.from(selectedIds) : fileIds; @@ -106,6 +132,58 @@ export function FileEditScreen() { }); }, [fileIds]); + const openPreview = useCallback((files: PreviewFile[], index: number) => { + setPreviewFiles(files); + setPreviewIndex(index); + }, []); + + const handlePreview = useCallback(async (fileId: string) => { + const record = fileStore.getByBackendId(fileId) ?? fileStore.getById(fileId); + + if (record?.localUri) { + openPreview([{ uri: record.localUri, width: SCREEN_WIDTH, height: SCREEN_WIDTH }], 0); + return; + } + + setPreviewLoading(true); + try { + const data = await apiClient.get(`${ENDPOINTS.RESOURCES}/${fileId}`); + const url = data?.url; + if (url) { + const fullVariants: Variant[] = (data?.variants ?? []) + .filter((v: Variant) => v.variantType === 'thumbnail_full') + .sort((a: Variant, b: Variant) => a.pageNumber - b.pageNumber); + + if (fullVariants.length > 0) { + const pages: PreviewFile[] = fullVariants.map((v) => ({ + uri: v.url, + width: v.width, + height: v.height, + })); + openPreview(pages, 0); + } else { + openPreview([{ uri: url, width: SCREEN_WIDTH, height: SCREEN_WIDTH }], 0); + } + } else { + Alert.alert('Erreur', 'Impossible de charger l\'aperçu'); + } + } catch { + Alert.alert('Erreur', 'Impossible de charger l\'aperçu'); + } finally { + setPreviewLoading(false); + } + }, [openPreview]); + + const handleSwipeVertical = useCallback((direction: 'up' | 'down') => { + setPreviewIndex((prev) => { + if (!previewFiles || previewFiles.length <= 1) return prev; + const next = direction === 'up' + ? Math.min(prev + 1, previewFiles.length - 1) + : Math.max(prev - 1, 0); + return next; + }); + }, [previewFiles]); + const handleAddTag = () => { const tag = tagInput.trim().toLowerCase(); if (!tag || pendingTags.includes(tag)) return; @@ -131,15 +209,15 @@ export function FileEditScreen() { setUploading(true); try { - const imageUris: { uri: string }[] = []; - - for (const fileId of targetIds) { - const data = await apiClient.get<{ url: string }>(`${ENDPOINTS.RESOURCES}/${fileId}`); - const url = data?.url; - if (url) { - imageUris.push({ uri: url }); - } - } + const results = await Promise.all( + targetIds.map(async (fileId) => { + const record = fileStore.getByBackendId(fileId) ?? fileStore.getById(fileId); + if (record?.localUri) return { uri: record.localUri }; + const data = await apiClient.get<{ url: string }>(`${ENDPOINTS.RESOURCES}/${fileId}`); + return { uri: data?.url || '' }; + }) + ); + const imageUris = results.filter((r) => r.uri); if (imageUris.length === 0) { Alert.alert('Erreur', 'Aucune image trouvée pour la génération du PDF'); @@ -184,7 +262,17 @@ export function FileEditScreen() { } }, [targetIds, generatePdf, upload, navigation]); - const isLoading = generating || uploading; + const isLoading = generating || uploading || previewLoading; + + const renderItem = useCallback(({ item }: { item: string }) => ( + + ), [hasSelection, selectedIds, toggleSelection, handlePreview]); return ( @@ -199,7 +287,9 @@ export function FileEditScreen() { - {hasSelection && selectedIds.size === fileIds.length ? 'Tout' : 'Tout'} + + {hasSelection && selectedIds.size === fileIds.length ? 'Tout' : 'Tout'} + @@ -210,13 +300,7 @@ export function FileEditScreen() { keyExtractor={(item) => item} contentContainerStyle={styles.grid} columnWrapperStyle={styles.gridRow} - renderItem={({ item }) => ( - toggleSelection(item)} - /> - )} + renderItem={renderItem} /> @@ -261,6 +345,12 @@ export function FileEditScreen() { )} + {previewLoading && ( + + + Chargement de l'aperçu... + + )} Créer un PDF + + setPreviewFiles(null)} + > + {previewFiles && ( + + setPreviewFiles(null)} + onSwipeVertical={handleSwipeVertical} + /> + {previewFiles.length > 1 && ( + + + {previewIndex + 1} / {previewFiles.length} + + + )} + + )} + ); } @@ -284,7 +402,7 @@ const styles = StyleSheet.create({ backgroundColor: '#fff', }, header: { - padding: 16, + padding: PADDING, borderBottomWidth: 1, borderBottomColor: '#e0e0e0', }, @@ -314,47 +432,13 @@ const styles = StyleSheet.create({ color: '#1976D2', }, grid: { - padding: 16, + padding: PADDING, }, gridRow: { - gap: 6, - }, - gridItem: { - width: ITEM_SIZE, - height: ITEM_SIZE, - borderRadius: 6, - overflow: 'hidden', - backgroundColor: '#f0f0f0', - marginBottom: 6, - }, - gridItemLoading: { - justifyContent: 'center', - alignItems: 'center', - }, - selectedOverlay: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - justifyContent: 'flex-start', - alignItems: 'flex-end', - padding: 4, - }, - checkCircle: { - width: 24, - height: 24, - borderRadius: 12, - backgroundColor: '#1976D2', - justifyContent: 'center', - alignItems: 'center', - }, - thumb: { - width: '100%', - height: '100%', + gap: ITEM_GAP, }, tagSection: { - padding: 16, + padding: PADDING, borderTopWidth: 1, borderTopColor: '#e0e0e0', }, @@ -405,7 +489,7 @@ const styles = StyleSheet.create({ fontWeight: '600', }, footer: { - padding: 16, + padding: PADDING, borderTopWidth: 1, borderTopColor: '#e0e0e0', gap: 8, @@ -437,4 +521,48 @@ const styles = StyleSheet.create({ fontSize: 16, fontWeight: '600', }, + previewBtn: { + position: 'absolute', + top: 4, + right: 28, + width: 24, + height: 24, + borderRadius: 12, + backgroundColor: 'rgba(0,0,0,0.5)', + justifyContent: 'center', + alignItems: 'center', + }, + selectBtn: { + position: 'absolute', + bottom: 4, + right: 4, + padding: 4, + }, + checkCircle: { + width: 22, + height: 22, + borderRadius: 11, + borderWidth: 2, + borderColor: '#fff', + backgroundColor: 'rgba(0,0,0,0.3)', + justifyContent: 'center', + alignItems: 'center', + }, + checkCircleSelected: { + backgroundColor: '#1976D2', + borderColor: '#1976D2', + }, + modalPagination: { + position: 'absolute', + bottom: 40, + alignSelf: 'center', + backgroundColor: 'rgba(0,0,0,0.5)', + borderRadius: 12, + paddingHorizontal: 12, + paddingVertical: 4, + }, + modalPaginationText: { + color: '#fff', + fontSize: 13, + }, });