select image and generate pdf
This commit is contained in:
@@ -1,11 +1,15 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { View, Text, StyleSheet, FlatList, TouchableOpacity, Image, TextInput, Alert, ActivityIndicator } from 'react-native';
|
import { View, Text, StyleSheet, FlatList, TouchableOpacity, Image, TextInput, Alert, ActivityIndicator, Modal, Dimensions } from 'react-native';
|
||||||
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
|
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
|
||||||
import { useBatchStore } from '../hooks/useBatchStore';
|
import { useBatchStore } from '../hooks/useBatchStore';
|
||||||
import { usePdfGeneration } from '../hooks/usePdfGeneration';
|
import { usePdfGeneration } from '../hooks/usePdfGeneration';
|
||||||
import { useUpload } from '../hooks/useUpload';
|
import { useUpload } from '../hooks/useUpload';
|
||||||
import { CapturedPhoto } from '../types';
|
import { CapturedPhoto } from '../types';
|
||||||
|
|
||||||
|
const NUM_COLUMNS = 3;
|
||||||
|
const SCREEN_WIDTH = Dimensions.get('window').width;
|
||||||
|
const ITEM_SIZE = (SCREEN_WIDTH - 16 * 2 - (NUM_COLUMNS - 1) * 6) / NUM_COLUMNS;
|
||||||
|
|
||||||
type PendingReviewRouteParams = {
|
type PendingReviewRouteParams = {
|
||||||
PendingReview: { batchId: string; photoIds: string[] };
|
PendingReview: { batchId: string; photoIds: string[] };
|
||||||
};
|
};
|
||||||
@@ -24,26 +28,21 @@ export function PendingReviewScreen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [photos, setPhotos] = useState<CapturedPhoto[]>(initialPhotos);
|
const [photos, setPhotos] = useState<CapturedPhoto[]>(initialPhotos);
|
||||||
|
const [selectedSet, setSelectedSet] = useState<Set<string>>(new Set(initialPhotos.map((p) => p.id)));
|
||||||
const [batchTags, setBatchTags] = useState<string[]>(batch?.tags ?? []);
|
const [batchTags, setBatchTags] = useState<string[]>(batch?.tags ?? []);
|
||||||
const [tagInput, setTagInput] = useState('');
|
const [tagInput, setTagInput] = useState('');
|
||||||
|
const [previewUri, setPreviewUri] = useState<string | null>(null);
|
||||||
|
|
||||||
const moveUp = useCallback((index: number) => {
|
const orderedPhotos = photos.filter((p) => selectedSet.has(p.id));
|
||||||
if (index === 0) return;
|
|
||||||
setPhotos((prev) => {
|
const togglePhoto = (id: string) => {
|
||||||
const next = [...prev];
|
setSelectedSet((prev) => {
|
||||||
[next[index - 1], next[index]] = [next[index], next[index - 1]];
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const moveDown = useCallback((index: number) => {
|
|
||||||
if (index >= photos.length - 1) return;
|
|
||||||
setPhotos((prev) => {
|
|
||||||
const next = [...prev];
|
|
||||||
[next[index], next[index + 1]] = [next[index + 1], next[index]];
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, [photos.length]);
|
|
||||||
|
|
||||||
const handleAddTag = () => {
|
const handleAddTag = () => {
|
||||||
const tag = tagInput.trim().toLowerCase();
|
const tag = tagInput.trim().toLowerCase();
|
||||||
@@ -59,9 +58,12 @@ export function PendingReviewScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleFinalize = async () => {
|
const handleFinalize = async () => {
|
||||||
if (photos.length === 0) return;
|
if (orderedPhotos.length === 0) {
|
||||||
|
Alert.alert('Aucune photo', 'Sélectionnez au moins une photo');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const pdfUri = await generatePdf(photos.map((p) => ({ uri: p.uri })));
|
const pdfUri = await generatePdf(orderedPhotos.map((p) => ({ uri: p.uri })));
|
||||||
if (!pdfUri) {
|
if (!pdfUri) {
|
||||||
Alert.alert('Erreur', 'Échec de la génération du PDF');
|
Alert.alert('Erreur', 'Échec de la génération du PDF');
|
||||||
return;
|
return;
|
||||||
@@ -103,39 +105,44 @@ export function PendingReviewScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderPhoto = ({ item, index }: { item: CapturedPhoto; index: number }) => {
|
||||||
|
const isSelected = selectedSet.has(item.id);
|
||||||
|
const pageNumber = isSelected
|
||||||
|
? orderedPhotos.findIndex((p) => p.id === item.id) + 1
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.gridItem}
|
||||||
|
onPress={() => togglePhoto(item.id)}
|
||||||
|
onLongPress={() => setPreviewUri(item.uri)}
|
||||||
|
>
|
||||||
|
<Image source={{ uri: item.uri }} style={styles.thumb} />
|
||||||
|
{isSelected && (
|
||||||
|
<View style={styles.selectedOverlay}>
|
||||||
|
<Text style={styles.pageNumber}>{pageNumber}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.title}>Réorganiser les photos</Text>
|
<Text style={styles.title}>Réorganiser les photos</Text>
|
||||||
<Text style={styles.subtitle}>{photos.length} photo{photos.length > 1 ? 's' : ''}</Text>
|
<Text style={styles.subtitle}>
|
||||||
|
{orderedPhotos.length}/{photos.length} sélectionnée{orderedPhotos.length > 1 ? 's' : ''}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<FlatList
|
<FlatList
|
||||||
data={photos}
|
data={photos}
|
||||||
|
numColumns={NUM_COLUMNS}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
contentContainerStyle={styles.list}
|
contentContainerStyle={styles.grid}
|
||||||
renderItem={({ item, index }) => (
|
columnWrapperStyle={styles.gridRow}
|
||||||
<View style={styles.photoRow}>
|
renderItem={renderPhoto}
|
||||||
<Image source={{ uri: item.uri }} style={styles.thumb} />
|
|
||||||
<Text style={styles.photoIndex}>#{index + 1}</Text>
|
|
||||||
<View style={styles.arrows}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.arrowBtn, index === 0 && styles.arrowDisabled]}
|
|
||||||
onPress={() => moveUp(index)}
|
|
||||||
disabled={index === 0}
|
|
||||||
>
|
|
||||||
<Text style={styles.arrowText}>▲</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.arrowBtn, index >= photos.length - 1 && styles.arrowDisabled]}
|
|
||||||
onPress={() => moveDown(index)}
|
|
||||||
disabled={index >= photos.length - 1}
|
|
||||||
>
|
|
||||||
<Text style={styles.arrowText}>▼</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={styles.tagSection}>
|
<View style={styles.tagSection}>
|
||||||
@@ -181,6 +188,14 @@ export function PendingReviewScreen() {
|
|||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<Modal visible={previewUri !== null} transparent animationType="fade" onRequestClose={() => setPreviewUri(null)}>
|
||||||
|
<TouchableOpacity style={styles.modalOverlay} activeOpacity={1} onPress={() => setPreviewUri(null)}>
|
||||||
|
{previewUri && (
|
||||||
|
<Image source={{ uri: previewUri }} style={styles.previewImage} resizeMode="contain" />
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -213,47 +228,34 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#666',
|
color: '#666',
|
||||||
},
|
},
|
||||||
list: {
|
grid: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
},
|
||||||
photoRow: {
|
gridRow: {
|
||||||
flexDirection: 'row',
|
gap: 6,
|
||||||
alignItems: 'center',
|
},
|
||||||
marginBottom: 12,
|
gridItem: {
|
||||||
backgroundColor: '#f9f9f9',
|
width: ITEM_SIZE,
|
||||||
borderRadius: 8,
|
height: ITEM_SIZE,
|
||||||
padding: 8,
|
borderRadius: 6,
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: '#f0f0f0',
|
||||||
|
marginBottom: 6,
|
||||||
},
|
},
|
||||||
thumb: {
|
thumb: {
|
||||||
width: 60,
|
width: '100%',
|
||||||
height: 60,
|
height: '100%',
|
||||||
borderRadius: 6,
|
|
||||||
backgroundColor: '#e0e0e0',
|
|
||||||
},
|
},
|
||||||
photoIndex: {
|
selectedOverlay: {
|
||||||
marginLeft: 12,
|
...StyleSheet.absoluteFill,
|
||||||
fontSize: 16,
|
backgroundColor: 'rgba(25,118,210,0.35)',
|
||||||
fontWeight: '600',
|
|
||||||
color: '#333',
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
arrows: {
|
|
||||||
gap: 4,
|
|
||||||
},
|
|
||||||
arrowBtn: {
|
|
||||||
width: 36,
|
|
||||||
height: 28,
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: '#e0e0e0',
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
arrowDisabled: {
|
pageNumber: {
|
||||||
opacity: 0.3,
|
color: '#fff',
|
||||||
},
|
fontSize: 28,
|
||||||
arrowText: {
|
fontWeight: '800',
|
||||||
fontSize: 14,
|
|
||||||
color: '#333',
|
|
||||||
},
|
},
|
||||||
tagSection: {
|
tagSection: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
@@ -336,4 +338,14 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.9)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
previewImage: {
|
||||||
|
width: SCREEN_WIDTH * 0.95,
|
||||||
|
height: '80%',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user