import { useCallback, useEffect, useState } from "react"; import { ActivityIndicator, Dimensions, FlatList, Image, Linking, Pressable, StyleSheet, Text, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useRouter } from "expo-router"; import * as MediaLibrary from "expo-media-library"; import Animated, { FadeIn, FadeInDown } from "react-native-reanimated"; import { X } from "phosphor-react-native"; import { GlassCard, GlassButton, GlassPill } from "@/components/glass"; import { useWallpaperStore } from "@/stores/wallpaper.store"; import { requestGalleryPermission } from "@/services/media/gallery.service"; import { colors, typography, spacing, layout } from "@/theme"; const { width: SCREEN_WIDTH } = Dimensions.get("window"); const NUM_COLUMNS = 3; const IMAGE_GAP = 2; const IMAGE_SIZE = (SCREEN_WIDTH - IMAGE_GAP * (NUM_COLUMNS - 1)) / NUM_COLUMNS; type Tab = "recent" | "favorites" | "albums"; interface PhotoItem { id: string; uri: string; } export default function PickerScreen() { const insets = useSafeAreaInsets(); const router = useRouter(); const setSourceImage = useWallpaperStore((s) => s.setSourceImage); const resetEditor = useWallpaperStore((s) => s.resetEditor); const [tab, setTab] = useState("recent"); const [photos, setPhotos] = useState([]); const [hasPermission, setHasPermission] = useState(false); const [loading, setLoading] = useState(true); const [offset, setOffset] = useState(0); const [hasMore, setHasMore] = useState(true); useEffect(() => { (async () => { const granted = await requestGalleryPermission(); setHasPermission(granted); if (granted) { await loadPhotos(0); } setLoading(false); })(); }, []); const loadPhotos = useCallback(async (startOffset: number) => { try { const query = new MediaLibrary.Query() .eq(MediaLibrary.AssetField.MEDIA_TYPE, MediaLibrary.MediaType.IMAGE) .orderBy({ key: MediaLibrary.AssetField.CREATION_TIME, ascending: false, }) .limit(60) .offset(startOffset); const assets = await query.exe(); // Resolve URIs from assets const items: PhotoItem[] = []; for (const asset of assets) { try { const uri = await asset.getUri(); items.push({ id: asset.id, uri }); } catch { // Skip assets that can't be resolved (corrupted, cloud-only, etc.) } } setPhotos((prev) => (startOffset === 0 ? items : [...prev, ...items])); setOffset(startOffset + assets.length); setHasMore(assets.length === 60); } catch { // Gallery read error — device-specific edge case } }, []); const loadMore = useCallback(() => { if (hasMore) { loadPhotos(offset); } }, [hasMore, offset, loadPhotos]); const handleSelectPhoto = useCallback( (item: PhotoItem) => { resetEditor(); setSourceImage(item.uri); router.replace("/editor/new"); }, [router, setSourceImage, resetEditor] ); if (loading) { return ( ); } if (!hasPermission) { return ( ); } return ( {/* Header */} router.back()}> Choisir une photo {/* Tab pills */} setTab("recent")} /> setTab("favorites")} /> setTab("albums")} /> {/* Photo grid */} item.id} onEndReached={loadMore} onEndReachedThreshold={0.5} showsVerticalScrollIndicator={false} contentContainerStyle={[styles.grid, photos.length === 0 && styles.centered]} ListEmptyComponent={ Aucune photo trouvée dans votre galerie } renderItem={({ item }) => ( handleSelectPhoto(item)} style={({ pressed }) => [ styles.imageContainer, pressed && styles.imagePressed, ]} > )} /> ); } function PermissionDenied() { return ( Accès aux photos requis Lively a besoin d'accéder à votre galerie{"\n"}pour créer des fonds d'écran animés Linking.openSettings()} variant="primary" size="md" /> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.background, }, header: { paddingHorizontal: layout.screenPadding, marginBottom: spacing.sm, }, headerRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", }, tabs: { flexDirection: "row", paddingHorizontal: layout.screenPadding, marginBottom: spacing.md, }, grid: { paddingBottom: 40, }, imageContainer: { width: IMAGE_SIZE, height: IMAGE_SIZE, padding: IMAGE_GAP / 2, }, image: { flex: 1, }, imagePressed: { opacity: 0.7, }, permissionContainer: { flex: 1, justifyContent: "center", paddingHorizontal: layout.screenPadding + spacing.md, }, centered: { flex: 1, justifyContent: "center", alignItems: "center", }, emptyGallery: { paddingTop: 80, paddingHorizontal: layout.screenPadding, }, });