import React, { useEffect, useState, useCallback, useRef } from 'react'; import { View, Text, FlatList, StyleSheet, TouchableOpacity, ActivityIndicator, RefreshControl, Animated, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { Image } from 'expo-image'; import { Ionicons } from '@expo/vector-icons'; import { getUploaderInfo, getUploaderVideos } from '../../services/bilibili'; import type { VideoItem } from '../../services/types'; import { useTheme } from '../../utils/theme'; import { formatCount, formatDuration, formatTime } from '../../utils/format'; import { proxyImageUrl, coverImageUrl } from '../../utils/imageUrl'; import { useSettingsStore } from '../../store/settingsStore'; const PAGE_SIZE = 20; const TOPBAR_HEIGHT = 44; const FADE_START = 80; const FADE_END = 160; const AnimatedFlatList = Animated.createAnimatedComponent(FlatList); export default function CreatorScreen() { const { mid: midStr } = useLocalSearchParams<{ mid: string }>(); const mid = Number(midStr); const router = useRouter(); const theme = useTheme(); const insets = useSafeAreaInsets(); const trafficSaving = useSettingsStore(s => s.trafficSaving); const [info, setInfo] = useState<{ name: string; face: string; sign: string; follower: number; archiveCount: number; } | null>(null); const [videos, setVideos] = useState([]); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const [infoLoading, setInfoLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const loadingRef = useRef(false); const scrollY = useRef(new Animated.Value(0)).current; const topBarOpacity = scrollY.interpolate({ inputRange: [FADE_START, FADE_END], outputRange: [0, 1], extrapolate: 'clamp', }); useEffect(() => { getUploaderInfo(mid) .then(setInfo) .catch(() => {}) .finally(() => setInfoLoading(false)); loadVideos(1, true); }, [mid]); const loadVideos = useCallback(async (pn: number, reset = false) => { if (loadingRef.current) return; loadingRef.current = true; setLoading(true); try { const { videos: newVideos, total: t } = await getUploaderVideos(mid, pn, PAGE_SIZE); setTotal(t); setVideos(prev => reset ? newVideos : [...prev, ...newVideos]); setPage(pn); } catch {} finally { loadingRef.current = false; setLoading(false); } }, [mid]); const handleRefresh = useCallback(async () => { setRefreshing(true); try { const [infoData, { videos: newVideos, total: t }] = await Promise.all([ getUploaderInfo(mid), getUploaderVideos(mid, 1, PAGE_SIZE), ]); setInfo(infoData); setTotal(t); setVideos(newVideos); setPage(1); } catch {} finally { setRefreshing(false); } }, [mid]); const hasMore = videos.length < total; const HeroHeader = ( {info ? ( <> ) : ( )} {infoLoading ? ( ) : info ? ( {info.name} {info.sign ? ( {info.sign} ) : null} {formatCount(info.follower)} 粉丝 {formatCount(info.archiveCount)} 视频 全部视频({total}) ) : null} ); return ( item.bvid} showsVerticalScrollIndicator={false} onEndReached={() => { if (hasMore && !loading) loadVideos(page + 1); }} onEndReachedThreshold={0.3} windowSize={7} maxToRenderPerBatch={6} onScroll={Animated.event( [{ nativeEvent: { contentOffset: { y: scrollY } } }], { useNativeDriver: true }, )} scrollEventThrottle={16} refreshControl={ } ListHeaderComponent={HeroHeader} renderItem={({ item }) => ( router.push(`/video/${item.bvid}` as any)} activeOpacity={0.85} > {formatDuration(item.duration)} {item.title} {formatCount(item.stat?.view ?? 0)} {!!item.pubdate && ( · {formatTime(item.pubdate)} )} )} ListEmptyComponent={ !loading && !infoLoading ? ( 暂无视频 ) : null } ListFooterComponent={ {loading && } } /> {/* 浮动 topBar:背景透明 → 不透明,跟随滚动 */} router.back()} style={styles.backBtn}> {info?.name ?? 'UP主主页'} ); } const styles = StyleSheet.create({ safe: { flex: 1 }, hero: { overflow: 'hidden', position: 'relative', borderBottomWidth: StyleSheet.hairlineWidth, }, heroBg: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, }, heroOverlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, opacity: 0.82, }, topBarFloat: { position: 'absolute', left: 0, right: 0, }, topBarBorder: { position: 'absolute', bottom: 0, left: 0, right: 0, height: StyleSheet.hairlineWidth, }, topBarContent: { flex: 1, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, }, backBtn: { padding: 4, width: 32 }, topTitle: { flex: 1, fontSize: 16, fontWeight: '600', textAlign: 'center' }, profileContent: { alignItems: 'center', paddingBottom: 16, }, avatar: { width: 72, height: 72, borderRadius: 36, marginBottom: 10 }, name: { fontSize: 18, fontWeight: '700', marginBottom: 6 }, sign: { fontSize: 13, textAlign: 'center', paddingHorizontal: 24, marginBottom: 12, lineHeight: 19 }, statsRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 16 }, statItem: { alignItems: 'center', paddingHorizontal: 24 }, statNum: { fontSize: 18, fontWeight: '700' }, statLabel: { fontSize: 12, marginTop: 2 }, statDivider: { width: 1, height: 28 }, videoListHeader: { alignSelf: 'flex-start', paddingHorizontal: 14, fontSize: 13, paddingBottom: 4 }, videoRow: { flexDirection: 'row', paddingHorizontal: 12, paddingVertical: 10, borderBottomWidth: StyleSheet.hairlineWidth, gap: 10, }, thumbWrap: { width: 120, height: 68, borderRadius: 4, overflow: 'hidden', flexShrink: 0, position: 'relative' }, thumb: { width: 120, height: 68 }, durationBadge: { position: 'absolute', bottom: 3, right: 3, backgroundColor: 'rgba(0,0,0,0.6)', borderRadius: 3, paddingHorizontal: 4, paddingVertical: 1, }, durationText: { color: '#fff', fontSize: 10 }, videoInfo: { flex: 1, justifyContent: 'space-between', paddingVertical: 2 }, videoTitle: { fontSize: 13, lineHeight: 18 }, videoMeta: { flexDirection: 'row', alignItems: 'center', gap: 4 }, metaText: { fontSize: 12 }, emptyTxt: { textAlign: 'center', padding: 40 }, footer: { height: 48, justifyContent: 'center', alignItems: 'center' }, });