import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react'; import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator, Alert, Animated, Easing, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { fileStore, FileRecord } from '../services/fileStore'; import { useSyncQueue, useSyncProgress } from '../hooks/useSyncQueue'; import { useAutoSync } from '../hooks/useAutoSync'; import { useSyncPush } from '../hooks/useSyncPush'; import { useUploadQueue } from '../hooks/useUploadQueue'; import { UploadTask } from '../services/uploadQueue'; function uploadStatusIcon(task: UploadTask) { switch (task.status) { case 'pending': return ; case 'uploading': return ; case 'done': return ; case 'error': return ; } } function SpinningSyncIcon({ size, color }: { size: number; color: string }) { const spin = useRef(new Animated.Value(0)).current; useEffect(() => { const animation = Animated.loop( Animated.timing(spin, { toValue: 1, duration: 1200, easing: Easing.linear, useNativeDriver: true, }) ); animation.start(); return () => animation.stop(); }, [spin]); const rotate = spin.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'], }); return ( ); } type PendingFile = { id: string; name: string }; const PendingSyncCard = React.memo(function PendingSyncCard({ syncing, pendingCount, shortList, onSyncPress, onCancel, }: { syncing: boolean; pendingCount: number; shortList: PendingFile[] | null; onSyncPress: () => void; onCancel: () => void; }) { return ( {syncing ? ( ) : ( )} {syncing ? 'Synchronisation en cours...' : pendingCount > 1 ? `Vous avez ${pendingCount} fichiers locaux pouvant être synchronisés` : 'Vous avez 1 fichier local pouvant être synchronisé'} {syncing ? shortList && shortList[0] ? `En cours : ${shortList[0].name}` : 'Synchronisation en cours...' : 'Appuyer maintenant pour les synchroniser'} {!syncing && } {syncing && shortList && shortList.length > 0 && ( {shortList.map((f, i) => ( {i === 0 ? ( ) : ( )} {f.name} ))} )} {syncing && ( Arrêter la synchronisation )} ); }); export function SyncDetailScreen() { const { pendingCount, isSyncing, refresh } = useSyncQueue(); const { syncProgress } = useSyncProgress(); const { syncManually, cancelSync } = useAutoSync(); const { push } = useSyncPush(); const { tasks: uploadTasks, retry, retryAll } = useUploadQueue(); const [listVersion, setListVersion] = useState(0); const bumpList = useCallback(() => setListVersion((v) => v + 1), []); useEffect(() => { const interval = setInterval(bumpList, 5000); return () => clearInterval(interval); }, [bumpList]); const errorFiles = useMemo(() => { return fileStore.getErrorFiles(); }, [listVersion]); const uploadErrors = uploadTasks.filter((t) => t.status === 'error'); const handleSyncAll = useCallback(async () => { for (const f of fileStore.getErrorFiles()) { fileStore.resetSyncError(f.id); } await syncManually(); try { const { getStoredDeviceServerId } = await import('../hooks/useDeviceRegistration'); const serverId = await getStoredDeviceServerId(); if (serverId) { await push(serverId); } } catch { } refresh(); bumpList(); }, [syncManually, push, refresh, bumpList]); const handleSyncButtonPress = useCallback(async () => { if (uploadErrors.length > 0) { retryAll(); } await handleSyncAll(); }, [uploadErrors.length, retryAll, handleSyncAll]); const handleErrorFilePress = useCallback((file: FileRecord) => { Alert.alert( 'Fichier en erreur', 'Réessayer la synchronisation de ce fichier ?', [ { text: 'Annuler', style: 'cancel' }, { text: 'Réessayer', onPress: () => { fileStore.resetSyncError(file.id); syncManually().finally(() => { refresh(); bumpList(); }); }, }, ], ); }, [syncManually, refresh, bumpList]); const handleTaskPress = useCallback((task: UploadTask) => { if (task.status !== 'error') return; Alert.alert( 'Erreur d\'upload', task.error || 'Erreur inconnue', [ { text: 'Annuler', style: 'cancel' }, { text: 'Réessayer', onPress: () => retry(task.id) }, ], ); }, [retry]); const syncing = !!syncProgress; const hasUploads = uploadTasks.length > 0; const hasPending = pendingCount > 0 || syncing; const hasErrors = errorFiles.length > 0; const hasContent = hasUploads || hasPending || hasErrors; const shortList = useMemo(() => { if (!syncProgress) return null; const { files, currentIndex } = syncProgress; const start = Math.max(0, currentIndex); return files.slice(start, start + 5); }, [syncProgress]); return ( {!hasContent ? ( Tout est synchronisé Aucun fichier en attente ) : ( ({ type: 'upload' as const, data: t })), ...(hasErrors ? [{ type: 'section', label: 'Fichiers en erreur' } as const] : []), ...errorFiles.map((f) => ({ type: 'error' as const, data: f })), ...(hasPending ? [{ type: 'pendingCard' } as const] : []), ]} keyExtractor={(item) => item.type === 'section' || item.type === 'pendingCard' ? item.type === 'pendingCard' ? 'pending-card' : item.label : item.data.id } extraData={[isSyncing, syncProgress]} renderItem={({ item }) => { if (item.type === 'section') { return {item.label}; } if (item.type === 'pendingCard') { return ( ); } if (item.type === 'upload') { const task = item.data; return ( handleTaskPress(task)} activeOpacity={task.status === 'error' ? 0.6 : 1} > {uploadStatusIcon(task)} {task.file.name} {task.status === 'uploading' && ( )} {task.status === 'error' && task.error && ( {task.error} )} {task.status === 'done' && ( Upload terminé )} {task.status === 'pending' && ( En attente )} ); } if (item.type === 'error') { const file = item.data; return ( handleErrorFilePress(file)} activeOpacity={0.6} > {file.name} Échec de synchronisation · toucher pour réessayer ); } return null; }} contentContainerStyle={styles.list} /> )} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#f5f5f5', }, list: { padding: 16, }, headerText: { fontSize: 14, color: '#666', marginBottom: 12, }, fileRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', borderRadius: 10, padding: 12, marginBottom: 8, gap: 12, }, pendingCard: { backgroundColor: '#fff', borderRadius: 14, padding: 16, marginBottom: 8, gap: 14, shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 8, shadowOffset: { width: 0, height: 2 }, elevation: 2, }, pendingCardTop: { flexDirection: 'row', alignItems: 'center', gap: 14, }, pendingCardIcon: { width: 48, height: 48, borderRadius: 24, backgroundColor: '#E3F2FD', justifyContent: 'center', alignItems: 'center', }, pendingCardInfo: { flex: 1, }, pendingCardTitle: { fontSize: 15, color: '#333', fontWeight: '600', }, pendingCardSubtitle: { fontSize: 13, color: '#1976D2', marginTop: 4, }, pendingList: { borderTopWidth: 1, borderTopColor: '#f0f0f0', paddingTop: 12, gap: 8, }, pendingRow: { flexDirection: 'row', alignItems: 'center', gap: 10, }, pendingRowText: { flex: 1, fontSize: 13, color: '#999', }, pendingRowActive: { color: '#1976D2', fontWeight: '500', }, stopBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, backgroundColor: '#E53935', borderRadius: 8, paddingVertical: 10, marginTop: 4, }, stopBtnText: { fontSize: 14, color: '#fff', fontWeight: '600', }, fileInfo: { flex: 1, }, fileName: { fontSize: 15, color: '#333', fontWeight: '500', }, fileMeta: { fontSize: 12, color: '#999', marginTop: 2, }, localBadge: { width: 28, height: 28, borderRadius: 14, backgroundColor: '#f5f5f5', justifyContent: 'center', alignItems: 'center', }, fileRowError: { backgroundColor: '#FFF0F0', }, progressBar: { height: 4, backgroundColor: '#E0E0E0', borderRadius: 2, marginTop: 6, overflow: 'hidden', }, progressFill: { height: '100%', backgroundColor: '#1976D2', borderRadius: 2, }, errorText: { fontSize: 12, color: '#E53935', marginTop: 2, }, doneText: { fontSize: 12, color: '#4CAF50', marginTop: 2, }, pendingText: { fontSize: 12, color: '#FFA000', marginTop: 2, }, empty: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 12, }, emptyTitle: { fontSize: 18, fontWeight: '600', color: '#333', }, emptySubtitle: { fontSize: 14, color: '#999', }, });