import { BackButton } from '@/components/ui/back-button'; import { getTagByName, type TagTemplate } from '@/lib/api/tags'; import { getTemplateById } from '@/lib/api/templates'; import Feather from '@expo/vector-icons/Feather'; import { Stack, router, useLocalSearchParams } from 'expo-router'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { ActivityIndicator, Image, ScrollView, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; type TemplateFrame = { id: string; title: string; uri: string; accent: string; }; type TemplateLike = { id: string; title?: string | null; titleEn?: string | null; coverImageUrl?: string | null; previewUrl?: string | null; }; const ACCENT_COLORS = ['#D1FF00', '#FF7A45', '#7A8BFF', '#FF5F6D', '#21D4FD', '#FFAA00']; const FALLBACK_PREVIEW = 'https://images.unsplash.com/photo-1542204165-0198c1e21a3b?auto=format&fit=crop&w=1200&q=80'; const createFrame = (template: TemplateLike, index = 0): TemplateFrame => { const title = (template.titleEn || template.title || '').trim() || 'Untitled Template'; const uri = template.coverImageUrl || template.previewUrl || FALLBACK_PREVIEW; return { id: template.id, title, uri, accent: ACCENT_COLORS[index % ACCENT_COLORS.length], }; }; const mapTagTemplates = (templates: TagTemplate[]): TemplateFrame[] => templates.map((template, index) => createFrame(template, index)); const quickActions = [ { id: 'upscale', label: 'Upscale' }, { id: 'collect', label: 'Collect' }, { id: 'download', label: 'Download' }, ]; const PreviewFrame = React.memo<{ frame: TemplateFrame; isActive: boolean; onPress: () => void; }>(({ frame, isActive, onPress }) => { return ( ); }); PreviewFrame.displayName = 'PreviewFrame'; const CACHE_DURATION = 5 * 60 * 1000; const cache = new Map(); export default function TemplateDetailScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const [looks, setLooks] = useState([]); const [selectedId, setSelectedId] = useState(null); const [menuVisible, setMenuVisible] = useState(false); const [loadingLooks, setLoadingLooks] = useState(true); const [errorMessage, setErrorMessage] = useState(null); const [retryTrigger, setRetryTrigger] = useState(0); const abortControllerRef = useRef(null); const imageCache = useRef(new Set()); const getCachedData = useCallback((key: string) => { const cached = cache.get(key); if (cached && Date.now() - cached.timestamp < CACHE_DURATION) { return cached.data; } return null; }, []); const setCachedData = useCallback((key: string, data: TemplateFrame[]) => { cache.set(key, { data, timestamp: Date.now() }); }, []); const preloadImage = useCallback((uri: string) => { if (imageCache.current.has(uri)) { return; } Image.prefetch(uri).then(() => { imageCache.current.add(uri); }).catch(() => { }); }, []); useEffect(() => { abortControllerRef.current?.abort(); abortControllerRef.current = new AbortController(); const signal = abortControllerRef.current.signal; let isMounted = true; const loadByTagName = async (tagName?: string | null): Promise => { if (!tagName) { return []; } const cacheKey = `tag:${tagName}`; const cached = getCachedData(cacheKey); if (cached) { return cached; } try { const response = await getTagByName(tagName); if (response.success && response.data?.templates?.length) { const templates = mapTagTemplates(response.data.templates); setCachedData(cacheKey, templates); return templates; } } catch (tagError) { console.warn('Failed to load tag templates:', tagError); } return []; }; const hydrateLooks = async () => { if (!id || typeof id !== 'string') { setLooks([]); setSelectedId(null); setErrorMessage('Missing template identifier'); setLoadingLooks(false); return; } setLoadingLooks(true); setErrorMessage(null); try { const templateResponse = await getTemplateById(id); if (!signal.aborted && templateResponse.success && templateResponse.data) { const template = templateResponse.data; const primaryTagName = template.tags?.[0]?.nameEn || template.tags?.[0]?.name; const [relatedFrames, mainFrame] = await Promise.allSettled([ loadByTagName(primaryTagName), Promise.resolve(createFrame(template, 0)) ]); const frames: TemplateFrame[] = []; if (relatedFrames.status === 'fulfilled' && relatedFrames.value.length > 0) { frames.push(...relatedFrames.value); } else { frames.push(mainFrame.status === 'fulfilled' ? mainFrame.value : createFrame(template, 0)); } if (!isMounted || signal.aborted) { return; } setLooks(frames); setCachedData(`template:${id}`, frames); const preferredId = frames.find(frame => frame.id === id)?.id ?? frames[0]?.id ?? template.id; setSelectedId(preferredId); frames.slice(0, 3).forEach(frame => preloadImage(frame.uri)); } else { if (!isMounted || signal.aborted) { return; } setErrorMessage('Template not found'); setLooks([]); setSelectedId(null); } } catch (templateError) { if (!isMounted || signal.aborted) { return; } console.warn('Failed to fetch template detail:', templateError); setErrorMessage('Failed to load template'); setLooks([]); setSelectedId(null); } finally { if (isMounted && !signal.aborted) { setLoadingLooks(false); } } }; hydrateLooks(); return () => { isMounted = false; abortControllerRef.current?.abort(); }; }, [id, getCachedData, setCachedData, preloadImage]); const current = useMemo(() => { if (looks.length === 0) { return null; } if (!selectedId) { return looks[0]; } return looks.find(look => look.id === selectedId) ?? looks[0]; }, [looks, selectedId]); const resolvedTemplateId = current?.id ?? (typeof id === 'string' ? id : ''); const heroImageUri = current?.uri ?? FALLBACK_PREVIEW; const infoTitle = current?.title ?? 'Loading template'; const handleRetry = useCallback(() => { cache.delete(`template:${id}`); cache.delete(`tag:${current?.title}`); setErrorMessage(null); setLoadingLooks(true); setRetryTrigger(prev => prev + 1); }, [id, current?.title]); useEffect(() => { if (retryTrigger > 0) { const newController = new AbortController(); abortControllerRef.current = newController; } }, [retryTrigger]); const handleGenerate = () => { if (!resolvedTemplateId) { return; } router.push(`/templates/${resolvedTemplateId}/form`); }; return ( <> router.back()} /> {infoTitle} {loadingLooks && ( Loading templates... )} {!loadingLooks && errorMessage && ( {errorMessage} Retry )} {looks.length > 0 ? ( {looks.map(look => ( setSelectedId(look.id)} /> ))} ) : ( !loadingLooks && ( Nothing to show yet ) )} {infoTitle} setMenuVisible(prev => !prev)} > Generate Video {menuVisible && ( {quickActions.map(action => ( setMenuVisible(false)} > {action.label} ))} )} ); } const styles = StyleSheet.create({ canvas: { flex: 1, backgroundColor: '#040404', }, safeArea: { flex: 1, }, body: { flex: 1, }, scrollContent: { paddingHorizontal: 24, paddingBottom: 180, }, loadingState: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 24, paddingVertical: 8, }, loadingLabel: { fontSize: 13, color: 'rgba(255, 255, 255, 0.7)', }, errorBanner: { marginHorizontal: 24, marginTop: 8, paddingHorizontal: 16, paddingVertical: 10, borderRadius: 16, backgroundColor: 'rgba(255, 94, 94, 0.12)', borderWidth: 1, borderColor: 'rgba(255, 94, 94, 0.3)', }, errorText: { color: '#FF8A8A', fontSize: 13, fontWeight: '600', marginBottom: 8, }, retryButton: { backgroundColor: 'rgba(255, 94, 94, 0.2)', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 12, alignSelf: 'flex-start', }, retryLabel: { color: '#FF8A8A', fontSize: 13, fontWeight: '700', }, headerBar: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingTop: 8, paddingBottom: 8, gap: 12, }, headerTitle: { flex: 1, textAlign: 'center', fontSize: 18, fontWeight: '600', color: '#FFFFFF', letterSpacing: 0.4, }, headerPlaceholder: { width: 48, height: 48, }, topCarousel: { marginTop: 12, paddingVertical: 8, borderRadius: 22, backgroundColor: 'rgba(255, 255, 255, 0.04)', }, emptyCarousel: { alignItems: 'center', justifyContent: 'center', height: 80, }, emptyCarouselText: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 13, }, carouselContent: { paddingHorizontal: 12, alignItems: 'center', gap: 12, }, previewFrame: { width: 56, height: 56, borderRadius: 18, borderWidth: 1, overflow: 'hidden', backgroundColor: 'rgba(255, 255, 255, 0.08)', }, previewFrameActive: { transform: [{ scale: 1.05 }], }, previewImage: { width: '100%', height: '100%', }, heroStage: { marginTop: 32, borderRadius: 36, backgroundColor: '#111111', overflow: 'hidden', shadowColor: '#000000', shadowOpacity: 0.45, shadowRadius: 32, shadowOffset: { width: 0, height: 18 }, elevation: 24, }, heroImage: { width: '100%', height: 456, borderRadius: 36, }, bottomInfoRow: { flexDirection: 'row', alignItems: 'center', gap: 16, marginTop: 32, }, infoCard: { flex: 1, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, borderRadius: 24, backgroundColor: 'rgba(255, 255, 255, 0.08)', gap: 12, }, infoThumbnail: { width: 32, height: 32, borderRadius: 10, marginRight: 0, }, infoTitle: { fontSize: 14, fontWeight: '600', color: '#FFFFFF', flex: 1, flexShrink: 1, }, generateButton: { width: '100%', height: 56, borderRadius: 28, backgroundColor: '#D1FF00', alignItems: 'center', justifyContent: 'center', flexDirection: 'row', gap: 10, marginTop: 18, }, generateButtonDisabled: { opacity: 0.6, }, generateLabel: { fontSize: 16, fontWeight: '700', color: '#050505', letterSpacing: 0.3, }, menuTrigger: { width: 56, height: 56, borderRadius: 28, backgroundColor: 'rgba(255, 255, 255, 0.08)', alignItems: 'center', justifyContent: 'center', }, menuDots: { justifyContent: 'space-between', height: 24, }, dot: { width: 6, height: 6, borderRadius: 3, backgroundColor: '#FFFFFF', }, dotMiddle: { opacity: 0.7, }, menuPanel: { position: 'absolute', right: 24, bottom: 200, zIndex: 50, elevation: 24, paddingVertical: 12, paddingHorizontal: 16, borderRadius: 20, backgroundColor: 'rgba(10, 10, 10, 0.94)', borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.08)', gap: 12, }, menuItem: { paddingVertical: 4, }, menuLabel: { fontSize: 15, fontWeight: '600', color: '#FFFFFF', }, });