sync files

This commit is contained in:
m
2026-07-27 21:44:57 +02:00
parent 5c85ffa5fe
commit cb9cea6ff3
14 changed files with 1032 additions and 114 deletions
+41 -110
View File
@@ -10,7 +10,11 @@ import { UnifiedFileItem } from '../hooks/useUnifiedFiles';
import { FileItem, isFolder } from '../types';
import { SearchBar, SearchFilters } from '../components/SearchBar';
import { FileThumbnail } from '../components/FileThumbnail';
import { safDirectory, StoredFolder } from '../services/safDirectory';
import { SettingsModal } from '../components/SettingsModal';
import { SyncStatusIcon } from '../components/SyncStatusIcon';
import { useSyncQueue } from '../hooks/useSyncQueue';
import { useAutoSync } from '../hooks/useAutoSync';
import { safDirectory, StoredFolder, SyncMode } from '../services/safDirectory';
const NUM_COLUMNS = 3;
const SCREEN_WIDTH = Dimensions.get('window').width;
@@ -23,6 +27,7 @@ type RootStackParamList = {
FileDetail: { fileIds: string[]; initialIndex: number; deviceFiles?: Record<string, { localUri: string; name: string; mimeType: string; createdAt: string }> };
FileEdit: { fileIds: string[] };
Folder: { folderId: string; folderName: string };
SyncDetail: undefined;
};
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
@@ -149,7 +154,9 @@ export function HomeScreen() {
const moveFiles = useMoveFiles();
const { data: foldersData } = useFolders();
const [moveModalVisible, setMoveModalVisible] = useState(false);
const [folderSheetVisible, setFolderSheetVisible] = useState(false);
const [settingsModalVisible, setSettingsModalVisible] = useState(false);
const { pendingCount, isSyncing } = useSyncQueue();
useAutoSync();
useEffect(() => {
const show = Keyboard.addListener('keyboardDidShow', () => setKeyboardOpen(true));
@@ -160,12 +167,19 @@ export function HomeScreen() {
useEffect(() => {
navigation.setOptions({
headerRight: () => (
<TouchableOpacity onPress={() => setFolderSheetVisible(true)} style={{ marginRight: 4, padding: 8 }}>
<MaterialIcons name="settings" size={22} color="#666" />
</TouchableOpacity>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<SyncStatusIcon
isSyncing={isSyncing}
pendingCount={pendingCount}
onPress={() => navigation.navigate('SyncDetail')}
/>
<TouchableOpacity onPress={() => setSettingsModalVisible(true)} style={{ marginRight: 4, padding: 8 }}>
<MaterialIcons name="settings" size={22} color="#666" />
</TouchableOpacity>
</View>
),
});
}, [navigation]);
}, [navigation, pendingCount, isSyncing]);
const selectionMode = selectedIds.size > 0;
@@ -305,10 +319,20 @@ export function HomeScreen() {
}, [refreshFolders]);
const handleAddFolder = useCallback(async () => {
setFolderSheetVisible(false);
setSettingsModalVisible(false);
await pickDirectory();
}, [pickDirectory]);
const handleUpdateSyncMode = useCallback((folderId: string, mode: SyncMode) => {
safDirectory.updateSyncMode(folderId, mode);
refreshFolders();
}, [refreshFolders]);
const handleUpdateSyncCellular = useCallback((folderId: string, enabled: boolean) => {
safDirectory.updateSyncCellular(folderId, enabled);
refreshFolders();
}, [refreshFolders]);
const handleItemPress = useCallback((file: UnifiedFileItem) => {
if (selectionMode) {
toggleSelection(file.id);
@@ -584,44 +608,16 @@ export function HomeScreen() {
</TouchableOpacity>
</Modal>
<Modal visible={folderSheetVisible} transparent animationType="slide" onRequestClose={() => setFolderSheetVisible(false)}>
<TouchableOpacity style={styles.bottomSheetOverlay} activeOpacity={1} onPress={() => setFolderSheetVisible(false)}>
<TouchableOpacity activeOpacity={1} style={styles.bottomSheet} onPress={() => {}}>
<View style={styles.bottomSheetHandle} />
<Text style={styles.bottomSheetTitle}>Mes dossiers</Text>
{folders.length === 0 ? (
<Text style={styles.emptyFoldersText}>Aucun dossier configuré</Text>
) : (
folders.map((folder) => (
<View key={folder.id} style={styles.folderRow}>
<MaterialIcons name="folder" size={20} color="#F57C00" />
<Text style={styles.folderRowName} numberOfLines={1}>{folder.name}</Text>
<TouchableOpacity
onPress={() => handleToggleFolderVisibility(folder.id)}
style={styles.folderRowAction}
>
<MaterialIcons
name={folder.visible ? 'visibility' : 'visibility-off'}
size={20}
color={folder.visible ? '#1976D2' : '#999'}
/>
</TouchableOpacity>
<TouchableOpacity
onPress={() => handleRemoveFolder(folder.id)}
style={styles.folderRowAction}
>
<MaterialIcons name="delete-outline" size={20} color="#E53935" />
</TouchableOpacity>
</View>
))
)}
<TouchableOpacity style={styles.addFolderBtn} onPress={handleAddFolder}>
<MaterialIcons name="add" size={20} color="#fff" />
<Text style={styles.addFolderBtnText}>Ajouter un dossier</Text>
</TouchableOpacity>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
<SettingsModal
visible={settingsModalVisible}
onClose={() => setSettingsModalVisible(false)}
folders={folders}
onToggleVisibility={handleToggleFolderVisibility}
onRemoveFolder={handleRemoveFolder}
onAddFolder={handleAddFolder}
onUpdateSyncMode={handleUpdateSyncMode}
onUpdateSyncCellular={handleUpdateSyncCellular}
/>
</KeyboardAvoidingView>
);
}
@@ -884,69 +880,4 @@ const styles = StyleSheet.create({
color: '#fff',
fontWeight: '600',
},
emptyFoldersText: {
fontSize: 14,
color: '#999',
textAlign: 'center',
marginBottom: 16,
},
folderRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
gap: 10,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
folderRowName: {
flex: 1,
fontSize: 15,
color: '#333',
},
folderRowAction: {
padding: 6,
},
addFolderBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F57C00',
borderRadius: 10,
paddingVertical: 12,
marginTop: 16,
gap: 8,
},
addFolderBtnText: {
fontSize: 15,
color: '#fff',
fontWeight: '600',
},
bottomSheetOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'flex-end',
},
bottomSheet: {
backgroundColor: '#fff',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
paddingHorizontal: 20,
paddingTop: 12,
paddingBottom: 32,
maxHeight: '70%',
},
bottomSheetHandle: {
width: 36,
height: 4,
borderRadius: 2,
backgroundColor: '#ddd',
alignSelf: 'center',
marginBottom: 16,
},
bottomSheetTitle: {
fontSize: 18,
fontWeight: '700',
color: '#333',
marginBottom: 16,
},
});
+1 -1
View File
@@ -28,7 +28,7 @@ export function OnboardingScreen() {
const complete = useCallback(() => {
onboardingStorage.setCompletedVersion(CURRENT_ONBOARDING_VERSION);
navigation.navigate('Home' as never);
navigation.reset({ index: 0, routes: [{ name: 'Home' as never }] });
}, [navigation]);
const handlePickDirectory = useCallback(async () => {
+176
View File
@@ -0,0 +1,176 @@
import React, { useMemo, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
ActivityIndicator,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialIcons } from '@expo/vector-icons';
import { localFileRegistry } from '../services/localFileRegistry';
import { safDirectory } from '../services/safDirectory';
import { useSyncQueue } from '../hooks/useSyncQueue';
import { LocalFileEntry } from '../types';
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`;
}
export function SyncDetailScreen() {
const insets = useSafeAreaInsets();
const { pendingCount, isSyncing, refresh } = useSyncQueue();
const pendingFiles = useMemo(() => {
return localFileRegistry.getAll().filter(
(entry) => !entry.backendFileId && entry.syncStatus === 'local'
);
}, []);
const handleSyncAll = useCallback(() => {
// TODO: lancer l'upload batch de tous les fichiers pending
console.log(`[SyncDetail] sync demandé pour ${pendingFiles.length} fichiers`);
}, [pendingFiles.length]);
const renderItem = useCallback(({ item }: { item: LocalFileEntry }) => (
<View style={styles.fileRow}>
<MaterialIcons name="insert-drive-file" size={20} color="#999" />
<View style={styles.fileInfo}>
<Text style={styles.fileName} numberOfLines={1}>{item.name}</Text>
<Text style={styles.fileMeta}>
{formatSize(item.size)}
{item.mimeType ? ` · ${item.mimeType.split('/').pop()}` : ''}
</Text>
</View>
<View style={styles.localBadge}>
<MaterialIcons name="phone-android" size={14} color="#757575" />
</View>
</View>
), []);
return (
<View style={styles.container}>
{pendingCount > 0 && (
<TouchableOpacity
style={[styles.syncBtn, isSyncing && styles.syncBtnDisabled]}
onPress={handleSyncAll}
disabled={isSyncing}
>
{isSyncing ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<MaterialIcons name="cloud-upload" size={20} color="#fff" />
)}
<Text style={styles.syncBtnText}>
{isSyncing ? 'Synchronisation...' : `Synchroniser (${pendingCount})`}
</Text>
</TouchableOpacity>
)}
{pendingFiles.length === 0 ? (
<View style={styles.empty}>
<MaterialIcons name="cloud-done" size={48} color="#4CAF50" />
<Text style={styles.emptyTitle}>Tout est synchronisé</Text>
<Text style={styles.emptySubtitle}>
Aucun fichier en attente d'upload
</Text>
</View>
) : (
<FlatList
data={pendingFiles}
keyExtractor={(item) => item.id}
renderItem={renderItem}
contentContainerStyle={styles.list}
ListHeaderComponent={
<Text style={styles.headerText}>
{pendingFiles.length} fichier{pendingFiles.length > 1 ? 's' : ''} en attente
</Text>
}
/>
)}
</View>
);
}
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',
},
empty: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
gap: 12,
},
emptyTitle: {
fontSize: 18,
fontWeight: '600',
color: '#333',
},
emptySubtitle: {
fontSize: 14,
color: '#999',
},
});