diff --git a/mobile/app/upload.tsx b/mobile/app/upload.tsx index 3031ba1..cda28db 100644 --- a/mobile/app/upload.tsx +++ b/mobile/app/upload.tsx @@ -7,41 +7,52 @@ import { UploadProgress } from '../components/UploadProgress'; export function UploadScreen() { const [uploadStatus, setUploadStatus] = useState<'idle' | 'uploading' | 'processing' | 'success' | 'error'>('idle'); const [error, setError] = useState(); + const [uploadedCount, setUploadedCount] = useState(0); + const [totalCount, setTotalCount] = useState(0); const upload = useUpload(); - const pickImage = async () => { + const pickImages = async () => { try { const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== 'granted') { - Alert.alert('Permission requise', "L'accès à la galerie est nécessaire pour sélectionner une photo."); + Alert.alert('Permission requise', "L'accès à la galerie est nécessaire pour sélectionner des photos."); return; } - console.log('yes') + const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], quality: 1, + allowsMultipleSelection: true, }); - console.log(result) + if (result.canceled) return; - if (result.assets && result.assets[0]) { - setUploadStatus('uploading'); - const asset = result.assets[0]; - const file = { - uri: asset.uri, - type: asset.mimeType || 'image/jpeg', - name: asset.fileName || 'photo.jpg', - }; + const assets = result.assets; + if (assets.length === 0) return; - try { - const response = await upload.mutateAsync(file); - setUploadStatus('processing'); - console.log('Upload success:', response); - setUploadStatus('success'); - } catch (err) { + setUploadStatus('uploading'); + setUploadedCount(0); + setTotalCount(assets.length); + + const files = assets.map((asset) => ({ + uri: asset.uri, + type: asset.mimeType || 'image/jpeg', + name: asset.fileName || 'photo.jpg', + })); + + try { + const response = await upload.mutateAsync(files); + setUploadedCount(response.uploaded.length); + + if (response.errors.length > 0) { setUploadStatus('error'); - setError(err instanceof Error ? err.message : 'Upload failed'); + setError(`${response.uploaded.length}/${totalCount} uploadés. Erreurs : ${response.errors.map((e) => e.name).join(', ')}`); + } else { + setUploadStatus('success'); } + } catch (err) { + setUploadStatus('error'); + setError(err instanceof Error ? err.message : 'Upload failed'); } } catch (err) { Alert.alert('Erreur', "Impossible d'accéder à la galerie"); @@ -50,10 +61,15 @@ export function UploadScreen() { return ( - + - - Sélectionner une photo + + Sélectionner des photos ); diff --git a/mobile/components/UploadProgress.tsx b/mobile/components/UploadProgress.tsx index e53aeda..ac0fc10 100644 --- a/mobile/components/UploadProgress.tsx +++ b/mobile/components/UploadProgress.tsx @@ -5,18 +5,24 @@ interface UploadProgressProps { progress?: number; status: 'idle' | 'uploading' | 'processing' | 'success' | 'error'; error?: string; + uploadedCount?: number; + totalCount?: number; } -export function UploadProgress({ progress, status, error }: UploadProgressProps) { +export function UploadProgress({ progress, status, error, uploadedCount, totalCount }: UploadProgressProps) { if (status === 'idle') return null; + const hasMulti = totalCount !== undefined && totalCount > 1; + return ( {status === 'uploading' && ( <> - Upload en cours... {progress !== undefined && `${progress}%`} + {hasMulti + ? `Upload ${uploadedCount || 0}/${totalCount}...` + : `Upload en cours...${progress !== undefined ? ` ${progress}%` : ''}`} )} @@ -29,7 +35,9 @@ export function UploadProgress({ progress, status, error }: UploadProgressProps) )} {status === 'success' && ( - Upload terminé ! + + {hasMulti ? `${uploadedCount} fichiers uploadés !` : 'Upload terminé !'} + )} {status === 'error' && ( diff --git a/mobile/hooks/useUpload.ts b/mobile/hooks/useUpload.ts index e6dfa08..88783d0 100644 --- a/mobile/hooks/useUpload.ts +++ b/mobile/hooks/useUpload.ts @@ -4,20 +4,43 @@ import { apiClient } from '../api/client'; import { API_BASE_URL, ENDPOINTS } from '../constants/api'; import { OcrJob } from '../types'; +export type UploadFile = { uri: string; type: string; name: string }; +export type UploadResult = { name: string; id: string }; + export function useUpload() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (file: { uri: string; type: string; name: string }) => { - const fsFile = new File(file.uri); - const result = await fsFile.upload(`${API_BASE_URL}${ENDPOINTS.UPLOAD}`, { - httpMethod: 'POST', - uploadType: UploadType.MULTIPART, - fieldName: 'file', - mimeType: file.type, + mutationFn: async (files: UploadFile[]) => { + const results = await Promise.allSettled( + files.map(async (file) => { + const fsFile = new File(file.uri); + const result = await fsFile.upload(`${API_BASE_URL}${ENDPOINTS.UPLOAD}`, { + httpMethod: 'POST', + uploadType: UploadType.MULTIPART, + fieldName: 'file', + mimeType: file.type, + }); + return JSON.parse(result.body) as UploadResult; + }) + ); + + const uploaded: UploadResult[] = []; + const errors: { name: string; error: string }[] = []; + + results.forEach((r, i) => { + if (r.status === 'fulfilled') { + uploaded.push(r.value); + } else { + errors.push({ name: files[i].name, error: r.reason?.message || 'Upload failed' }); + } }); - return JSON.parse(result.body) as { id: string; ocrJobId: string }; + if (errors.length > 0 && uploaded.length === 0) { + throw new Error(errors.map((e) => `${e.name}: ${e.error}`).join('\n')); + } + + return { uploaded, errors }; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['files'] });