import { Feather } from '@expo/vector-icons'; import * as ImagePicker from 'expo-image-picker'; import { useCallback, useEffect, useState } from 'react'; import { Alert, FlatList, Image, Modal, Pressable, StyleSheet, Text, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { classifyImageUri, type ClassificationResult } from '../lib/classifier'; import { loadGallery, removeFromGallery, type GalleryItem } from '../lib/gallery'; import { colors, layout } from '../theme'; import { GlassSurface } from './GlassSurface'; import { HotDogIcon } from './HotDogIcon'; type ScanGalleryProps = { visible: boolean; onClose: () => void; onSelect: (item: GalleryItem) => void; onImport: (uri: string, result: ClassificationResult) => void; }; export function ScanGallery({ visible, onClose, onSelect, onImport, }: ScanGalleryProps) { const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const refresh = useCallback(async () => { setItems(await loadGallery()); }, []); useEffect(() => { if (visible) void refresh(); }, [visible, refresh]); const handleImport = useCallback(async () => { const permission = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (!permission.granted) { Alert.alert('Photos access needed to import an image.'); return; } setLoading(true); try { const picked = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], quality: 0.9, }); if (picked.canceled || !picked.assets[0]?.uri) return; const uri = picked.assets[0].uri; const result = await classifyImageUri(uri); onImport(uri, result); onClose(); } catch { Alert.alert('Could not scan that photo.'); } finally { setLoading(false); } }, [onClose, onImport]); const handleDelete = useCallback( (item: GalleryItem) => { Alert.alert('Delete scan?', undefined, [ { text: 'Cancel', style: 'cancel' }, { text: 'Delete', style: 'destructive', onPress: () => { void removeFromGallery(item.id).then(refresh); }, }, ]); }, [refresh], ); return ( Gallery void handleImport()} style={styles.headerButton} disabled={loading} accessibilityLabel="Import photo" > item.id} numColumns={3} contentContainerStyle={styles.grid} columnWrapperStyle={styles.gridRow} ListEmptyComponent={ No scans yet Scan a hot dog or tap + to import a photo. } renderItem={({ item }) => ( { onSelect(item); onClose(); }} onLongPress={() => handleDelete(item)} accessibilityLabel={ item.result.isHotDog ? 'Hot dog scan' : 'Not hot dog scan' } > {item.result.isHotDog ? ( ) : ( )} )} /> ); } const GAP = 8; const styles = StyleSheet.create({ root: { flex: 1, backgroundColor: colors.bg, }, safe: { flex: 1, paddingHorizontal: 16, }, headerWrap: { marginTop: layout.galleryHeaderTop, marginBottom: 16, }, header: { paddingHorizontal: 8, paddingVertical: 8, }, headerRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, headerButton: { width: layout.controlSize, height: layout.controlSize, alignItems: 'center', justifyContent: 'center', }, headerTitle: { color: colors.text, fontSize: 17, fontWeight: '700', }, grid: { paddingBottom: 24, flexGrow: 1, }, gridRow: { gap: GAP, marginBottom: GAP, }, cell: { flex: 1, aspectRatio: 1, borderRadius: layout.radius, overflow: 'hidden', backgroundColor: 'rgba(255, 255, 255, 0.04)', }, thumb: { width: '100%', height: '100%', }, badge: { position: 'absolute', top: 6, right: 6, width: 22, height: 22, borderRadius: 11, alignItems: 'center', justifyContent: 'center', }, empty: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingTop: 80, gap: 10, paddingHorizontal: 32, }, emptyTitle: { color: colors.text, fontSize: 18, fontWeight: '700', }, emptyBody: { color: colors.textMuted, fontSize: 15, textAlign: 'center', lineHeight: 21, }, });