import React, { useMemo, useCallback, useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
ActivityIndicator,
Alert,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialIcons } from '@expo/vector-icons';
import { fileStore, FileRecord } from '../services/fileStore';
import { safDirectory } from '../services/safDirectory';
import { useSyncQueue } from '../hooks/useSyncQueue';
import { useAutoSync } from '../hooks/useAutoSync';
import { useSyncPush } from '../hooks/useSyncPush';
import { useUploadQueue } from '../hooks/useUploadQueue';
import { UploadTask } from '../services/uploadQueue';
function formatSize(bytes: number): string {
if (bytes === 0) return '';
if (bytes < 1024) return `${bytes} o`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} Ko`;
return `${(bytes / (1024 * 1024)).toFixed(1)} Mo`;
}
function uploadStatusIcon(task: UploadTask) {
switch (task.status) {
case 'pending':
return ;
case 'uploading':
return ;
case 'done':
return ;
case 'error':
return ;
}
}
export function SyncDetailScreen() {
const insets = useSafeAreaInsets();
const { pendingCount, isSyncing, refresh } = useSyncQueue();
const { triggerSync } = 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 pendingFiles = useMemo(() => {
return fileStore.getPendingSync();
}, [listVersion]);
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 triggerSync();
try {
const { getStoredDeviceServerId } = await import('../hooks/useDeviceRegistration');
const serverId = await getStoredDeviceServerId();
if (serverId) {
await push(serverId);
}
} catch { }
refresh();
bumpList();
}, [triggerSync, 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);
triggerSync().finally(() => {
refresh();
bumpList();
});
},
},
],
);
}, [triggerSync, 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 hasUploads = uploadTasks.length > 0;
const hasPending = pendingFiles.length > 0;
const hasErrors = errorFiles.length > 0;
const hasContent = hasUploads || hasPending || hasErrors;
return (
{(hasPending || hasErrors || uploadErrors.length > 0) && (
{isSyncing ? (
) : (
0 ? "refresh" : "cloud-upload"}
size={20}
color="#fff"
/>
)}
{isSyncing ? 'Synchronisation...'
: uploadErrors.length > 0
? `Tout réessayer (${uploadErrors.length + (hasPending ? pendingCount : 0)})`
: `Synchroniser (${pendingCount})`}
)}
{!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: 'section', label: 'Fichiers locaux à synchroniser' } as const] : []),
...pendingFiles.map((f) => ({ type: 'file' as const, data: f })),
]}
keyExtractor={(item) =>
item.type === 'section' ? item.label : item.data.id
}
renderItem={({ item }) => {
if (item.type === 'section') {
return {item.label};
}
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
);
}
const file = item.data;
return (
{file.name}
{formatSize(file.size)}
{file.mimeType ? ` · ${file.mimeType.split('/').pop()}` : ''}
);
}}
contentContainerStyle={styles.list}
/>
)}
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
syncBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#1976D2',
marginHorizontal: 16,
marginTop: 16,
borderRadius: 10,
paddingVertical: 14,
gap: 10,
},
syncBtnDisabled: {
backgroundColor: '#90CAF9',
},
syncBtnText: {
fontSize: 16,
color: '#fff',
fontWeight: '600',
},
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,
},
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',
},
});