import { Feather } from "@expo/vector-icons"; import { Image } from "expo-image"; import React, { useCallback, useEffect, useState } from "react"; import { ActivityIndicator, FlatList, Modal, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useColors } from "@/hooks/useColors"; interface MediaItem { id: string; path: string; createdAt?: string; } interface Props { visible: boolean; baseUrl: string; apiKey: string; maxSelect: number; onClose: () => void; onSelect: (items: MediaItem[]) => void; } function resolveUrl(path: string, baseUrl: string): string { if (path.startsWith("http://") || path.startsWith("https://")) return path; const origin = baseUrl.replace(/\/api\/.*$/, ""); return `${origin}/${path.replace(/^\//, "")}`; } export function MediaLibraryModal({ visible, baseUrl, apiKey, maxSelect, onClose, onSelect }: Props) { const colors = useColors(); const insets = useSafeAreaInsets(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selected, setSelected] = useState>(new Set()); const load = useCallback(async () => { if (!baseUrl || !apiKey) return; setLoading(true); setError(null); try { // eslint-disable-next-line no-undef const res = await globalThis.fetch(`${baseUrl}/media`, { headers: { Authorization: apiKey }, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const list: MediaItem[] = Array.isArray(data) ? data : (data?.media ?? data?.items ?? data?.files ?? []); setItems(list); } catch (e: unknown) { setError(e instanceof Error ? e.message : "Failed to load media"); } finally { setLoading(false); } }, [baseUrl, apiKey]); useEffect(() => { if (visible) { setSelected(new Set()); load(); } }, [visible, load]); const toggle = (id: string) => { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else if (next.size < maxSelect) { next.add(id); } return next; }); }; const handleConfirm = () => { const chosen = items.filter((i) => selected.has(i.id)); onSelect(chosen); }; return ( Media Library 0 ? colors.primary : colors.muted }, ]} > {selected.size > 0 ? `Add ${selected.size}` : "Add"} {loading ? ( ) : error ? ( {error} Retry ) : items.length === 0 ? ( No media found ) : ( item.id} numColumns={3} contentContainerStyle={[styles.grid, { paddingBottom: insets.bottom + 16 }]} renderItem={({ item }) => { const isSelected = selected.has(item.id); const uri = resolveUrl(item.path, baseUrl); return ( toggle(item.id)} activeOpacity={0.8} style={styles.cell} > {isSelected && ( )} ); }} /> )} ); } const CELL = 120; const styles = StyleSheet.create({ root: { flex: 1 }, header: { flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: StyleSheet.hairlineWidth, gap: 12, }, closeBtn: { padding: 4 }, title: { flex: 1, fontSize: 17, fontFamily: "Inter_600SemiBold" }, addBtn: { paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20 }, addBtnText: { fontSize: 14, fontFamily: "Inter_600SemiBold" }, centered: { flex: 1, alignItems: "center", justifyContent: "center", gap: 12 }, errorText: { fontSize: 14, fontFamily: "Inter_400Regular", textAlign: "center", paddingHorizontal: 32 }, emptyText: { fontSize: 14, fontFamily: "Inter_400Regular" }, retryBtn: { paddingHorizontal: 20, paddingVertical: 10, borderRadius: 10 }, retryText: { fontSize: 14, fontFamily: "Inter_600SemiBold" }, grid: { padding: 2 }, cell: { width: CELL, height: CELL, margin: 2 }, cellImage: { width: CELL, height: CELL, borderRadius: 4 }, selectedOverlay: { ...StyleSheet.absoluteFillObject, borderRadius: 4, alignItems: "center", justifyContent: "center", }, checkCircle: { width: 28, height: 28, borderRadius: 14, alignItems: "center", justifyContent: "center", }, });