diff --git a/app/picker.tsx b/app/picker.tsx new file mode 100644 index 0000000..416b15e --- /dev/null +++ b/app/picker.tsx @@ -0,0 +1,266 @@ +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, + }, +});