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 { 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<SyncGlobalMode>(() => 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}
/>
</KeyboardAvoidingView>
);
+163 -15
View File
@@ -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<SettingsView>('menu');
@@ -103,6 +117,10 @@ export function SettingsModal({
onBack={() => setView('menu')}
onUpdateSyncMode={onUpdateSyncMode}
onUpdateSyncCellular={onUpdateSyncCellular}
globalSyncMode={globalSyncMode}
onSetGlobalSyncMode={onSetGlobalSyncMode}
globalSyncCellular={globalSyncCellular}
onSetGlobalSyncCellular={onSetGlobalSyncCellular}
/>
)}
</TouchableOpacity>
@@ -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 (
<ScrollView style={styles.viewContainer} showsVerticalScrollIndicator={false}>
@@ -225,9 +251,76 @@ function SyncView({
</View>
<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>
<View style={styles.globalModeCard}>
<Text style={styles.sectionLabel}>Mode de synchronisation</Text>
{GLOBAL_MODES.map((mode) => (
<TouchableOpacity
key={mode.value}
style={[
styles.globalModeRow,
globalSyncMode === mode.value && styles.globalModeRowActive,
]}
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 style={styles.globalModeDescription}>{mode.description}</Text>
</View>
<MaterialIcons
name={mode.icon as any}
size={20}
color={globalSyncMode === mode.value ? mode.color : '#ccc'}
/>
</TouchableOpacity>
))}
</View>
{globalSyncMode === 'auto' && (
<View style={styles.cellularCard}>
<View style={styles.cellularRow}>
<MaterialIcons name="cell-tower" size={20} color="#666" />
<Text style={styles.cellularLabel}>Autoriser le réseau cellulaire</Text>
<TouchableOpacity
style={[
styles.toggleBtn,
globalSyncCellular && styles.toggleBtnActive,
]}
onPress={() => onSetGlobalSyncCellular(!globalSyncCellular)}
>
<View
style={[
styles.toggleDot,
globalSyncCellular && styles.toggleDotActive,
]}
/>
</TouchableOpacity>
</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>
) : (
@@ -294,6 +387,8 @@ function SyncView({
</View>
))
)}
</View>
)}
</ScrollView>
);
}
@@ -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,
+30 -23
View File
@@ -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) {
+18 -1
View File
@@ -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<string, LocalFileEntry>();
+13 -6
View File
@@ -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;
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);
+21
View File
@@ -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);
},