import { Toast } from '@/utils/toast.utils'; import { Ionicons } from '@expo/vector-icons'; import { BlurView } from 'expo-blur'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import dayjs from 'dayjs'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Animated, Modal, Platform, Pressable, Share, StyleSheet, Text, View } from 'react-native'; import { captureRef } from 'react-native-view-shot'; import type { BadgeDto } from '@/services/badges'; export type BadgeShowcaseModalProps = { badge: BadgeDto | null; onClose: () => void; username: string; appName: string; visible?: boolean; }; export const BadgeShowcaseModal = ({ badge, onClose, username, appName, visible }: BadgeShowcaseModalProps) => { const { t } = useTranslation(); const isVisible = visible ?? Boolean(badge); const scaleAnim = useRef(new Animated.Value(0.8)).current; const glowAnim = useRef(new Animated.Value(0)).current; const pulseAnim = useRef(null); const shareCardRef = useRef(null); const [sharing, setSharing] = useState(false); useEffect(() => { if (!isVisible) { scaleAnim.setValue(0.8); glowAnim.setValue(0); pulseAnim.current?.stop(); return; } Animated.spring(scaleAnim, { toValue: 1, stiffness: 220, damping: 18, mass: 0.8, useNativeDriver: true, }).start(); pulseAnim.current = Animated.loop( Animated.sequence([ Animated.timing(glowAnim, { toValue: 1, duration: 1600, useNativeDriver: true, }), Animated.timing(glowAnim, { toValue: 0, duration: 1400, useNativeDriver: true, }), ]) ); pulseAnim.current.start(); return () => { pulseAnim.current?.stop(); }; }, [glowAnim, isVisible, scaleAnim]); const glowScale = glowAnim.interpolate({ inputRange: [0, 1], outputRange: [1, 1.25], }); const glowOpacity = glowAnim.interpolate({ inputRange: [0, 1], outputRange: [0.45, 0.9], }); const handleShareBadge = useCallback(async () => { if (!badge?.isAwarded || !shareCardRef.current) { return; } setSharing(true); try { Toast.show({ type: 'info', text1: t('badges.share.generating', '正在生成分享卡片…') }); const uri = await captureRef(shareCardRef, { format: 'png', quality: 0.95, }); const shareMessage = t('badges.share.message', { badgeName: badge.name, defaultValue: `我刚刚在 Digital Pilates 解锁了「${badge.name}」勋章!`, }); await Share.share({ title: badge.name, message: shareMessage, url: Platform.OS === 'ios' ? uri : `file://${uri}`, }); } catch (error) { console.warn('badge-share-failed', error); Toast.error(t('badges.share.failed', '分享失败,请稍后再试')); } finally { setSharing(false); } }, [badge, t]); if (!badge) { return null; } return ( {badge.isAwarded ? ( isLiquidGlassAvailable() ? ( [styles.shareCta, pressed && styles.shareCtaPressed]} onPress={handleShareBadge} accessibilityRole="button" > {sharing ? t('badges.share.processing', '生成中…') : t('badges.share.cta', '分享')} ) : ( [styles.shareCta, pressed && styles.shareCtaPressed]} onPress={handleShareBadge} accessibilityRole="button" > {sharing ? t('badges.share.processing', '生成中…') : t('badges.share.cta', '分享')} ) ) : null} ); }; type BadgeShowcaseContentProps = { badge: BadgeDto; glowOpacity: Animated.AnimatedInterpolation; glowScale: Animated.AnimatedInterpolation; username: string; appName: string; statusText: string; }; const BadgeShowcaseContent = ({ badge, glowOpacity, glowScale, username, appName, statusText }: BadgeShowcaseContentProps) => { const awardDate = badge.earnedAt ?? badge.awardedAt; const formattedAwardDate = badge.isAwarded && awardDate ? dayjs(awardDate).format('YYYY-MM-DD') : null; return ( {badge.imageUrl ? ( ) : ( {badge.icon ?? '🏅'} )} @{username} {appName} {badge.name} {badge.description} {statusText} {formattedAwardDate ? ( {formattedAwardDate} ) : null} ); }; const styles = StyleSheet.create({ modalBackdrop: { flex: 1, backgroundColor: 'rgba(3,7,18,0.75)', justifyContent: 'center', alignItems: 'center', padding: 24, }, showcaseCard: { width: '90%', maxWidth: 360, borderRadius: 28, overflow: 'hidden', backgroundColor: '#F8FAFC', paddingBottom: 24, }, showcaseGradient: { height: 280, borderBottomLeftRadius: 32, borderBottomRightRadius: 32, justifyContent: 'center', alignItems: 'center', padding: 24, }, showcaseGlowWrapper: { width: 220, height: 220, justifyContent: 'center', alignItems: 'center', }, glowRing: { position: 'absolute', width: 240, height: 240, borderRadius: 120, backgroundColor: 'rgba(255,255,255,0.35)', shadowColor: '#FFFFFF', shadowOpacity: 0.9, shadowRadius: 20, }, showcaseImageShell: { width: 180, height: 180, borderRadius: 90, backgroundColor: 'rgba(255,255,255,0.2)', borderWidth: 1.5, borderColor: 'rgba(255,255,255,0.45)', overflow: 'hidden', }, showcaseImage: { width: '100%', height: '100%', }, showcaseImageFallback: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(255,255,255,0.15)', }, showcaseImageFallbackText: { fontSize: 74, }, showcaseTextBlock: { paddingHorizontal: 24, paddingTop: 24, }, showcaseMetaRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, }, showcaseUsername: { fontSize: 14, fontWeight: '600', color: '#0F172A', }, showcaseAppPill: { flexDirection: 'row', alignItems: 'center', columnGap: 4, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 999, backgroundColor: '#FDE68A', }, showcaseAppText: { fontSize: 12, fontWeight: '600', color: '#0F172A', }, showcaseTitle: { fontSize: 24, fontWeight: '700', color: '#0F172A', textAlign: 'center', }, showcaseDescription: { marginTop: 12, fontSize: 14, lineHeight: 20, color: '#475467', textAlign: 'center', }, statusPill: { flexDirection: 'row', alignItems: 'center', columnGap: 6, paddingHorizontal: 16, paddingVertical: 8, borderRadius: 999, }, statusPillEarned: { backgroundColor: 'rgba(16,185,129,0.18)', }, statusPillLocked: { backgroundColor: 'rgba(129,140,248,0.18)', }, statusPillText: { fontSize: 13, fontWeight: '600', }, statusPillTextEarned: { color: '#047857', }, statusPillTextLocked: { color: '#312E81', }, statusRow: { marginTop: 18, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', columnGap: 12, }, awardDateLabel: { fontSize: 13, color: '#475467', fontWeight: '600', }, closeButton: { position: 'absolute', top: 18, right: 18, width: 36, height: 36, borderRadius: 18, backgroundColor: 'rgba(255,255,255,0.9)', alignItems: 'center', justifyContent: 'center', }, shareCta: { marginTop: 16, marginHorizontal: 24, borderRadius: 999, overflow: 'hidden', }, shareCtaGradient: { paddingVertical: 14, paddingHorizontal: 20, borderRadius: 999, }, shareCtaContent: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', columnGap: 10, }, shareCtaLabel: { fontSize: 15, fontWeight: '700', color: '#1C1917', }, shareCtaPressed: { opacity: 0.85, }, shareCtaGlassInner: { paddingVertical: 14, paddingHorizontal: 20, }, shareCardOffscreen: { position: 'absolute', opacity: 0, top: -10000, left: 0, right: 0, }, });