From c5a959a27ba7b541366a335949783b7b4c5a7e70 Mon Sep 17 00:00:00 2001 From: m Date: Mon, 27 Jul 2026 22:13:13 +0200 Subject: [PATCH] sync files --- mobile/app/index.tsx | 18 +- mobile/components/SettingsModal.tsx | 300 +++++++++++++++++++++------- mobile/hooks/useAutoSync.ts | 53 ++--- mobile/hooks/useLocalFiles.ts | 19 +- mobile/hooks/useSyncQueue.ts | 21 +- mobile/services/safDirectory.ts | 21 ++ 6 files changed, 324 insertions(+), 108 deletions(-) diff --git a/mobile/app/index.tsx b/mobile/app/index.tsx index 036113e..6639542 100644 --- a/mobile/app/index.tsx +++ b/mobile/app/index.tsx @@ -14,7 +14,7 @@ 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'; +import { safDirectory, StoredFolder, SyncMode, SyncGlobalMode } from '../services/safDirectory'; const NUM_COLUMNS = 3; const SCREEN_WIDTH = Dimensions.get('window').width; @@ -155,6 +155,8 @@ export function HomeScreen() { const { data: foldersData } = useFolders(); const [moveModalVisible, setMoveModalVisible] = useState(false); const [settingsModalVisible, setSettingsModalVisible] = useState(false); + const [globalSyncMode, setGlobalSyncMode] = useState(() => safDirectory.getGlobalSyncMode()); + const [globalSyncCellular, setGlobalSyncCellular] = useState(() => safDirectory.getGlobalSyncCellular()); const { pendingCount, isSyncing } = useSyncQueue(); useAutoSync(); @@ -333,6 +335,16 @@ export function HomeScreen() { refreshFolders(); }, [refreshFolders]); + const handleSetGlobalSyncMode = useCallback((mode: SyncGlobalMode) => { + safDirectory.setGlobalSyncMode(mode); + setGlobalSyncMode(mode); + }, []); + + const handleSetGlobalSyncCellular = useCallback((enabled: boolean) => { + safDirectory.setGlobalSyncCellular(enabled); + setGlobalSyncCellular(enabled); + }, []); + const handleItemPress = useCallback((file: UnifiedFileItem) => { if (selectionMode) { toggleSelection(file.id); @@ -617,6 +629,10 @@ export function HomeScreen() { onAddFolder={handleAddFolder} onUpdateSyncMode={handleUpdateSyncMode} onUpdateSyncCellular={handleUpdateSyncCellular} + globalSyncMode={globalSyncMode} + onSetGlobalSyncMode={handleSetGlobalSyncMode} + globalSyncCellular={globalSyncCellular} + onSetGlobalSyncCellular={handleSetGlobalSyncCellular} /> ); diff --git a/mobile/components/SettingsModal.tsx b/mobile/components/SettingsModal.tsx index 7ffa07c..0bd616c 100644 --- a/mobile/components/SettingsModal.tsx +++ b/mobile/components/SettingsModal.tsx @@ -9,7 +9,7 @@ import { Alert, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; -import { StoredFolder, SyncMode } from '../services/safDirectory'; +import { StoredFolder, SyncMode, SyncGlobalMode } from '../services/safDirectory'; type SettingsView = 'menu' | 'folders' | 'sync'; @@ -22,6 +22,10 @@ interface SettingsModalProps { onAddFolder: () => void; onUpdateSyncMode: (folderId: string, mode: SyncMode) => void; onUpdateSyncCellular: (folderId: string, enabled: boolean) => void; + globalSyncMode: SyncGlobalMode; + onSetGlobalSyncMode: (mode: SyncGlobalMode) => void; + globalSyncCellular: boolean; + onSetGlobalSyncCellular: (enabled: boolean) => void; } const SYNC_MODES: { value: SyncMode; label: string; icon: string; color: string }[] = [ @@ -30,6 +34,12 @@ const SYNC_MODES: { value: SyncMode; label: string; icon: string; color: string { value: 'auto', label: 'Auto', icon: 'sync-problem', color: '#43A047' }, ]; +const GLOBAL_MODES: { value: SyncGlobalMode; label: string; description: string; icon: string; color: string }[] = [ + { value: 'off', label: 'Désactivé', description: 'Aucun upload automatique', icon: 'sync-disabled', color: '#999' }, + { value: 'auto', label: 'Automatique', description: 'Tout synchroniser automatiquement', icon: 'sync-problem', color: '#43A047' }, + { value: 'manual', label: 'Par dossier', description: 'Choisir dossier par dossier', icon: 'tune', color: '#1976D2' }, +]; + export function SettingsModal({ visible, onClose, @@ -39,6 +49,10 @@ export function SettingsModal({ onAddFolder, onUpdateSyncMode, onUpdateSyncCellular, + globalSyncMode, + onSetGlobalSyncMode, + globalSyncCellular, + onSetGlobalSyncCellular, }: SettingsModalProps) { const [view, setView] = useState('menu'); @@ -103,6 +117,10 @@ export function SettingsModal({ onBack={() => setView('menu')} onUpdateSyncMode={onUpdateSyncMode} onUpdateSyncCellular={onUpdateSyncCellular} + globalSyncMode={globalSyncMode} + onSetGlobalSyncMode={onSetGlobalSyncMode} + globalSyncCellular={globalSyncCellular} + onSetGlobalSyncCellular={onSetGlobalSyncCellular} /> )} @@ -209,11 +227,19 @@ function SyncView({ onBack, onUpdateSyncMode, onUpdateSyncCellular, + globalSyncMode, + onSetGlobalSyncMode, + globalSyncCellular, + onSetGlobalSyncCellular, }: { folders: StoredFolder[]; onBack: () => void; onUpdateSyncMode: (id: string, mode: SyncMode) => void; onUpdateSyncCellular: (id: string, enabled: boolean) => void; + globalSyncMode: SyncGlobalMode; + onSetGlobalSyncMode: (mode: SyncGlobalMode) => void; + globalSyncCellular: boolean; + onSetGlobalSyncCellular: (enabled: boolean) => void; }) { return ( @@ -225,74 +251,143 @@ function SyncView({ - Configurez comment les fichiers de chaque dossier sont envoyés au serveur. + Choisissez comment vos fichiers sont envoyés au serveur. - {folders.length === 0 ? ( - Aucun dossier configuré - ) : ( - folders.map((folder) => ( - - - - - {folder.name} + + Mode de synchronisation + {GLOBAL_MODES.map((mode) => ( + onSetGlobalSyncMode(mode.value)} + > + + + + {mode.label} + {mode.description} + + + ))} + - - {SYNC_MODES.map((mode) => ( - onUpdateSyncMode(folder.id, mode.value)} - > - - - {mode.label} - - - ))} - - - {folder.syncMode !== 'none' && ( - - - Réseau cellulaire - onUpdateSyncCellular(folder.id, !folder.syncCellular)} - > - - - - )} + {globalSyncMode === 'auto' && ( + + + + Autoriser le réseau cellulaire + onSetGlobalSyncCellular(!globalSyncCellular)} + > + + - )) + + {globalSyncCellular + ? 'Upload via WiFi et données mobiles' + : 'Upload uniquement en WiFi'} + + + )} + + {globalSyncMode === 'manual' && ( + + Configuration par dossier + {folders.length === 0 ? ( + Aucun dossier configuré + ) : ( + folders.map((folder) => ( + + + + + {folder.name} + + + + + {SYNC_MODES.map((mode) => ( + onUpdateSyncMode(folder.id, mode.value)} + > + + + {mode.label} + + + ))} + + + {folder.syncMode !== 'none' && ( + + + Réseau cellulaire + onUpdateSyncCellular(folder.id, !folder.syncCellular)} + > + + + + )} + + )) + )} + )} ); @@ -420,6 +515,72 @@ const styles = StyleSheet.create({ marginBottom: 16, lineHeight: 18, }, + globalModeCard: { + backgroundColor: '#fafafa', + borderRadius: 10, + padding: 12, + marginBottom: 12, + }, + sectionLabel: { + fontSize: 13, + fontWeight: '600', + color: '#666', + marginBottom: 10, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + globalModeRow: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 10, + paddingHorizontal: 8, + gap: 10, + borderRadius: 8, + marginBottom: 4, + }, + globalModeRowActive: { + backgroundColor: '#fff', + borderWidth: 1, + borderColor: '#e0e0e0', + }, + globalModeTextContainer: { + flex: 1, + }, + globalModeLabel: { + fontSize: 15, + fontWeight: '600', + color: '#333', + }, + globalModeDescription: { + fontSize: 12, + color: '#999', + marginTop: 2, + }, + cellularCard: { + backgroundColor: '#fafafa', + borderRadius: 10, + padding: 12, + marginBottom: 12, + }, + cellularRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + cellularLabel: { + flex: 1, + fontSize: 14, + color: '#333', + }, + cellularHint: { + fontSize: 12, + color: '#999', + marginTop: 6, + marginLeft: 28, + }, + perFolderSection: { + marginTop: 4, + }, syncFolderCard: { backgroundColor: '#fafafa', borderRadius: 10, @@ -464,19 +625,6 @@ const styles = StyleSheet.create({ fontWeight: '600', color: '#999', }, - cellularRow: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - paddingTop: 6, - borderTopWidth: 1, - borderTopColor: '#e8e8e8', - }, - cellularLabel: { - flex: 1, - fontSize: 13, - color: '#666', - }, toggleBtn: { width: 44, height: 24, diff --git a/mobile/hooks/useAutoSync.ts b/mobile/hooks/useAutoSync.ts index ca2eace..5812ee2 100644 --- a/mobile/hooks/useAutoSync.ts +++ b/mobile/hooks/useAutoSync.ts @@ -9,13 +9,6 @@ import { API_BASE_URL, ENDPOINTS } from '../constants/api'; import { ApiError } from '../types'; import { setIsSyncing } from './useSyncQueue'; -function canSyncFolder(folder: StoredFolder, netInfo: NetInfoState): boolean { - if (folder.syncMode !== 'auto') return false; - if (!netInfo.isConnected) return false; - if (!folder.syncCellular && netInfo.type !== 'wifi') return false; - return true; -} - interface UploadResult { name: string; id: string; @@ -53,38 +46,52 @@ async function uploadFile(file: { uri: string; type: string; name: string }): Pr return item as UploadResult; } +function canSyncBasedOnNetwork(netInfo: NetInfoState, cellularAllowed: boolean): boolean { + if (!netInfo.isConnected) return false; + if (!cellularAllowed && netInfo.type !== 'wifi') return false; + return true; +} + export function useAutoSync() { const queryClient = useQueryClient(); const isRunning = useRef(false); const checkAndSync = useCallback(async () => { if (isRunning.current) return; + + const globalMode = safDirectory.getGlobalSyncMode(); + if (globalMode === 'off') return; + isRunning.current = true; try { - const folders = safDirectory.getAll(); - const autoFolders = folders.filter((f) => f.syncMode === 'auto'); - if (autoFolders.length === 0) return; - + const globalCellular = safDirectory.getGlobalSyncCellular(); const netInfo = await NetInfo.fetch(); - const eligible = autoFolders.filter((f) => canSyncFolder(f, netInfo)); - if (eligible.length === 0) return; - const eligibleIds = new Set(eligible.map((f) => f.id)); + if (!canSyncBasedOnNetwork(netInfo, globalCellular)) return; + const registry = localFileRegistry.getAll(); - - const pendingFiles = registry.filter( - (entry) => - !entry.backendFileId && - entry.syncStatus === 'local' && - entry.folderId && - eligibleIds.has(entry.folderId) && - entry.localUri + let pendingFiles = registry.filter( + (entry) => !entry.backendFileId && entry.syncStatus === 'local' && entry.localUri ); + if (globalMode === 'auto') { + // Mode auto global : tous les fichiers locaux sans backendFileId + // (pas de filtre par dossier) + } else { + // Mode manuel : uniquement les fichiers des dossiers en mode auto + const allFolders = safDirectory.getAll(); + const autoFolderIds = new Set( + allFolders.filter((f) => f.syncMode === 'auto').map((f) => f.id) + ); + pendingFiles = pendingFiles.filter( + (entry) => entry.folderId && autoFolderIds.has(entry.folderId) + ); + } + if (pendingFiles.length === 0) return; - console.log(`[useAutoSync] upload de ${pendingFiles.length} fichier(s)`); + console.log(`[useAutoSync] mode=${globalMode}, upload de ${pendingFiles.length} fichier(s)`); setIsSyncing(true); for (const entry of pendingFiles) { diff --git a/mobile/hooks/useLocalFiles.ts b/mobile/hooks/useLocalFiles.ts index cb892af..27b482e 100644 --- a/mobile/hooks/useLocalFiles.ts +++ b/mobile/hooks/useLocalFiles.ts @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useMemo, useEffect, useRef } from 'react'; import { useDeviceFiles } from './useDeviceFiles'; import { localFileRegistry } from '../services/localFileRegistry'; import { LocalFileEntry } from '../types'; @@ -8,6 +8,23 @@ export function useLocalFiles() { const registryEntries = useMemo(() => localFileRegistry.getAll(), []); + useEffect(() => { + for (const df of deviceFiles) { + if (localFileRegistry.get(df.id)) continue; + const entry: LocalFileEntry = { + id: df.id, + localUri: df.uri, + name: df.name, + mimeType: df.mimeType, + size: df.size, + syncStatus: 'local', + createdAt: df.createdAt, + folderId: df.folderId, + }; + localFileRegistry.register(entry); + } + }, [deviceFiles]); + const localFiles = useMemo(() => { const merged = new Map(); diff --git a/mobile/hooks/useSyncQueue.ts b/mobile/hooks/useSyncQueue.ts index d45ced9..1211486 100644 --- a/mobile/hooks/useSyncQueue.ts +++ b/mobile/hooks/useSyncQueue.ts @@ -20,12 +20,9 @@ export function useSyncQueue() { const { folders } = useDeviceFiles(); const refresh = useCallback(() => { - const allFolders = safDirectory.getAll(); - const syncFolderIds = new Set( - allFolders.filter((f) => f.syncMode !== 'none').map((f) => f.id) - ); + const globalMode = safDirectory.getGlobalSyncMode(); - if (syncFolderIds.size === 0) { + if (globalMode === 'off') { setPendingCount(0); setIsSyncingState(getIsSyncing()); return; @@ -33,11 +30,21 @@ export function useSyncQueue() { const registry = localFileRegistry.getAll(); let count = 0; + for (const entry of registry) { if (entry.backendFileId) continue; if (entry.syncStatus !== 'local') continue; - if (entry.folderId && !syncFolderIds.has(entry.folderId)) continue; - count++; + + if (globalMode === 'auto') { + count++; + } else { + // mode manuel : uniquement les fichiers des dossiers en mode auto + if (!entry.folderId) continue; + const folder = safDirectory.getAll().find((f) => f.id === entry.folderId); + if (folder && folder.syncMode === 'auto') { + count++; + } + } } setPendingCount(count); diff --git a/mobile/services/safDirectory.ts b/mobile/services/safDirectory.ts index 86eb8c0..87fcb0c 100644 --- a/mobile/services/safDirectory.ts +++ b/mobile/services/safDirectory.ts @@ -1,6 +1,7 @@ import { createMMKV } from 'react-native-mmkv'; export type SyncMode = 'none' | 'manual' | 'auto'; +export type SyncGlobalMode = 'off' | 'auto' | 'manual'; export type StoredFolder = { id: string; @@ -14,6 +15,8 @@ export type StoredFolder = { const storage = createMMKV({ id: 'vaultdrop-saf' }); const FOLDERS_KEY = 'saf_folders'; +const SYNC_GLOBAL_MODE_KEY = 'sync_global_mode'; +const SYNC_GLOBAL_CELLULAR_KEY = 'sync_global_cellular'; function generateId(): string { return `folder_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; @@ -88,6 +91,24 @@ export const safDirectory = { saveAll(folders); }, + getGlobalSyncMode(): SyncGlobalMode { + const raw = storage.getString(SYNC_GLOBAL_MODE_KEY); + if (raw === 'auto' || raw === 'manual') return raw; + return 'off'; + }, + + setGlobalSyncMode(mode: SyncGlobalMode) { + storage.set(SYNC_GLOBAL_MODE_KEY, mode); + }, + + getGlobalSyncCellular(): boolean { + return storage.getString(SYNC_GLOBAL_CELLULAR_KEY) === 'true'; + }, + + setGlobalSyncCellular(enabled: boolean) { + storage.set(SYNC_GLOBAL_CELLULAR_KEY, enabled ? 'true' : 'false'); + }, + clear() { storage.remove(FOLDERS_KEY); },