sync files
This commit is contained in:
+17
-1
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user