302 lines
6.8 KiB
TypeScript
302 lines
6.8 KiB
TypeScript
import { GenerateBtn } from '@/components/sker/generate-btn';
|
|
import { Header } from '@/components/sker/header';
|
|
import { ImgTab } from '@/components/sker/img-tab';
|
|
import { Page } from '@/components/sker/page';
|
|
import VideoPlayer from '@/components/sker/video-player/video-player';
|
|
import { getTagByName } from '@/lib/api/tags';
|
|
import Feather from '@expo/vector-icons/Feather';
|
|
import { router, useLocalSearchParams } from 'expo-router';
|
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import {
|
|
Image,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View,
|
|
} from 'react-native';
|
|
|
|
type TemplateFrame = {
|
|
id: string;
|
|
uri: string;
|
|
avatar?: string;
|
|
title?: string;
|
|
titleEn?: string;
|
|
};
|
|
|
|
|
|
export default function TemplateDetailScreen() {
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const [looks, setLooks] = useState<TemplateFrame[]>([]);
|
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!id || typeof id !== 'string') {
|
|
return;
|
|
}
|
|
|
|
abortControllerRef.current?.abort();
|
|
abortControllerRef.current = new AbortController();
|
|
const signal = abortControllerRef.current.signal;
|
|
|
|
const hydrateLooks = async () => {
|
|
|
|
try {
|
|
const templateResponse = await getTagByName(id);
|
|
if (signal.aborted) return;
|
|
|
|
if (templateResponse?.success && templateResponse.data) {
|
|
const template = templateResponse.data;
|
|
const frames: TemplateFrame[] = template.templates.map(it => ({
|
|
id: it.id,
|
|
uri: it.previewUrl,
|
|
title: it.title,
|
|
titleEn: it.titleEn,
|
|
avatar: it.coverImageUrl
|
|
}));
|
|
|
|
if (signal.aborted) return;
|
|
|
|
setLooks(frames);
|
|
setSelectedId(frames.find(f => f.id === id)?.id ?? frames[0]?.id ?? template.id);
|
|
} else {
|
|
if (signal.aborted) return;
|
|
}
|
|
} catch (templateError) {
|
|
if (signal.aborted) return;
|
|
console.warn('Failed to fetch template detail:', templateError);
|
|
} finally { }
|
|
};
|
|
|
|
hydrateLooks();
|
|
|
|
return () => abortControllerRef.current?.abort();
|
|
}, [id]);
|
|
|
|
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 infoTitle = useMemo(() => {
|
|
return current?.titleEn ?? current?.title ?? 'Loading template'
|
|
}, [current]);
|
|
|
|
const handleGenerate = useCallback(() => {
|
|
if (current?.id) {
|
|
router.push(`/templates/${current.id}/form`);
|
|
}
|
|
}, [current]);
|
|
|
|
return (
|
|
<Page>
|
|
<Header title={infoTitle} />
|
|
<ImgTab images={looks} activeId={selectedId} onActiveChange={(id) => {
|
|
setSelectedId(id)
|
|
}} />
|
|
<ScrollView
|
|
style={styles.body}
|
|
showsVerticalScrollIndicator={false}
|
|
contentContainerStyle={styles.scrollContent}
|
|
>
|
|
<View style={styles.heroStage}>
|
|
<VideoPlayer
|
|
source={{ uri: current?.uri! }}
|
|
style={styles.heroImage}
|
|
/>
|
|
</View>
|
|
|
|
</ScrollView>
|
|
<GenerateBtn onGenerate={handleGenerate} menuItems={[]}>
|
|
<View style={styles.bottomInfoRow}>
|
|
<View style={styles.infoCard}>
|
|
<Image source={{ uri: current?.avatar }} style={styles.infoThumbnail} />
|
|
<Text style={styles.infoTitle} numberOfLines={1}>
|
|
{infoTitle}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</GenerateBtn>
|
|
</Page>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
canvas: {
|
|
flex: 1,
|
|
backgroundColor: '#040404',
|
|
},
|
|
safeArea: {
|
|
flex: 1,
|
|
},
|
|
body: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
paddingHorizontal: 40,
|
|
paddingBottom: 40,
|
|
},
|
|
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)',
|
|
},
|
|
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,
|
|
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,
|
|
gap: 12,
|
|
},
|
|
infoThumbnail: {
|
|
width: 48,
|
|
height: 48,
|
|
borderRadius: 10,
|
|
marginRight: 0,
|
|
rotation: 6
|
|
},
|
|
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,
|
|
},
|
|
});
|