sync files
This commit is contained in:
@@ -18,6 +18,7 @@ import { FileDetailScreen } from './app/file-detail';
|
||||
import { FileEditScreen } from './app/file-edit';
|
||||
import { FolderScreen } from './app/folder';
|
||||
import { OnboardingScreen } from './app/onboarding';
|
||||
import { SyncDetailScreen } from './app/sync-detail';
|
||||
import { onboardingStorage } from './services/onboardingStorage';
|
||||
|
||||
const Stack = createNativeStackNavigator();
|
||||
@@ -66,6 +67,7 @@ function AppNavigator() {
|
||||
/>
|
||||
<Stack.Screen name="FileEdit" component={FileEditScreen} options={{ title: 'Édition' }} />
|
||||
<Stack.Screen name="Folder" component={FolderScreen} options={{ title: 'Dossier' }} />
|
||||
<Stack.Screen name="SyncDetail" component={SyncDetailScreen} options={{ title: 'Synchronisation' }} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
+38
-107
@@ -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 }}>
|
||||
<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'}
|
||||
<SettingsModal
|
||||
visible={settingsModalVisible}
|
||||
onClose={() => setSettingsModalVisible(false)}
|
||||
folders={folders}
|
||||
onToggleVisibility={handleToggleFolderVisibility}
|
||||
onRemoveFolder={handleRemoveFolder}
|
||||
onAddFolder={handleAddFolder}
|
||||
onUpdateSyncMode={handleUpdateSyncMode}
|
||||
onUpdateSyncCellular={handleUpdateSyncCellular}
|
||||
/>
|
||||
</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>
|
||||
</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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,500 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Modal,
|
||||
ScrollView,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { StoredFolder, SyncMode } from '../services/safDirectory';
|
||||
|
||||
type SettingsView = 'menu' | 'folders' | 'sync';
|
||||
|
||||
interface SettingsModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
folders: StoredFolder[];
|
||||
onToggleVisibility: (folderId: string) => void;
|
||||
onRemoveFolder: (folderId: string) => void;
|
||||
onAddFolder: () => void;
|
||||
onUpdateSyncMode: (folderId: string, mode: SyncMode) => void;
|
||||
onUpdateSyncCellular: (folderId: string, enabled: boolean) => void;
|
||||
}
|
||||
|
||||
const SYNC_MODES: { value: SyncMode; label: string; icon: string; color: string }[] = [
|
||||
{ value: 'none', label: 'Aucun', icon: 'sync-disabled', color: '#999' },
|
||||
{ value: 'manual', label: 'Manuel', icon: 'sync', color: '#1976D2' },
|
||||
{ value: 'auto', label: 'Auto', icon: 'sync-problem', color: '#43A047' },
|
||||
];
|
||||
|
||||
export function SettingsModal({
|
||||
visible,
|
||||
onClose,
|
||||
folders,
|
||||
onToggleVisibility,
|
||||
onRemoveFolder,
|
||||
onAddFolder,
|
||||
onUpdateSyncMode,
|
||||
onUpdateSyncCellular,
|
||||
}: SettingsModalProps) {
|
||||
const [view, setView] = useState<SettingsView>('menu');
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setView('menu');
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
const handleRemoveFolder = useCallback(
|
||||
(folder: StoredFolder) => {
|
||||
Alert.alert(
|
||||
'Supprimer le dossier',
|
||||
'Le dossier sera retiré de la liste. Les fichiers resteront sur votre appareil.',
|
||||
[
|
||||
{ text: 'Annuler', style: 'cancel' },
|
||||
{
|
||||
text: 'Supprimer',
|
||||
style: 'destructive',
|
||||
onPress: () => onRemoveFolder(folder.id),
|
||||
},
|
||||
]
|
||||
);
|
||||
},
|
||||
[onRemoveFolder]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={handleClose}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.overlay}
|
||||
activeOpacity={1}
|
||||
onPress={handleClose}
|
||||
>
|
||||
<TouchableOpacity activeOpacity={1} style={styles.container} onPress={() => {}}>
|
||||
<View style={styles.handle} />
|
||||
|
||||
{view === 'menu' && (
|
||||
<MenuView
|
||||
onSelect={(v) => setView(v)}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{view === 'folders' && (
|
||||
<FoldersView
|
||||
folders={folders}
|
||||
onBack={() => setView('menu')}
|
||||
onToggleVisibility={onToggleVisibility}
|
||||
onRemoveFolder={handleRemoveFolder}
|
||||
onAddFolder={onAddFolder}
|
||||
/>
|
||||
)}
|
||||
|
||||
{view === 'sync' && (
|
||||
<SyncView
|
||||
folders={folders}
|
||||
onBack={() => setView('menu')}
|
||||
onUpdateSyncMode={onUpdateSyncMode}
|
||||
onUpdateSyncCellular={onUpdateSyncCellular}
|
||||
/>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuView({ onSelect, onClose }: { onSelect: (v: SettingsView) => void; onClose: () => void }) {
|
||||
return (
|
||||
<View>
|
||||
<Text style={styles.title}>Paramètres</Text>
|
||||
|
||||
<TouchableOpacity style={styles.menuItem} onPress={() => onSelect('folders')}>
|
||||
<View style={[styles.menuIcon, { backgroundColor: '#FFF3E0' }]}>
|
||||
<MaterialIcons name="folder" size={22} color="#F57C00" />
|
||||
</View>
|
||||
<View style={styles.menuTextContainer}>
|
||||
<Text style={styles.menuLabel}>Dossiers</Text>
|
||||
<Text style={styles.menuDescription}>Gérer les dossiers affichés</Text>
|
||||
</View>
|
||||
<MaterialIcons name="chevron-right" size={24} color="#ccc" />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.menuItem} onPress={() => onSelect('sync')}>
|
||||
<View style={[styles.menuIcon, { backgroundColor: '#E3F2FD' }]}>
|
||||
<MaterialIcons name="cloud-sync" size={22} color="#1976D2" />
|
||||
</View>
|
||||
<View style={styles.menuTextContainer}>
|
||||
<Text style={styles.menuLabel}>Synchronisation</Text>
|
||||
<Text style={styles.menuDescription}>Configurer l'upload automatique</Text>
|
||||
</View>
|
||||
<MaterialIcons name="chevron-right" size={24} color="#ccc" />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.closeBtn} onPress={onClose}>
|
||||
<Text style={styles.closeBtnText}>Fermer</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function FoldersView({
|
||||
folders,
|
||||
onBack,
|
||||
onToggleVisibility,
|
||||
onRemoveFolder,
|
||||
onAddFolder,
|
||||
}: {
|
||||
folders: StoredFolder[];
|
||||
onBack: () => void;
|
||||
onToggleVisibility: (id: string) => void;
|
||||
onRemoveFolder: (folder: StoredFolder) => void;
|
||||
onAddFolder: () => void;
|
||||
}) {
|
||||
return (
|
||||
<ScrollView style={styles.viewContainer} showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.viewHeader}>
|
||||
<TouchableOpacity onPress={onBack} style={styles.backBtn}>
|
||||
<MaterialIcons name="arrow-back" size={22} color="#333" />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.title}>Dossiers</Text>
|
||||
</View>
|
||||
|
||||
{folders.length === 0 ? (
|
||||
<Text style={styles.emptyText}>Aucun dossier configuré</Text>
|
||||
) : (
|
||||
folders.map((folder) => (
|
||||
<View key={folder.id} style={styles.folderRow}>
|
||||
<MaterialIcons name="folder" size={20} color="#F57C00" />
|
||||
<Text style={styles.folderName} numberOfLines={1}>
|
||||
{folder.name}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => onToggleVisibility(folder.id)}
|
||||
style={styles.actionBtn}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={folder.visible ? 'visibility' : 'visibility-off'}
|
||||
size={20}
|
||||
color={folder.visible ? '#1976D2' : '#999'}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => onRemoveFolder(folder)}
|
||||
style={styles.actionBtn}
|
||||
>
|
||||
<MaterialIcons name="delete-outline" size={20} color="#E53935" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
|
||||
<TouchableOpacity style={styles.addBtn} onPress={onAddFolder}>
|
||||
<MaterialIcons name="add" size={20} color="#fff" />
|
||||
<Text style={styles.addBtnText}>Ajouter un dossier</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
function SyncView({
|
||||
folders,
|
||||
onBack,
|
||||
onUpdateSyncMode,
|
||||
onUpdateSyncCellular,
|
||||
}: {
|
||||
folders: StoredFolder[];
|
||||
onBack: () => void;
|
||||
onUpdateSyncMode: (id: string, mode: SyncMode) => void;
|
||||
onUpdateSyncCellular: (id: string, enabled: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<ScrollView style={styles.viewContainer} showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.viewHeader}>
|
||||
<TouchableOpacity onPress={onBack} style={styles.backBtn}>
|
||||
<MaterialIcons name="arrow-back" size={22} color="#333" />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.title}>Synchronisation</Text>
|
||||
</View>
|
||||
|
||||
<Text style={styles.syncInfo}>
|
||||
Configurez comment les fichiers de chaque dossier sont envoyés au serveur.
|
||||
</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>
|
||||
))
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
container: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 16,
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 20,
|
||||
width: '85%',
|
||||
maxHeight: '80%',
|
||||
},
|
||||
handle: {
|
||||
width: 36,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: '#ddd',
|
||||
alignSelf: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#333',
|
||||
marginBottom: 16,
|
||||
},
|
||||
menuIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
menuItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 14,
|
||||
gap: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#f0f0f0',
|
||||
},
|
||||
menuTextContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
menuLabel: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
},
|
||||
menuDescription: {
|
||||
fontSize: 13,
|
||||
color: '#999',
|
||||
marginTop: 2,
|
||||
},
|
||||
closeBtn: {
|
||||
marginTop: 16,
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
closeBtnText: {
|
||||
fontSize: 15,
|
||||
color: '#666',
|
||||
},
|
||||
viewContainer: {
|
||||
maxHeight: 500,
|
||||
},
|
||||
viewHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
marginBottom: 8,
|
||||
},
|
||||
backBtn: {
|
||||
padding: 4,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
color: '#999',
|
||||
textAlign: 'center',
|
||||
marginVertical: 20,
|
||||
},
|
||||
folderRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10,
|
||||
gap: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#f0f0f0',
|
||||
},
|
||||
folderName: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
color: '#333',
|
||||
},
|
||||
actionBtn: {
|
||||
padding: 6,
|
||||
},
|
||||
addBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#F57C00',
|
||||
borderRadius: 10,
|
||||
paddingVertical: 12,
|
||||
marginTop: 16,
|
||||
gap: 8,
|
||||
},
|
||||
addBtnText: {
|
||||
fontSize: 15,
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
syncInfo: {
|
||||
fontSize: 13,
|
||||
color: '#666',
|
||||
marginBottom: 16,
|
||||
lineHeight: 18,
|
||||
},
|
||||
syncFolderCard: {
|
||||
backgroundColor: '#fafafa',
|
||||
borderRadius: 10,
|
||||
padding: 12,
|
||||
marginBottom: 10,
|
||||
},
|
||||
syncFolderHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 10,
|
||||
},
|
||||
syncFolderName: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
color: '#333',
|
||||
flex: 1,
|
||||
},
|
||||
radioGroup: {
|
||||
flexDirection: 'row',
|
||||
gap: 6,
|
||||
marginBottom: 10,
|
||||
},
|
||||
radioBtn: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 6,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#fff',
|
||||
borderWidth: 1,
|
||||
borderColor: '#e0e0e0',
|
||||
gap: 4,
|
||||
},
|
||||
radioBtnActive: {
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
radioLabel: {
|
||||
fontSize: 12,
|
||||
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,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#ddd',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 2,
|
||||
},
|
||||
toggleBtnActive: {
|
||||
backgroundColor: '#1976D2',
|
||||
},
|
||||
toggleDot: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
toggleDotActive: {
|
||||
alignSelf: 'flex-end',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { TouchableOpacity, Text, StyleSheet, View } from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withTiming,
|
||||
withRepeat,
|
||||
withSequence,
|
||||
Easing,
|
||||
cancelAnimation,
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
interface SyncStatusIconProps {
|
||||
isSyncing: boolean;
|
||||
pendingCount: number;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
export function SyncStatusIcon({ isSyncing, pendingCount, onPress }: SyncStatusIconProps) {
|
||||
const rotation = useSharedValue(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSyncing) {
|
||||
rotation.value = withRepeat(
|
||||
withSequence(
|
||||
withTiming(360, { duration: 1000, easing: Easing.linear }),
|
||||
withTiming(0, { duration: 0 })
|
||||
),
|
||||
-1
|
||||
);
|
||||
} else {
|
||||
cancelAnimation(rotation);
|
||||
rotation.value = withTiming(0, { duration: 200 });
|
||||
}
|
||||
}, [isSyncing, rotation]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ rotate: `${rotation.value}deg` }],
|
||||
}));
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} style={styles.container}>
|
||||
<Animated.View style={animatedStyle}>
|
||||
<MaterialIcons
|
||||
name="sync"
|
||||
size={22}
|
||||
color={isSyncing ? '#1976D2' : pendingCount > 0 ? '#F57C00' : '#666'}
|
||||
/>
|
||||
</Animated.View>
|
||||
{pendingCount > 0 && !isSyncing && (
|
||||
<View style={styles.badge}>
|
||||
<Text style={styles.badgeText}>
|
||||
{pendingCount > 99 ? '99+' : pendingCount}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'relative',
|
||||
marginRight: 4,
|
||||
padding: 8,
|
||||
},
|
||||
badge: {
|
||||
position: 'absolute',
|
||||
top: 2,
|
||||
right: 0,
|
||||
backgroundColor: '#E53935',
|
||||
borderRadius: 8,
|
||||
minWidth: 16,
|
||||
height: 16,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 9,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { File, UploadType } from 'expo-file-system';
|
||||
import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
|
||||
import { safDirectory, StoredFolder } from '../services/safDirectory';
|
||||
import { localFileRegistry } from '../services/localFileRegistry';
|
||||
import { apiClient } from '../api/client';
|
||||
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;
|
||||
}
|
||||
|
||||
async function uploadFile(file: { uri: string; type: string; name: string }): Promise<UploadResult> {
|
||||
const fsFile = new File(file.uri);
|
||||
const headers: Record<string, string> = {};
|
||||
const token = apiClient.getAccessToken();
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
const result = await fsFile.upload(`${API_BASE_URL}${ENDPOINTS.UPLOAD}`, {
|
||||
httpMethod: 'POST',
|
||||
uploadType: UploadType.MULTIPART,
|
||||
fieldName: 'file',
|
||||
mimeType: file.type,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (result.status >= 400) {
|
||||
let message = 'Upload failed';
|
||||
try {
|
||||
const body: ApiError = JSON.parse(result.body);
|
||||
message = body.error?.message || message;
|
||||
} catch {
|
||||
message = result.body || message;
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const body = JSON.parse(result.body);
|
||||
const items = body.data ?? body;
|
||||
const item = Array.isArray(items) ? items[0] : items;
|
||||
return item as UploadResult;
|
||||
}
|
||||
|
||||
export function useAutoSync() {
|
||||
const queryClient = useQueryClient();
|
||||
const isRunning = useRef(false);
|
||||
|
||||
const checkAndSync = useCallback(async () => {
|
||||
if (isRunning.current) return;
|
||||
isRunning.current = true;
|
||||
|
||||
try {
|
||||
const folders = safDirectory.getAll();
|
||||
const autoFolders = folders.filter((f) => f.syncMode === 'auto');
|
||||
if (autoFolders.length === 0) return;
|
||||
|
||||
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));
|
||||
const registry = localFileRegistry.getAll();
|
||||
|
||||
const pendingFiles = registry.filter(
|
||||
(entry) =>
|
||||
!entry.backendFileId &&
|
||||
entry.syncStatus === 'local' &&
|
||||
entry.folderId &&
|
||||
eligibleIds.has(entry.folderId) &&
|
||||
entry.localUri
|
||||
);
|
||||
|
||||
if (pendingFiles.length === 0) return;
|
||||
|
||||
console.log(`[useAutoSync] upload de ${pendingFiles.length} fichier(s)`);
|
||||
setIsSyncing(true);
|
||||
|
||||
for (const entry of pendingFiles) {
|
||||
try {
|
||||
const uploaded = await uploadFile({
|
||||
uri: entry.localUri,
|
||||
type: entry.mimeType,
|
||||
name: entry.name,
|
||||
});
|
||||
|
||||
localFileRegistry.register({
|
||||
...entry,
|
||||
backendFileId: uploaded.id,
|
||||
syncStatus: 'synced',
|
||||
});
|
||||
|
||||
console.log(`[useAutoSync] "${entry.name}" uploadé → id=${uploaded.id}`);
|
||||
} catch (err) {
|
||||
console.error(`[useAutoSync] échec upload "${entry.name}":`, err);
|
||||
}
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ['files'] });
|
||||
console.log(`[useAutoSync] sync terminé`);
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
isRunning.current = false;
|
||||
}
|
||||
}, [queryClient]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(checkAndSync, 30_000);
|
||||
checkAndSync();
|
||||
return () => clearInterval(interval);
|
||||
}, [checkAndSync]);
|
||||
|
||||
return { triggerSync: checkAndSync };
|
||||
}
|
||||
@@ -180,7 +180,7 @@ export function useDeviceFiles() {
|
||||
safDirectory.addFolder(dirUri, dirName);
|
||||
setFolders(safDirectory.getAll());
|
||||
|
||||
const folderFiles = await scanSafFolder({ id: 'new', uri: dirUri, name: dirName, visible: true });
|
||||
const folderFiles = await scanSafFolder({ id: 'new', uri: dirUri, name: dirName, visible: true, syncMode: 'none', syncCellular: false });
|
||||
setFiles((prev) => {
|
||||
const existing = new Set(prev.map((f) => f.id));
|
||||
const merged = [...prev];
|
||||
|
||||
@@ -25,6 +25,7 @@ export function useLocalFiles() {
|
||||
size: df.size,
|
||||
syncStatus: 'local',
|
||||
createdAt: df.createdAt,
|
||||
folderId: df.folderId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { createMMKV } from 'react-native-mmkv';
|
||||
import { localFileRegistry } from '../services/localFileRegistry';
|
||||
import { safDirectory } from '../services/safDirectory';
|
||||
import { useDeviceFiles } from './useDeviceFiles';
|
||||
|
||||
const syncStateStorage = createMMKV({ id: 'vaultdrop-sync-state' });
|
||||
|
||||
export function getIsSyncing(): boolean {
|
||||
return syncStateStorage.getString('is_syncing') === 'true';
|
||||
}
|
||||
|
||||
export function setIsSyncing(value: boolean) {
|
||||
syncStateStorage.set('is_syncing', value ? 'true' : 'false');
|
||||
}
|
||||
|
||||
export function useSyncQueue() {
|
||||
const [pendingCount, setPendingCount] = useState(0);
|
||||
const [isSyncing, setIsSyncingState] = useState(() => getIsSyncing());
|
||||
const { folders } = useDeviceFiles();
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
const allFolders = safDirectory.getAll();
|
||||
const syncFolderIds = new Set(
|
||||
allFolders.filter((f) => f.syncMode !== 'none').map((f) => f.id)
|
||||
);
|
||||
|
||||
if (syncFolderIds.size === 0) {
|
||||
setPendingCount(0);
|
||||
setIsSyncingState(getIsSyncing());
|
||||
return;
|
||||
}
|
||||
|
||||
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++;
|
||||
}
|
||||
|
||||
setPendingCount(count);
|
||||
setIsSyncingState(getIsSyncing());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
const interval = setInterval(refresh, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [refresh]);
|
||||
|
||||
return { pendingCount, isSyncing, refresh };
|
||||
}
|
||||
Generated
+11
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@react-native-community/netinfo": "12.0.1",
|
||||
"@react-navigation/native": "^7.3.8",
|
||||
"@react-navigation/native-stack": "^7.17.10",
|
||||
"@tanstack/react-query": "^5.101.2",
|
||||
@@ -1625,6 +1626,16 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-community/netinfo": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-12.0.1.tgz",
|
||||
"integrity": "sha512-P/3caXIvfYSJG8AWJVefukg+ZGRPs+M4Lp3pNJtgcTYoJxCjWrKQGNnCkj/Cz//zWa/avGed0i/wzm0T8vV2IQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": ">=0.59"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/assets-registry": {
|
||||
"version": "0.86.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.86.0.tgz",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"main": "index.ts",
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@react-native-community/netinfo": "12.0.1",
|
||||
"@react-navigation/native": "^7.3.8",
|
||||
"@react-navigation/native-stack": "^7.17.10",
|
||||
"@tanstack/react-query": "^5.101.2",
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { createMMKV } from 'react-native-mmkv';
|
||||
|
||||
export type SyncMode = 'none' | 'manual' | 'auto';
|
||||
|
||||
export type StoredFolder = {
|
||||
id: string;
|
||||
uri: string;
|
||||
name: string;
|
||||
visible: boolean;
|
||||
syncMode: SyncMode;
|
||||
syncCellular: boolean;
|
||||
};
|
||||
|
||||
const storage = createMMKV({ id: 'vaultdrop-saf' });
|
||||
@@ -18,7 +22,20 @@ function generateId(): string {
|
||||
function getAllRaw(): StoredFolder[] {
|
||||
const raw = storage.getString(FOLDERS_KEY);
|
||||
if (!raw) return [];
|
||||
return JSON.parse(raw) as StoredFolder[];
|
||||
const parsed = JSON.parse(raw) as StoredFolder[];
|
||||
let migrated = false;
|
||||
for (const f of parsed) {
|
||||
if (f.syncMode === undefined) {
|
||||
(f as any).syncMode = 'none';
|
||||
migrated = true;
|
||||
}
|
||||
if (f.syncCellular === undefined) {
|
||||
(f as any).syncCellular = false;
|
||||
migrated = true;
|
||||
}
|
||||
}
|
||||
if (migrated) saveAll(parsed);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function saveAll(folders: StoredFolder[]) {
|
||||
@@ -39,7 +56,7 @@ export const safDirectory = {
|
||||
if (folders.some((f) => f.uri === uri)) {
|
||||
return folders.find((f) => f.uri === uri)!;
|
||||
}
|
||||
const folder: StoredFolder = { id: generateId(), uri, name, visible: true };
|
||||
const folder: StoredFolder = { id: generateId(), uri, name, visible: true, syncMode: 'none', syncCellular: false };
|
||||
folders.push(folder);
|
||||
saveAll(folders);
|
||||
return folder;
|
||||
@@ -57,6 +74,20 @@ export const safDirectory = {
|
||||
saveAll(folders);
|
||||
},
|
||||
|
||||
updateSyncMode(id: string, syncMode: SyncMode) {
|
||||
const folders = getAllRaw().map((f) =>
|
||||
f.id === id ? { ...f, syncMode } : f
|
||||
);
|
||||
saveAll(folders);
|
||||
},
|
||||
|
||||
updateSyncCellular(id: string, syncCellular: boolean) {
|
||||
const folders = getAllRaw().map((f) =>
|
||||
f.id === id ? { ...f, syncCellular } : f
|
||||
);
|
||||
saveAll(folders);
|
||||
},
|
||||
|
||||
clear() {
|
||||
storage.remove(FOLDERS_KEY);
|
||||
},
|
||||
|
||||
@@ -129,4 +129,5 @@ export interface LocalFileEntry {
|
||||
syncStatus: SyncStatus;
|
||||
createdAt: string;
|
||||
tags?: Tag[];
|
||||
folderId?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user