sync files

This commit is contained in:
m
2026-07-27 22:13:13 +02:00
parent cb9cea6ff3
commit c5a959a27b
6 changed files with 324 additions and 108 deletions
+17 -1
View File
@@ -14,7 +14,7 @@ import { SettingsModal } from '../components/SettingsModal';
import { SyncStatusIcon } from '../components/SyncStatusIcon'; import { SyncStatusIcon } from '../components/SyncStatusIcon';
import { useSyncQueue } from '../hooks/useSyncQueue'; import { useSyncQueue } from '../hooks/useSyncQueue';
import { useAutoSync } from '../hooks/useAutoSync'; 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 NUM_COLUMNS = 3;
const SCREEN_WIDTH = Dimensions.get('window').width; const SCREEN_WIDTH = Dimensions.get('window').width;
@@ -155,6 +155,8 @@ export function HomeScreen() {
const { data: foldersData } = useFolders(); const { data: foldersData } = useFolders();
const [moveModalVisible, setMoveModalVisible] = useState(false); const [moveModalVisible, setMoveModalVisible] = useState(false);
const [settingsModalVisible, setSettingsModalVisible] = useState(false); const [settingsModalVisible, setSettingsModalVisible] = useState(false);
const [globalSyncMode, setGlobalSyncMode] = useState<SyncGlobalMode>(() => safDirectory.getGlobalSyncMode());
const [globalSyncCellular, setGlobalSyncCellular] = useState(() => safDirectory.getGlobalSyncCellular());
const { pendingCount, isSyncing } = useSyncQueue(); const { pendingCount, isSyncing } = useSyncQueue();
useAutoSync(); useAutoSync();
@@ -333,6 +335,16 @@ export function HomeScreen() {
refreshFolders(); refreshFolders();
}, [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) => { const handleItemPress = useCallback((file: UnifiedFileItem) => {
if (selectionMode) { if (selectionMode) {
toggleSelection(file.id); toggleSelection(file.id);
@@ -617,6 +629,10 @@ export function HomeScreen() {
onAddFolder={handleAddFolder} onAddFolder={handleAddFolder}
onUpdateSyncMode={handleUpdateSyncMode} onUpdateSyncMode={handleUpdateSyncMode}
onUpdateSyncCellular={handleUpdateSyncCellular} onUpdateSyncCellular={handleUpdateSyncCellular}
globalSyncMode={globalSyncMode}
onSetGlobalSyncMode={handleSetGlobalSyncMode}
globalSyncCellular={globalSyncCellular}
onSetGlobalSyncCellular={handleSetGlobalSyncCellular}
/> />
</KeyboardAvoidingView> </KeyboardAvoidingView>
); );
+224 -76
View File
@@ -9,7 +9,7 @@ import {
Alert, Alert,
} from 'react-native'; } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { StoredFolder, SyncMode } from '../services/safDirectory'; import { StoredFolder, SyncMode, SyncGlobalMode } from '../services/safDirectory';
type SettingsView = 'menu' | 'folders' | 'sync'; type SettingsView = 'menu' | 'folders' | 'sync';
@@ -22,6 +22,10 @@ interface SettingsModalProps {
onAddFolder: () => void; onAddFolder: () => void;
onUpdateSyncMode: (folderId: string, mode: SyncMode) => void; onUpdateSyncMode: (folderId: string, mode: SyncMode) => void;
onUpdateSyncCellular: (folderId: string, enabled: boolean) => 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 }[] = [ 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' }, { 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({ export function SettingsModal({
visible, visible,
onClose, onClose,
@@ -39,6 +49,10 @@ export function SettingsModal({
onAddFolder, onAddFolder,
onUpdateSyncMode, onUpdateSyncMode,
onUpdateSyncCellular, onUpdateSyncCellular,
globalSyncMode,
onSetGlobalSyncMode,
globalSyncCellular,
onSetGlobalSyncCellular,
}: SettingsModalProps) { }: SettingsModalProps) {
const [view, setView] = useState<SettingsView>('menu'); const [view, setView] = useState<SettingsView>('menu');
@@ -103,6 +117,10 @@ export function SettingsModal({
onBack={() => setView('menu')} onBack={() => setView('menu')}
onUpdateSyncMode={onUpdateSyncMode} onUpdateSyncMode={onUpdateSyncMode}
onUpdateSyncCellular={onUpdateSyncCellular} onUpdateSyncCellular={onUpdateSyncCellular}
globalSyncMode={globalSyncMode}
onSetGlobalSyncMode={onSetGlobalSyncMode}
globalSyncCellular={globalSyncCellular}
onSetGlobalSyncCellular={onSetGlobalSyncCellular}
/> />
)} )}
</TouchableOpacity> </TouchableOpacity>
@@ -209,11 +227,19 @@ function SyncView({
onBack, onBack,
onUpdateSyncMode, onUpdateSyncMode,
onUpdateSyncCellular, onUpdateSyncCellular,
globalSyncMode,
onSetGlobalSyncMode,
globalSyncCellular,
onSetGlobalSyncCellular,
}: { }: {
folders: StoredFolder[]; folders: StoredFolder[];
onBack: () => void; onBack: () => void;
onUpdateSyncMode: (id: string, mode: SyncMode) => void; onUpdateSyncMode: (id: string, mode: SyncMode) => void;
onUpdateSyncCellular: (id: string, enabled: boolean) => void; onUpdateSyncCellular: (id: string, enabled: boolean) => void;
globalSyncMode: SyncGlobalMode;
onSetGlobalSyncMode: (mode: SyncGlobalMode) => void;
globalSyncCellular: boolean;
onSetGlobalSyncCellular: (enabled: boolean) => void;
}) { }) {
return ( return (
<ScrollView style={styles.viewContainer} showsVerticalScrollIndicator={false}> <ScrollView style={styles.viewContainer} showsVerticalScrollIndicator={false}>
@@ -225,74 +251,143 @@ function SyncView({
</View> </View>
<Text style={styles.syncInfo}> <Text style={styles.syncInfo}>
Configurez comment les fichiers de chaque dossier sont envoyés au serveur. Choisissez comment vos fichiers sont envoyés au serveur.
</Text> </Text>
{folders.length === 0 ? ( <View style={styles.globalModeCard}>
<Text style={styles.emptyText}>Aucun dossier configuré</Text> <Text style={styles.sectionLabel}>Mode de synchronisation</Text>
) : ( {GLOBAL_MODES.map((mode) => (
folders.map((folder) => ( <TouchableOpacity
<View key={folder.id} style={styles.syncFolderCard}> key={mode.value}
<View style={styles.syncFolderHeader}> style={[
<MaterialIcons name="folder" size={18} color="#F57C00" /> styles.globalModeRow,
<Text style={styles.syncFolderName} numberOfLines={1}> globalSyncMode === mode.value && styles.globalModeRowActive,
{folder.name} ]}
onPress={() => onSetGlobalSyncMode(mode.value)}
>
<MaterialIcons
name={globalSyncMode === mode.value ? 'radio-button-checked' : 'radio-button-unchecked'}
size={20}
color={globalSyncMode === mode.value ? mode.color : '#999'}
/>
<View style={styles.globalModeTextContainer}>
<Text
style={[
styles.globalModeLabel,
globalSyncMode === mode.value && { color: mode.color },
]}
>
{mode.label}
</Text> </Text>
<Text style={styles.globalModeDescription}>{mode.description}</Text>
</View> </View>
<MaterialIcons
name={mode.icon as any}
size={20}
color={globalSyncMode === mode.value ? mode.color : '#ccc'}
/>
</TouchableOpacity>
))}
</View>
<View style={styles.radioGroup}> {globalSyncMode === 'auto' && (
{SYNC_MODES.map((mode) => ( <View style={styles.cellularCard}>
<TouchableOpacity <View style={styles.cellularRow}>
key={mode.value} <MaterialIcons name="cell-tower" size={20} color="#666" />
style={[ <Text style={styles.cellularLabel}>Autoriser le réseau cellulaire</Text>
styles.radioBtn, <TouchableOpacity
folder.syncMode === mode.value && styles.radioBtnActive, style={[
]} styles.toggleBtn,
onPress={() => onUpdateSyncMode(folder.id, mode.value)} globalSyncCellular && styles.toggleBtnActive,
> ]}
<MaterialIcons onPress={() => onSetGlobalSyncCellular(!globalSyncCellular)}
name={ >
folder.syncMode === mode.value <View
? 'radio-button-checked' style={[
: 'radio-button-unchecked' styles.toggleDot,
} globalSyncCellular && styles.toggleDotActive,
size={18} ]}
color={folder.syncMode === mode.value ? mode.color : '#999'} />
/> </TouchableOpacity>
<Text
style={[
styles.radioLabel,
folder.syncMode === mode.value && { color: mode.color },
]}
>
{mode.label}
</Text>
</TouchableOpacity>
))}
</View>
{folder.syncMode !== 'none' && (
<View style={styles.cellularRow}>
<MaterialIcons name="cell-tower" size={18} color="#666" />
<Text style={styles.cellularLabel}>Réseau cellulaire</Text>
<TouchableOpacity
style={[
styles.toggleBtn,
folder.syncCellular && styles.toggleBtnActive,
]}
onPress={() => onUpdateSyncCellular(folder.id, !folder.syncCellular)}
>
<View
style={[
styles.toggleDot,
folder.syncCellular && styles.toggleDotActive,
]}
/>
</TouchableOpacity>
</View>
)}
</View> </View>
)) <Text style={styles.cellularHint}>
{globalSyncCellular
? 'Upload via WiFi et données mobiles'
: 'Upload uniquement en WiFi'}
</Text>
</View>
)}
{globalSyncMode === 'manual' && (
<View style={styles.perFolderSection}>
<Text style={styles.sectionLabel}>Configuration par dossier</Text>
{folders.length === 0 ? (
<Text style={styles.emptyText}>Aucun dossier configuré</Text>
) : (
folders.map((folder) => (
<View key={folder.id} style={styles.syncFolderCard}>
<View style={styles.syncFolderHeader}>
<MaterialIcons name="folder" size={18} color="#F57C00" />
<Text style={styles.syncFolderName} numberOfLines={1}>
{folder.name}
</Text>
</View>
<View style={styles.radioGroup}>
{SYNC_MODES.map((mode) => (
<TouchableOpacity
key={mode.value}
style={[
styles.radioBtn,
folder.syncMode === mode.value && styles.radioBtnActive,
]}
onPress={() => onUpdateSyncMode(folder.id, mode.value)}
>
<MaterialIcons
name={
folder.syncMode === mode.value
? 'radio-button-checked'
: 'radio-button-unchecked'
}
size={18}
color={folder.syncMode === mode.value ? mode.color : '#999'}
/>
<Text
style={[
styles.radioLabel,
folder.syncMode === mode.value && { color: mode.color },
]}
>
{mode.label}
</Text>
</TouchableOpacity>
))}
</View>
{folder.syncMode !== 'none' && (
<View style={styles.cellularRow}>
<MaterialIcons name="cell-tower" size={18} color="#666" />
<Text style={styles.cellularLabel}>Réseau cellulaire</Text>
<TouchableOpacity
style={[
styles.toggleBtn,
folder.syncCellular && styles.toggleBtnActive,
]}
onPress={() => onUpdateSyncCellular(folder.id, !folder.syncCellular)}
>
<View
style={[
styles.toggleDot,
folder.syncCellular && styles.toggleDotActive,
]}
/>
</TouchableOpacity>
</View>
)}
</View>
))
)}
</View>
)} )}
</ScrollView> </ScrollView>
); );
@@ -420,6 +515,72 @@ const styles = StyleSheet.create({
marginBottom: 16, marginBottom: 16,
lineHeight: 18, 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: { syncFolderCard: {
backgroundColor: '#fafafa', backgroundColor: '#fafafa',
borderRadius: 10, borderRadius: 10,
@@ -464,19 +625,6 @@ const styles = StyleSheet.create({
fontWeight: '600', fontWeight: '600',
color: '#999', color: '#999',
}, },
cellularRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingTop: 6,
borderTopWidth: 1,
borderTopColor: '#e8e8e8',
},
cellularLabel: {
flex: 1,
fontSize: 13,
color: '#666',
},
toggleBtn: { toggleBtn: {
width: 44, width: 44,
height: 24, height: 24,
+30 -23
View File
@@ -9,13 +9,6 @@ import { API_BASE_URL, ENDPOINTS } from '../constants/api';
import { ApiError } from '../types'; import { ApiError } from '../types';
import { setIsSyncing } from './useSyncQueue'; 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 { interface UploadResult {
name: string; name: string;
id: string; id: string;
@@ -53,38 +46,52 @@ async function uploadFile(file: { uri: string; type: string; name: string }): Pr
return item as UploadResult; 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() { export function useAutoSync() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isRunning = useRef(false); const isRunning = useRef(false);
const checkAndSync = useCallback(async () => { const checkAndSync = useCallback(async () => {
if (isRunning.current) return; if (isRunning.current) return;
const globalMode = safDirectory.getGlobalSyncMode();
if (globalMode === 'off') return;
isRunning.current = true; isRunning.current = true;
try { try {
const folders = safDirectory.getAll(); const globalCellular = safDirectory.getGlobalSyncCellular();
const autoFolders = folders.filter((f) => f.syncMode === 'auto');
if (autoFolders.length === 0) return;
const netInfo = await NetInfo.fetch(); 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 registry = localFileRegistry.getAll();
let pendingFiles = registry.filter(
const pendingFiles = registry.filter( (entry) => !entry.backendFileId && entry.syncStatus === 'local' && entry.localUri
(entry) =>
!entry.backendFileId &&
entry.syncStatus === 'local' &&
entry.folderId &&
eligibleIds.has(entry.folderId) &&
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; 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); setIsSyncing(true);
for (const entry of pendingFiles) { for (const entry of pendingFiles) {
+18 -1
View File
@@ -1,4 +1,4 @@
import { useMemo } from 'react'; import { useMemo, useEffect, useRef } from 'react';
import { useDeviceFiles } from './useDeviceFiles'; import { useDeviceFiles } from './useDeviceFiles';
import { localFileRegistry } from '../services/localFileRegistry'; import { localFileRegistry } from '../services/localFileRegistry';
import { LocalFileEntry } from '../types'; import { LocalFileEntry } from '../types';
@@ -8,6 +8,23 @@ export function useLocalFiles() {
const registryEntries = useMemo(() => localFileRegistry.getAll(), []); 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 localFiles = useMemo(() => {
const merged = new Map<string, LocalFileEntry>(); const merged = new Map<string, LocalFileEntry>();
+14 -7
View File
@@ -20,12 +20,9 @@ export function useSyncQueue() {
const { folders } = useDeviceFiles(); const { folders } = useDeviceFiles();
const refresh = useCallback(() => { const refresh = useCallback(() => {
const allFolders = safDirectory.getAll(); const globalMode = safDirectory.getGlobalSyncMode();
const syncFolderIds = new Set(
allFolders.filter((f) => f.syncMode !== 'none').map((f) => f.id)
);
if (syncFolderIds.size === 0) { if (globalMode === 'off') {
setPendingCount(0); setPendingCount(0);
setIsSyncingState(getIsSyncing()); setIsSyncingState(getIsSyncing());
return; return;
@@ -33,11 +30,21 @@ export function useSyncQueue() {
const registry = localFileRegistry.getAll(); const registry = localFileRegistry.getAll();
let count = 0; let count = 0;
for (const entry of registry) { for (const entry of registry) {
if (entry.backendFileId) continue; if (entry.backendFileId) continue;
if (entry.syncStatus !== 'local') 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); setPendingCount(count);
+21
View File
@@ -1,6 +1,7 @@
import { createMMKV } from 'react-native-mmkv'; import { createMMKV } from 'react-native-mmkv';
export type SyncMode = 'none' | 'manual' | 'auto'; export type SyncMode = 'none' | 'manual' | 'auto';
export type SyncGlobalMode = 'off' | 'auto' | 'manual';
export type StoredFolder = { export type StoredFolder = {
id: string; id: string;
@@ -14,6 +15,8 @@ export type StoredFolder = {
const storage = createMMKV({ id: 'vaultdrop-saf' }); const storage = createMMKV({ id: 'vaultdrop-saf' });
const FOLDERS_KEY = 'saf_folders'; const FOLDERS_KEY = 'saf_folders';
const SYNC_GLOBAL_MODE_KEY = 'sync_global_mode';
const SYNC_GLOBAL_CELLULAR_KEY = 'sync_global_cellular';
function generateId(): string { function generateId(): string {
return `folder_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; return `folder_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
@@ -88,6 +91,24 @@ export const safDirectory = {
saveAll(folders); 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() { clear() {
storage.remove(FOLDERS_KEY); storage.remove(FOLDERS_KEY);
}, },