delete fikes

This commit is contained in:
m
2026-07-29 22:54:10 +02:00
parent e4b6e393d8
commit 79ff07b7c4
6 changed files with 282 additions and 96 deletions
+18 -15
View File
@@ -1,9 +1,10 @@
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { View, Text, StyleSheet, FlatList, TouchableOpacity, Image, Alert, ActivityIndicator } from 'react-native'; import { View, Text, StyleSheet, FlatList, TouchableOpacity, Image, ActivityIndicator } from 'react-native';
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native'; import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useBatchStore } from '../hooks/useBatchStore'; import { useBatchStore } from '../hooks/useBatchStore';
import { usePdfGeneration } from '../hooks/usePdfGeneration'; import { usePdfGeneration } from '../hooks/usePdfGeneration';
import { ConfirmModal } from '../components/ConfirmModal';
type RootStackParamList = { type RootStackParamList = {
Home: undefined; Home: undefined;
@@ -22,6 +23,7 @@ export function BatchReviewScreen() {
const batch = getBatch(route.params.batchId); const batch = getBatch(route.params.batchId);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set()); const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false);
if (!batch) { if (!batch) {
return ( return (
@@ -44,22 +46,12 @@ export function BatchReviewScreen() {
const handleDelete = () => { const handleDelete = () => {
if (selectedCount === 0) return; if (selectedCount === 0) return;
const label = selectedCount === 1 ? 'cette photo' : `ces ${selectedCount} photos`; setConfirmDeleteVisible(true);
Alert.alert( };
'Supprimer',
`Retirer ${label} du lot ?`, const handleDeleteConfirm = () => {
[
{ text: 'Annuler', style: 'cancel' },
{
text: 'Supprimer',
style: 'destructive',
onPress: () => {
selectedIds.forEach((id) => removePhotoFromBatch(batch.id, id)); selectedIds.forEach((id) => removePhotoFromBatch(batch.id, id));
setSelectedIds(new Set()); setSelectedIds(new Set());
},
},
]
);
}; };
const handleGroup = () => { const handleGroup = () => {
@@ -131,6 +123,17 @@ export function BatchReviewScreen() {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </View>
<ConfirmModal
visible={confirmDeleteVisible}
title="Supprimer"
message={`Retirer ${selectedCount === 1 ? 'cette photo' : `ces ${selectedCount} photos`} du lot ?`}
options={[
{ label: 'Annuler' },
{ label: 'Supprimer', destructive: true, onPress: handleDeleteConfirm },
]}
onClose={() => setConfirmDeleteVisible(false)}
/>
</View> </View>
); );
} }
+19 -16
View File
@@ -10,7 +10,6 @@ import {
Pressable, Pressable,
TouchableOpacity, TouchableOpacity,
Share, Share,
Alert,
} from 'react-native'; } from 'react-native';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
@@ -20,6 +19,7 @@ import { useFile, useDownloadFile } from '../hooks/useFiles';
import { fileStore } from '../services/fileStore'; import { fileStore } from '../services/fileStore';
import { TagChip } from '../components/TagChip'; import { TagChip } from '../components/TagChip';
import { FileThumbnail } from '../components/FileThumbnail'; import { FileThumbnail } from '../components/FileThumbnail';
import { ConfirmModal } from '../components/ConfirmModal';
import { SyncStatusBadge } from '../components/SyncStatusBadge'; import { SyncStatusBadge } from '../components/SyncStatusBadge';
import { GestureHandlerRootView, Gesture, GestureDetector } from 'react-native-gesture-handler'; import { GestureHandlerRootView, Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle, withSpring, runOnJS } from 'react-native-reanimated'; import Animated, { useSharedValue, useAnimatedStyle, withSpring, runOnJS } from 'react-native-reanimated';
@@ -100,6 +100,7 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
const [optionsVisible, setOptionsVisible] = useState(false); const [optionsVisible, setOptionsVisible] = useState(false);
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
const [confirmDeleteVisible, setConfirmDeleteVisible] = useState(false);
const panelOffset = useSharedValue(PANEL_HEIGHT - PANEL_HEADER_VISIBLE); const panelOffset = useSharedValue(PANEL_HEIGHT - PANEL_HEADER_VISIBLE);
const panelStartY = useSharedValue(0); const panelStartY = useSharedValue(0);
const isPanelExpanded = useSharedValue(false); const isPanelExpanded = useSharedValue(false);
@@ -136,17 +137,7 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
} }
}, [uri, fileName, file?.url]); }, [uri, fileName, file?.url]);
const handleDelete = useCallback(() => { const handleDeleteConfirm = useCallback(async () => {
setOptionsVisible(false);
Alert.alert(
'Supprimer',
`Supprimer "${fileName}" définitivement ?`,
[
{ text: 'Annuler', style: 'cancel' },
{
text: 'Supprimer',
style: 'destructive',
onPress: async () => {
setDeleting(true); setDeleting(true);
try { try {
const bid = localEntry?.backendId ?? (file as any)?.backendResourceId; const bid = localEntry?.backendId ?? (file as any)?.backendResourceId;
@@ -164,12 +155,13 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
} catch {} finally { } catch {} finally {
setDeleting(false); setDeleting(false);
} }
},
},
],
);
}, [fileName, fileId, localEntry, file, navigation]); }, [fileName, fileId, localEntry, file, navigation]);
const handleDelete = useCallback(() => {
setOptionsVisible(false);
setConfirmDeleteVisible(true);
}, []);
const togglePanelJS = useCallback(() => { const togglePanelJS = useCallback(() => {
if (isPanelExpanded.value) { if (isPanelExpanded.value) {
panelOffset.value = withSpring(PANEL_HEIGHT - PANEL_HEADER_VISIBLE); panelOffset.value = withSpring(PANEL_HEIGHT - PANEL_HEADER_VISIBLE);
@@ -387,6 +379,17 @@ function DetailItem({ fileId, deviceFile, onSelectImage }: { fileId: string; dev
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</Modal> </Modal>
<ConfirmModal
visible={confirmDeleteVisible}
title="Supprimer"
message={`Supprimer "${fileName}" définitivement ?`}
options={[
{ label: 'Annuler' },
{ label: 'Supprimer', destructive: true, onPress: handleDeleteConfirm },
]}
onClose={() => setConfirmDeleteVisible(false)}
/>
</View> </View>
); );
} }
+18 -9
View File
@@ -1,5 +1,5 @@
import React, { useMemo, useState, useCallback, useEffect } from 'react'; import React, { useMemo, useState, useCallback, useEffect } from 'react';
import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, Alert, Modal, TextInput } from 'react-native'; import { View, FlatList, StyleSheet, TouchableOpacity, Text, Dimensions, Modal, TextInput } from 'react-native';
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native'; import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
@@ -9,6 +9,7 @@ import {
useAddTags, useMoveResources, useFolders, useCreateFolder, useAddTags, useMoveResources, useFolders, useCreateFolder,
} from '../hooks/useFiles'; } from '../hooks/useFiles';
import { SelectionPanel } from '../components/SelectionPanel'; import { SelectionPanel } from '../components/SelectionPanel';
import { ConfirmModal, type ConfirmOption } from '../components/ConfirmModal';
import { UnifiedFileItem } from '../types'; import { UnifiedFileItem } from '../types';
import { isFolder } from '../types'; import { isFolder } from '../types';
import { FileThumbnail } from '../components/FileThumbnail'; import { FileThumbnail } from '../components/FileThumbnail';
@@ -104,6 +105,7 @@ export function FolderScreen() {
const [tagModalMode, setTagModalMode] = useState<'tag' | 'folder'>('tag'); const [tagModalMode, setTagModalMode] = useState<'tag' | 'folder'>('tag');
const [tagInput, setTagInput] = useState(''); const [tagInput, setTagInput] = useState('');
const [moveModalVisible, setMoveModalVisible] = useState(false); const [moveModalVisible, setMoveModalVisible] = useState(false);
const [confirmDeleteState, setConfirmDeleteState] = useState<{ message: string; options: ConfirmOption[] } | null>(null);
const selectionMode = selectedIds.size > 0; const selectionMode = selectedIds.size > 0;
@@ -142,12 +144,10 @@ export function FolderScreen() {
return f?.syncStatus === 'synced'; return f?.syncStatus === 'synced';
}); });
const label = ids.length === 1 ? 'ce fichier' : `ces ${ids.length} fichiers`; const label = ids.length === 1 ? 'ce fichier' : `ces ${ids.length} fichiers`;
const options: Array<{ text: string; style?: 'default' | 'cancel' | 'destructive'; onPress?: () => void }> = [ const options: ConfirmOption[] = [];
{ text: 'Annuler', style: 'cancel' },
];
if (hasSynced) { if (hasSynced) {
options.push({ options.push({
text: 'Du device uniquement', label: 'Du device uniquement',
onPress: async () => { onPress: async () => {
const syncedIds = ids.filter((id) => { const syncedIds = ids.filter((id) => {
const f = files.find((fi) => fi.id === id); const f = files.find((fi) => fi.id === id);
@@ -161,8 +161,8 @@ export function FolderScreen() {
}); });
} }
options.push({ options.push({
text: 'Du device + serveur', label: 'Du device + serveur',
style: 'destructive', destructive: true,
onPress: async () => { onPress: async () => {
for (const id of ids) { for (const id of ids) {
const f = files.find((fi) => fi.id === id); const f = files.find((fi) => fi.id === id);
@@ -177,11 +177,12 @@ export function FolderScreen() {
} }
downloadRegistry.remove(id); downloadRegistry.remove(id);
} }
await queryClient.invalidateQueries({ queryKey: ['files'] }); await queryClient.invalidateQueries({ queryKey: ['resources'] });
setSelectedIds(new Set()); setSelectedIds(new Set());
}, },
}); });
Alert.alert('Supprimer', `Supprimer ${label} ?`, options); options.push({ label: 'Annuler' });
setConfirmDeleteState({ message: `Supprimer ${label} ?`, options });
}, [selectedIds, files, deleteFile, freeLocalSpace, queryClient]); }, [selectedIds, files, deleteFile, freeLocalSpace, queryClient]);
const handleEdit = useCallback(() => { const handleEdit = useCallback(() => {
@@ -358,6 +359,14 @@ export function FolderScreen() {
</TouchableOpacity> </TouchableOpacity>
</TouchableOpacity> </TouchableOpacity>
</Modal> </Modal>
<ConfirmModal
visible={confirmDeleteState !== null}
title="Supprimer"
message={confirmDeleteState?.message}
options={confirmDeleteState?.options ?? []}
onClose={() => setConfirmDeleteState(null)}
/>
</View> </View>
); );
} }
+38 -23
View File
@@ -12,6 +12,7 @@ import { UnifiedFileItem, isFolder } from '../types';
import { SearchBar, SearchFilters, MediaFilter } from '../components/SearchBar'; import { SearchBar, SearchFilters, MediaFilter } from '../components/SearchBar';
import { FileThumbnail } from '../components/FileThumbnail'; import { FileThumbnail } from '../components/FileThumbnail';
import { SettingsModal } from '../components/SettingsModal'; import { SettingsModal } from '../components/SettingsModal';
import { ConfirmModal, type ConfirmOption } from '../components/ConfirmModal';
import { UploadModal } from '../components/UploadModal'; import { UploadModal } from '../components/UploadModal';
import { SyncStatusIcon } from '../components/SyncStatusIcon'; import { SyncStatusIcon } from '../components/SyncStatusIcon';
import { useSyncQueue } from '../hooks/useSyncQueue'; import { useSyncQueue } from '../hooks/useSyncQueue';
@@ -119,6 +120,8 @@ export function HomeScreen() {
const [uploadModalVisible, setUploadModalVisible] = useState(false); const [uploadModalVisible, setUploadModalVisible] = useState(false);
const [globalSyncMode, setGlobalSyncMode] = useState<SyncGlobalMode>(() => safDirectory.getGlobalSyncMode()); const [globalSyncMode, setGlobalSyncMode] = useState<SyncGlobalMode>(() => safDirectory.getGlobalSyncMode());
const [globalSyncCellular, setGlobalSyncCellular] = useState(() => safDirectory.getGlobalSyncCellular()); const [globalSyncCellular, setGlobalSyncCellular] = useState(() => safDirectory.getGlobalSyncCellular());
const [confirmDeleteState, setConfirmDeleteState] = useState<{ message: string; options: ConfirmOption[] } | null>(null);
const [removeFolderConfirmId, setRemoveFolderConfirmId] = useState<string | null>(null);
const { pendingCount, isSyncing } = useSyncQueue(); const { pendingCount, isSyncing } = useSyncQueue();
useAutoSync(); useAutoSync();
@@ -247,12 +250,10 @@ export function HomeScreen() {
return f?.syncStatus === 'synced'; return f?.syncStatus === 'synced';
}); });
const label = ids.length === 1 ? 'ce fichier' : `ces ${ids.length} fichiers`; const label = ids.length === 1 ? 'ce fichier' : `ces ${ids.length} fichiers`;
const options: Array<{ text: string; style?: 'default' | 'cancel' | 'destructive'; onPress?: () => void }> = [ const options: ConfirmOption[] = [];
{ text: 'Annuler', style: 'cancel' },
];
if (hasSynced) { if (hasSynced) {
options.push({ options.push({
text: 'Du device uniquement', label: 'Du device uniquement',
onPress: async () => { onPress: async () => {
const syncedIds = ids.filter((id) => { const syncedIds = ids.filter((id) => {
const f = files.find((fi) => fi.id === id); const f = files.find((fi) => fi.id === id);
@@ -266,8 +267,8 @@ export function HomeScreen() {
}); });
} }
options.push({ options.push({
text: 'Du device + serveur', label: 'Du device + serveur',
style: 'destructive', destructive: true,
onPress: async () => { onPress: async () => {
for (const id of ids) { for (const id of ids) {
const f = files.find((fi) => fi.id === id); const f = files.find((fi) => fi.id === id);
@@ -282,11 +283,12 @@ export function HomeScreen() {
} }
downloadRegistry.remove(id); downloadRegistry.remove(id);
} }
await queryClient.invalidateQueries({ queryKey: ['files'] }); await queryClient.invalidateQueries({ queryKey: ['resources'] });
setSelectedIds(new Set()); setSelectedIds(new Set());
}, },
}); });
Alert.alert('Supprimer', `Supprimer ${label} ?`, options); options.push({ label: 'Annuler' });
setConfirmDeleteState({ message: `Supprimer ${label} ?`, options });
}, [selectedIds, files, freeLocalSpace, queryClient]); }, [selectedIds, files, freeLocalSpace, queryClient]);
const handleEdit = useCallback(() => { const handleEdit = useCallback(() => {
@@ -339,22 +341,16 @@ export function HomeScreen() {
}, [refreshFolders]); }, [refreshFolders]);
const handleRemoveFolder = useCallback((folderId: string) => { const handleRemoveFolder = useCallback((folderId: string) => {
Alert.alert( setRemoveFolderConfirmId(folderId);
'Supprimer le dossier', }, []);
'Le dossier sera retiré de la liste. Les fichiers resteront sur votre appareil.',
[ const handleRemoveFolderConfirm = useCallback(() => {
{ text: 'Annuler', style: 'cancel' }, if (removeFolderConfirmId) {
{ safDirectory.removeFolder(removeFolderConfirmId);
text: 'Supprimer',
style: 'destructive',
onPress: () => {
safDirectory.removeFolder(folderId);
refreshFolders(); refreshFolders();
}, }
}, setRemoveFolderConfirmId(null);
] }, [removeFolderConfirmId, refreshFolders]);
);
}, [refreshFolders]);
const handleAddFolder = useCallback(async () => { const handleAddFolder = useCallback(async () => {
setSettingsModalVisible(false); setSettingsModalVisible(false);
@@ -650,6 +646,25 @@ export function HomeScreen() {
globalSyncCellular={globalSyncCellular} globalSyncCellular={globalSyncCellular}
onSetGlobalSyncCellular={handleSetGlobalSyncCellular} onSetGlobalSyncCellular={handleSetGlobalSyncCellular}
/> />
<ConfirmModal
visible={confirmDeleteState !== null}
title="Supprimer"
message={confirmDeleteState?.message}
options={confirmDeleteState?.options ?? []}
onClose={() => setConfirmDeleteState(null)}
/>
<ConfirmModal
visible={removeFolderConfirmId !== null}
title="Supprimer le dossier"
message="Le dossier sera retiré de la liste. Les fichiers resteront sur votre appareil."
options={[
{ label: 'Annuler' },
{ label: 'Supprimer', destructive: true, onPress: handleRemoveFolderConfirm },
]}
onClose={() => setRemoveFolderConfirmId(null)}
/>
</KeyboardAvoidingView> </KeyboardAvoidingView>
); );
} }
+148
View File
@@ -0,0 +1,148 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Modal } from 'react-native';
export interface ConfirmOption {
label: string;
onPress?: () => void;
destructive?: boolean;
}
interface ConfirmModalProps {
visible: boolean;
title: string;
message?: string;
options: ConfirmOption[];
onClose: () => void;
}
export function ConfirmModal({ visible, title, message, options, onClose }: ConfirmModalProps) {
const stacked = options.length > 2;
return (
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
<TouchableOpacity style={styles.overlay} activeOpacity={1} onPress={onClose}>
<TouchableOpacity activeOpacity={1} style={styles.container} onPress={() => {}}>
<View style={styles.handle} />
<Text style={styles.title}>{title}</Text>
{message ? <Text style={styles.message}>{message}</Text> : null}
<View style={[styles.optionsContainer, stacked && styles.optionsStacked]}>
{options.map((opt, i) => (
<TouchableOpacity
key={i}
style={[
stacked ? styles.stackedBtn : styles.sideBtn,
opt.destructive && (stacked ? styles.stackedDestructive : styles.sideDestructive),
!opt.destructive && !stacked && styles.sideBtnSecondary,
!opt.destructive && stacked && styles.stackedSecondary,
i < options.length - 1 && stacked && styles.stackedBtnBorder,
]}
onPress={() => {
onClose();
opt.onPress?.();
}}
>
<Text
style={[
stacked ? styles.stackedBtnText : styles.sideBtnText,
opt.destructive && (stacked ? styles.stackedDestructiveText : styles.sideDestructiveText),
!opt.destructive && !stacked && styles.sideBtnSecondaryText,
!opt.destructive && stacked && styles.stackedSecondaryText,
]}
>
{opt.label}
</Text>
</TouchableOpacity>
))}
</View>
</TouchableOpacity>
</TouchableOpacity>
</Modal>
);
}
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%',
},
handle: {
width: 36,
height: 4,
borderRadius: 2,
backgroundColor: '#ddd',
alignSelf: 'center',
marginBottom: 16,
},
title: {
fontSize: 18,
fontWeight: '700',
color: '#333',
marginBottom: 8,
},
message: {
fontSize: 14,
color: '#666',
lineHeight: 20,
marginBottom: 20,
},
optionsContainer: {
flexDirection: 'row',
gap: 10,
},
optionsStacked: {
flexDirection: 'column',
gap: 0,
},
sideBtn: {
flex: 1,
paddingVertical: 12,
borderRadius: 10,
alignItems: 'center',
},
sideDestructive: {
backgroundColor: '#E53935',
},
sideBtnSecondary: {
backgroundColor: '#f5f5f5',
},
sideBtnText: {
fontSize: 15,
fontWeight: '600',
},
sideDestructiveText: {
color: '#fff',
},
sideBtnSecondaryText: {
color: '#666',
},
stackedBtn: {
paddingVertical: 14,
alignItems: 'center',
},
stackedBtnBorder: {
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
stackedDestructive: {},
stackedSecondary: {},
stackedBtnText: {
fontSize: 16,
fontWeight: '600',
},
stackedDestructiveText: {
color: '#E53935',
},
stackedSecondaryText: {
color: '#333',
},
});
+22 -14
View File
@@ -6,10 +6,10 @@ import {
TouchableOpacity, TouchableOpacity,
Modal, Modal,
ScrollView, ScrollView,
Alert,
} from 'react-native'; } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { StoredFolder, SyncMode, SyncGlobalMode, type FolderSource } from '../services/safDirectory'; import { StoredFolder, SyncMode, SyncGlobalMode, type FolderSource } from '../services/safDirectory';
import { ConfirmModal } from './ConfirmModal';
type SettingsView = 'menu' | 'folders' | 'sync'; type SettingsView = 'menu' | 'folders' | 'sync';
@@ -57,6 +57,7 @@ export function SettingsModal({
onSetGlobalSyncCellular, onSetGlobalSyncCellular,
}: SettingsModalProps) { }: SettingsModalProps) {
const [view, setView] = useState<SettingsView>('menu'); const [view, setView] = useState<SettingsView>('menu');
const [confirmRemoveFolder, setConfirmRemoveFolder] = useState<StoredFolder | null>(null);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
setView('menu'); setView('menu');
@@ -65,22 +66,18 @@ export function SettingsModal({
const handleRemoveFolder = useCallback( const handleRemoveFolder = useCallback(
(folder: StoredFolder) => { (folder: StoredFolder) => {
Alert.alert( setConfirmRemoveFolder(folder);
'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]
); );
const handleRemoveFolderConfirm = useCallback(() => {
if (confirmRemoveFolder) {
onRemoveFolder(confirmRemoveFolder.id);
}
setConfirmRemoveFolder(null);
}, [confirmRemoveFolder, onRemoveFolder]);
return ( return (
<Modal <Modal
visible={visible} visible={visible}
@@ -128,6 +125,17 @@ export function SettingsModal({
)} )}
</TouchableOpacity> </TouchableOpacity>
</TouchableOpacity> </TouchableOpacity>
<ConfirmModal
visible={confirmRemoveFolder !== null}
title="Supprimer le dossier"
message="Le dossier sera retiré de la liste. Les fichiers resteront sur votre appareil."
options={[
{ label: 'Annuler' },
{ label: 'Supprimer', destructive: true, onPress: handleRemoveFolderConfirm },
]}
onClose={() => setConfirmRemoveFolder(null)}
/>
</Modal> </Modal>
); );
} }