import React, { useState, useEffect, useRef } from "react"; import { View, Text, ScrollView, FlatList, StyleSheet, TouchableOpacity, Image, ActivityIndicator, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { useLocalSearchParams, useRouter } from "expo-router"; import { Ionicons } from "@expo/vector-icons"; import { VideoPlayer } from "../../components/VideoPlayer"; import { CommentItem } from "../../components/CommentItem"; import { getDanmaku } from "../../services/bilibili"; import { DanmakuItem } from "../../services/types"; import DanmakuList from "../../components/DanmakuList"; import { useVideoDetail } from "../../hooks/useVideoDetail"; import { useComments } from "../../hooks/useComments"; import { useVideoStore } from "../../store/videoStore"; import { formatCount } from "../../utils/format"; import { proxyImageUrl } from "../../utils/imageUrl"; type Tab = "intro" | "comments" | "danmaku"; export default function VideoDetailScreen() { const { bvid } = useLocalSearchParams<{ bvid: string }>(); const router = useRouter(); const { video, playData, loading: videoLoading, qualities, currentQn, changeQuality, } = useVideoDetail(bvid as string); const [commentSort, setCommentSort] = useState<0 | 2>(2); const flatListHeightRef = useRef(0); const { comments, loading: cmtLoading, hasMore: cmtHasMore, load: loadComments, } = useComments(video?.aid ?? 0, commentSort); const [tab, setTab] = useState("intro"); const [danmakus, setDanmakus] = useState([]); const [currentTime, setCurrentTime] = useState(0); const { setVideo, clearVideo } = useVideoStore(); useEffect(() => { clearVideo(); }, [bvid]); useEffect(() => { if (video?.aid) loadComments(); }, [video?.aid, commentSort]); useEffect(() => { if (!video?.cid) return; getDanmaku(video.cid).then(setDanmakus); }, [video?.cid]); function handleMiniPlayer() { if (video) { setVideo(bvid as string, video.title, video.pic); router.back(); } } return ( {/* TopBar */} router.back()} style={styles.backBtn}> {video?.title ?? "视频详情"} {/* Video player — fixed 16:9 */} {/* TabBar — sits directly below player, always visible once video loads */} {video && ( setTab("intro")}> 简介 {tab === "intro" && } setTab("comments")}> 评论{video.stat?.reply > 0 ? ` ${formatCount(video.stat.reply)}` : ""} {tab === "comments" && } setTab("danmaku")}> 弹幕{danmakus.length > 0 ? ` ${formatCount(danmakus.length)}` : ""} {tab === "danmaku" && } )} {/* Tab content */} {videoLoading ? ( ) : video ? ( tab === "intro" ? ( // 简介:视频信息 + 合集 + 简介文本 {video.title} {video.owner.name} + 关注 {video.ugc_season && ( router.replace(`/video/${epBvid}`)} /> )} {video.desc || "暂无简介"} ) : tab === "danmaku" ? ( {}} style={styles.danmakuTab} /> ) : ( String(c.rpid)} renderItem={({ item }) => } onEndReached={() => { if (cmtHasMore && !cmtLoading) loadComments(); }} onEndReachedThreshold={0.3} onLayout={({ nativeEvent }) => { flatListHeightRef.current = nativeEvent.layout.height; }} onContentSizeChange={(_, contentHeight) => { if (contentHeight < flatListHeightRef.current && cmtHasMore && !cmtLoading) { loadComments(); } }} showsVerticalScrollIndicator={false} ListHeaderComponent={ 排序 setCommentSort(2)} > 热门 setCommentSort(0)} > 最新 } ListFooterComponent={ cmtLoading ? ( ) : !cmtHasMore && comments.length > 0 ? ( 已加载全部评论 ) : null } ListEmptyComponent={ !cmtLoading ? 暂无评论 : null } /> ) ) : null} ); } function StatBadge({ icon, count }: { icon: string; count: number }) { return ( {formatCount(count)} ); } function SeasonSection({ season, currentBvid, onEpisodePress, }: { season: NonNullable; currentBvid: string; onEpisodePress: (bvid: string) => void; }) { const episodes = season.sections?.[0]?.episodes ?? []; const currentIndex = episodes.findIndex((ep) => ep.bvid === currentBvid); const listRef = useRef(null); useEffect(() => { if (currentIndex <= 0 || episodes.length === 0) return; // 等布局完成再滚动 listRef.current?.scrollToIndex({ index: currentIndex, viewPosition: 0.5, // 居中 animated: false, }); }, [currentIndex, episodes.length]); return ( 合集 · {season.title} {season.ep_count}个视频 ep.bvid} contentContainerStyle={{ paddingHorizontal: 12, gap: 10 }} // 每个卡片宽 120,gap 10,让 FlatList 直接算任意索引的偏移量 getItemLayout={(_data, index) => ({ length: 130, offset: 12 + index * 130, index, })} onScrollToIndexFailed={() => {}} renderItem={({ item: ep, index }) => { const isCurrent = ep.bvid === currentBvid; return ( !isCurrent && onEpisodePress(ep.bvid)} activeOpacity={0.8} > {ep.arc?.pic && ( )} 第{index + 1}集 {ep.title} ); }} /> ); } const styles = StyleSheet.create({ safe: { flex: 1, backgroundColor: "#fff" }, topBar: { flexDirection: "row", alignItems: "center", paddingHorizontal: 8, paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: "#eee", }, backBtn: { padding: 4 }, topTitle: { flex: 1, fontSize: 15, fontWeight: "600", marginLeft: 4, color: "#212121", }, miniBtn: { padding: 4 }, loader: { marginVertical: 30 }, titleSection: { padding: 14 }, title: { fontSize: 16, fontWeight: "600", color: "#212121", lineHeight: 22, marginBottom: 8, }, statsRow: { flexDirection: "row", gap: 16 }, stat: { flexDirection: "row", alignItems: "center", gap: 3 }, statText: { fontSize: 12, color: "#999" }, upRow: { flexDirection: "row", alignItems: "center", paddingHorizontal: 14, paddingBottom: 12, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: "#f0f0f0", }, avatar: { width: 38, height: 38, borderRadius: 19, marginRight: 10 }, upName: { flex: 1, fontSize: 14, color: "#212121", fontWeight: "500" }, followBtn: { backgroundColor: "#00AEEC", paddingHorizontal: 14, paddingVertical: 5, borderRadius: 14, }, followTxt: { color: "#fff", fontSize: 12, fontWeight: "600" }, seasonBox: { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: "#f0f0f0", paddingVertical: 10, }, seasonHeader: { flexDirection: "row", alignItems: "center", paddingHorizontal: 14, paddingBottom: 8, gap: 4, }, seasonTitle: { flex: 1, fontSize: 13, fontWeight: "600", color: "#212121" }, seasonCount: { fontSize: 12, color: "#999" }, epCard: { width: 120, borderRadius: 6, overflow: "hidden", backgroundColor: "#f8f8f8", borderWidth: 1.5, borderColor: "transparent", }, epCardActive: { borderColor: "#00AEEC" }, epThumb: { width: 120, height: 68, backgroundColor: "#eee" }, epNum: { fontSize: 11, color: "#999", paddingHorizontal: 6, paddingTop: 4 }, epNumActive: { color: "#00AEEC", fontWeight: "600" }, epTitle: { fontSize: 12, color: "#333", paddingHorizontal: 6, paddingBottom: 6, lineHeight: 16, }, tabBar: { flexDirection: "row", borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: "#eee", }, tabItem: { flex: 1, alignItems: "center", paddingVertical: 12, position: "relative", }, tabLabel: { fontSize: 14, color: "#999" }, tabActive: { color: "#00AEEC", fontWeight: "700" }, tabUnderline: { position: "absolute", bottom: 0, width: 24, height: 2, backgroundColor: "#00AEEC", borderRadius: 1, }, tabScroll: { flex: 1 }, descBox: { padding: 16 }, descText: { fontSize: 14, color: "#555", lineHeight: 22 }, danmakuTab: { flex: 1 }, emptyTxt: { textAlign: "center", color: "#bbb", padding: 30 }, sortRow: { flexDirection: "row", alignItems: "center", paddingHorizontal: 14, paddingVertical: 10, gap: 8, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: "#f0f0f0", }, sortLabel: { fontSize: 13, color: "#999", marginRight: 4 }, sortBtn: { paddingHorizontal: 12, paddingVertical: 4, borderRadius: 12, borderWidth: 1, borderColor: "#e0e0e0", }, sortBtnActive: { borderColor: "#00AEEC", backgroundColor: "#e8f7fd" }, sortBtnTxt: { fontSize: 12, color: "#666" }, sortBtnTxtActive: { color: "#00AEEC", fontWeight: "600" as const }, });