mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-07 23:18:38 +08:00
feat: 视频详情页和直播详情页支持暗黑模式主题切换
This commit is contained in:
@@ -17,12 +17,14 @@ import { LivePlayer } from "../../components/LivePlayer";
|
|||||||
import DanmakuList from "../../components/DanmakuList";
|
import DanmakuList from "../../components/DanmakuList";
|
||||||
import { formatCount } from "../../utils/format";
|
import { formatCount } from "../../utils/format";
|
||||||
import { proxyImageUrl } from "../../utils/imageUrl";
|
import { proxyImageUrl } from "../../utils/imageUrl";
|
||||||
|
import { useTheme } from "../../utils/theme";
|
||||||
|
|
||||||
type Tab = "intro" | "danmaku";
|
type Tab = "intro" | "danmaku";
|
||||||
|
|
||||||
export default function LiveDetailScreen() {
|
export default function LiveDetailScreen() {
|
||||||
const { roomId } = useLocalSearchParams<{ roomId: string }>();
|
const { roomId } = useLocalSearchParams<{ roomId: string }>();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const theme = useTheme();
|
||||||
const id = parseInt(roomId ?? "0", 10);
|
const id = parseInt(roomId ?? "0", 10);
|
||||||
const { room, anchor, stream, loading, error, changeQuality } =
|
const { room, anchor, stream, loading, error, changeQuality } =
|
||||||
useLiveDetail(id);
|
useLiveDetail(id);
|
||||||
@@ -34,17 +36,17 @@ export default function LiveDetailScreen() {
|
|||||||
const qualities = stream?.qualities ?? [];
|
const qualities = stream?.qualities ?? [];
|
||||||
const currentQn = stream?.qn ?? 0;
|
const currentQn = stream?.qn ?? 0;
|
||||||
|
|
||||||
// Use actual roomid from room detail (not the short/alias ID from the URL)
|
|
||||||
const actualRoomId = room?.roomid ?? id;
|
const actualRoomId = room?.roomid ?? id;
|
||||||
const { danmakus, giftCounts } = useLiveDanmaku(isLive ? actualRoomId : 0);
|
const { danmakus, giftCounts } = useLiveDanmaku(isLive ? actualRoomId : 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe}>
|
<SafeAreaView style={[styles.safe, { backgroundColor: theme.card }]}>
|
||||||
{/* TopBar */}
|
{/* TopBar */}
|
||||||
<View style={styles.topBar}>
|
<View style={[styles.topBar, { borderBottomColor: theme.border }]}>
|
||||||
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
||||||
<Ionicons name="chevron-back" size={24} color="#212121" />
|
<Ionicons name="chevron-back" size={24} color={theme.text} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.topTitle} numberOfLines={1}>
|
<Text style={[styles.topTitle, { color: theme.text }]} numberOfLines={1}>
|
||||||
{room?.title ?? "直播间"}
|
{room?.title ?? "直播间"}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -60,12 +62,12 @@ export default function LiveDetailScreen() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* TabBar */}
|
{/* TabBar */}
|
||||||
<View style={styles.tabBar}>
|
<View style={[styles.tabBar, { backgroundColor: theme.card, borderBottomColor: theme.border }]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.tabItem}
|
style={styles.tabItem}
|
||||||
onPress={() => setTab("intro")}
|
onPress={() => setTab("intro")}
|
||||||
>
|
>
|
||||||
<Text style={[styles.tabLabel, tab === "intro" && styles.tabActive]}>
|
<Text style={[styles.tabLabel, { color: theme.textSub }, tab === "intro" && styles.tabActive]}>
|
||||||
简介
|
简介
|
||||||
</Text>
|
</Text>
|
||||||
{tab === "intro" && <View style={styles.tabUnderline} />}
|
{tab === "intro" && <View style={styles.tabUnderline} />}
|
||||||
@@ -75,7 +77,7 @@ export default function LiveDetailScreen() {
|
|||||||
onPress={() => setTab("danmaku")}
|
onPress={() => setTab("danmaku")}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[styles.tabLabel, tab === "danmaku" && styles.tabActive]}
|
style={[styles.tabLabel, { color: theme.textSub }, tab === "danmaku" && styles.tabActive]}
|
||||||
>
|
>
|
||||||
弹幕{danmakus.length > 0 ? ` ${danmakus.length}` : ""}
|
弹幕{danmakus.length > 0 ? ` ${danmakus.length}` : ""}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -83,7 +85,7 @@ export default function LiveDetailScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Content — 两个面板始终挂载,通过 display 切换,保留弹幕列表状态 */}
|
{/* Content */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
||||||
) : error ? (
|
) : error ? (
|
||||||
@@ -95,7 +97,7 @@ export default function LiveDetailScreen() {
|
|||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<View style={styles.titleSection}>
|
<View style={styles.titleSection}>
|
||||||
<Text style={styles.title}>{room?.title}</Text>
|
<Text style={[styles.title, { color: theme.text }]}>{room?.title}</Text>
|
||||||
<View style={styles.metaRow}>
|
<View style={styles.metaRow}>
|
||||||
{isLive ? (
|
{isLive ? (
|
||||||
<View style={styles.livePill}>
|
<View style={styles.livePill}>
|
||||||
@@ -126,7 +128,7 @@ export default function LiveDetailScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.divider} />
|
<View style={[styles.divider, { backgroundColor: theme.border }]} />
|
||||||
|
|
||||||
{anchor && (
|
{anchor && (
|
||||||
<View style={styles.anchorRow}>
|
<View style={styles.anchorRow}>
|
||||||
@@ -134,7 +136,7 @@ export default function LiveDetailScreen() {
|
|||||||
source={{ uri: proxyImageUrl(anchor.face) }}
|
source={{ uri: proxyImageUrl(anchor.face) }}
|
||||||
style={styles.avatar}
|
style={styles.avatar}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.anchorName}>{anchor.uname}</Text>
|
<Text style={[styles.anchorName, { color: theme.text }]}>{anchor.uname}</Text>
|
||||||
<TouchableOpacity style={styles.followBtn}>
|
<TouchableOpacity style={styles.followBtn}>
|
||||||
<Text style={styles.followTxt}>+ 关注</Text>
|
<Text style={styles.followTxt}>+ 关注</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -143,7 +145,7 @@ export default function LiveDetailScreen() {
|
|||||||
|
|
||||||
{!!room?.description && (
|
{!!room?.description && (
|
||||||
<View style={styles.descBox}>
|
<View style={styles.descBox}>
|
||||||
<Text style={styles.descText}>{room.description}</Text>
|
<Text style={[styles.descText, { color: theme.text }]}>{room.description}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -166,14 +168,13 @@ export default function LiveDetailScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
safe: { flex: 1, backgroundColor: "#fff" },
|
safe: { flex: 1 },
|
||||||
topBar: {
|
topBar: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: "#eee",
|
|
||||||
},
|
},
|
||||||
backBtn: { padding: 4 },
|
backBtn: { padding: 4 },
|
||||||
topTitle: {
|
topTitle: {
|
||||||
@@ -181,15 +182,12 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
color: "#212121",
|
|
||||||
},
|
},
|
||||||
loader: { marginVertical: 30 },
|
loader: { marginVertical: 30 },
|
||||||
errorText: { textAlign: "center", color: "#f00", padding: 20 },
|
errorText: { textAlign: "center", color: "#f00", padding: 20 },
|
||||||
tabBar: {
|
tabBar: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: "#eee",
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
},
|
},
|
||||||
tabItem: {
|
tabItem: {
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
@@ -197,7 +195,7 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
},
|
},
|
||||||
tabLabel: { fontSize: 14, color: "#999", fontWeight: "500" },
|
tabLabel: { fontSize: 14, fontWeight: "500" },
|
||||||
tabActive: { color: "#00AEEC", fontWeight: "700" },
|
tabActive: { color: "#00AEEC", fontWeight: "700" },
|
||||||
tabUnderline: {
|
tabUnderline: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -212,7 +210,6 @@ const styles = StyleSheet.create({
|
|||||||
title: {
|
title: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
color: "#212121",
|
|
||||||
lineHeight: 22,
|
lineHeight: 22,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
@@ -248,7 +245,6 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
divider: {
|
divider: {
|
||||||
height: StyleSheet.hairlineWidth,
|
height: StyleSheet.hairlineWidth,
|
||||||
backgroundColor: "#f0f0f0",
|
|
||||||
marginHorizontal: 14,
|
marginHorizontal: 14,
|
||||||
},
|
},
|
||||||
anchorRow: {
|
anchorRow: {
|
||||||
@@ -258,7 +254,7 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
},
|
},
|
||||||
avatar: { width: 40, height: 40, borderRadius: 20, marginRight: 10 },
|
avatar: { width: 40, height: 40, borderRadius: 20, marginRight: 10 },
|
||||||
anchorName: { flex: 1, fontSize: 14, color: "#212121", fontWeight: "500" },
|
anchorName: { flex: 1, fontSize: 14, fontWeight: "500" },
|
||||||
followBtn: {
|
followBtn: {
|
||||||
backgroundColor: "#00AEEC",
|
backgroundColor: "#00AEEC",
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
@@ -267,7 +263,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
followTxt: { color: "#fff", fontSize: 12, fontWeight: "600" },
|
followTxt: { color: "#fff", fontSize: 12, fontWeight: "600" },
|
||||||
descBox: { padding: 14, paddingTop: 4 },
|
descBox: { padding: 14, paddingTop: 4 },
|
||||||
descText: { fontSize: 14, color: "#555", lineHeight: 22 },
|
descText: { fontSize: 14, lineHeight: 22 },
|
||||||
danmakuFull: { flex: 1 },
|
danmakuFull: { flex: 1 },
|
||||||
hidden: { display: "none" },
|
hidden: { display: "none" },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ import { useRelatedVideos } from "../../hooks/useRelatedVideos";
|
|||||||
import { formatCount, formatDuration } from "../../utils/format";
|
import { formatCount, formatDuration } from "../../utils/format";
|
||||||
import { proxyImageUrl } from "../../utils/imageUrl";
|
import { proxyImageUrl } from "../../utils/imageUrl";
|
||||||
import { DownloadSheet } from "../../components/DownloadSheet";
|
import { DownloadSheet } from "../../components/DownloadSheet";
|
||||||
|
import { useTheme } from "../../utils/theme";
|
||||||
|
|
||||||
type Tab = "intro" | "comments" | "danmaku";
|
type Tab = "intro" | "comments" | "danmaku";
|
||||||
|
|
||||||
export default function VideoDetailScreen() {
|
export default function VideoDetailScreen() {
|
||||||
const { bvid } = useLocalSearchParams<{ bvid: string }>();
|
const { bvid } = useLocalSearchParams<{ bvid: string }>();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const theme = useTheme();
|
||||||
const {
|
const {
|
||||||
video,
|
video,
|
||||||
playData,
|
playData,
|
||||||
@@ -67,20 +69,20 @@ export default function VideoDetailScreen() {
|
|||||||
}, [video?.cid]);
|
}, [video?.cid]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe}>
|
<SafeAreaView style={[styles.safe, { backgroundColor: theme.card }]}>
|
||||||
{/* TopBar */}
|
{/* TopBar */}
|
||||||
<View style={styles.topBar}>
|
<View style={[styles.topBar, { borderBottomColor: theme.border }]}>
|
||||||
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
||||||
<Ionicons name="chevron-back" size={24} color="#212121" />
|
<Ionicons name="chevron-back" size={24} color={theme.text} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.topTitle} numberOfLines={1}>
|
<Text style={[styles.topTitle, { color: theme.text }]} numberOfLines={1}>
|
||||||
{video?.title ?? "视频详情"}
|
{video?.title ?? "视频详情"}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.miniBtn}
|
style={styles.miniBtn}
|
||||||
onPress={() => setShowDownload(true)}
|
onPress={() => setShowDownload(true)}
|
||||||
>
|
>
|
||||||
<Ionicons name="cloud-download-outline" size={22} color="#212121" />
|
<Ionicons name="cloud-download-outline" size={22} color={theme.text} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -105,15 +107,15 @@ export default function VideoDetailScreen() {
|
|||||||
qualities={qualities}
|
qualities={qualities}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* TabBar — sits directly below player, always visible once video loads */}
|
{/* TabBar */}
|
||||||
{video && (
|
{video && (
|
||||||
<View style={styles.tabBar}>
|
<View style={[styles.tabBar, { borderBottomColor: theme.border }]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.tabItem}
|
style={styles.tabItem}
|
||||||
onPress={() => setTab("intro")}
|
onPress={() => setTab("intro")}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[styles.tabLabel, tab === "intro" && styles.tabActive]}
|
style={[styles.tabLabel, { color: theme.textSub }, tab === "intro" && styles.tabActive]}
|
||||||
>
|
>
|
||||||
简介
|
简介
|
||||||
</Text>
|
</Text>
|
||||||
@@ -124,7 +126,7 @@ export default function VideoDetailScreen() {
|
|||||||
onPress={() => setTab("comments")}
|
onPress={() => setTab("comments")}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[styles.tabLabel, tab === "comments" && styles.tabActive]}
|
style={[styles.tabLabel, { color: theme.textSub }, tab === "comments" && styles.tabActive]}
|
||||||
>
|
>
|
||||||
评论
|
评论
|
||||||
{video.stat?.reply > 0 ? ` ${formatCount(video.stat.reply)}` : ""}
|
{video.stat?.reply > 0 ? ` ${formatCount(video.stat.reply)}` : ""}
|
||||||
@@ -136,7 +138,7 @@ export default function VideoDetailScreen() {
|
|||||||
onPress={() => setTab("danmaku")}
|
onPress={() => setTab("danmaku")}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[styles.tabLabel, tab === "danmaku" && styles.tabActive]}
|
style={[styles.tabLabel, { color: theme.textSub }, tab === "danmaku" && styles.tabActive]}
|
||||||
>
|
>
|
||||||
弹幕
|
弹幕
|
||||||
{danmakus.length > 0 ? ` ${formatCount(danmakus.length)}` : ""}
|
{danmakus.length > 0 ? ` ${formatCount(danmakus.length)}` : ""}
|
||||||
@@ -168,13 +170,13 @@ export default function VideoDetailScreen() {
|
|||||||
source={{ uri: proxyImageUrl(video.owner.face) }}
|
source={{ uri: proxyImageUrl(video.owner.face) }}
|
||||||
style={styles.avatar}
|
style={styles.avatar}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.upName}>{video.owner.name}</Text>
|
<Text style={[styles.upName, { color: theme.text }]}>{video.owner.name}</Text>
|
||||||
<TouchableOpacity style={styles.followBtn}>
|
<TouchableOpacity style={styles.followBtn}>
|
||||||
<Text style={styles.followTxt}>+ 关注</Text>
|
<Text style={styles.followTxt}>+ 关注</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.titleSection}>
|
<View style={[styles.titleSection, { borderBottomColor: theme.border }]}>
|
||||||
<Text style={styles.title}>{video.title}</Text>
|
<Text style={[styles.title, { color: theme.text }]}>{video.title}</Text>
|
||||||
<View style={styles.statsRow}>
|
<View style={styles.statsRow}>
|
||||||
<StatBadge icon="play" count={video.stat.view} />
|
<StatBadge icon="play" count={video.stat.view} />
|
||||||
<StatBadge icon="heart" count={video.stat.like} />
|
<StatBadge icon="heart" count={video.stat.like} />
|
||||||
@@ -192,22 +194,22 @@ export default function VideoDetailScreen() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<View style={styles.descBox}>
|
<View style={styles.descBox}>
|
||||||
<Text style={styles.descText}>
|
<Text style={[styles.descText, { color: theme.text }]}>
|
||||||
{video.desc || "暂无简介"}
|
{video.desc || "暂无简介"}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.relatedHeader}>
|
<View style={[styles.relatedHeader, { backgroundColor: theme.bg, borderTopColor: theme.border }]}>
|
||||||
<Text style={styles.relatedHeaderText}>推荐视频</Text>
|
<Text style={[styles.relatedHeaderText, { color: theme.text }]}>推荐视频</Text>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.relatedCard}
|
style={[styles.relatedCard, { backgroundColor: theme.card, borderBottomColor: theme.border }]}
|
||||||
onPress={() => router.push(`/video/${item.bvid}` as any)}
|
onPress={() => router.push(`/video/${item.bvid}` as any)}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
<View style={styles.relatedThumbWrap}>
|
<View style={[styles.relatedThumbWrap, { backgroundColor: theme.bg }]}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: proxyImageUrl(item.pic) }}
|
source={{ uri: proxyImageUrl(item.pic) }}
|
||||||
style={styles.relatedThumb}
|
style={styles.relatedThumb}
|
||||||
@@ -220,7 +222,7 @@ export default function VideoDetailScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.relatedInfo}>
|
<View style={styles.relatedInfo}>
|
||||||
<Text style={styles.relatedTitle} numberOfLines={2}>
|
<Text style={[styles.relatedTitle, { color: theme.text }]} numberOfLines={2}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
<View
|
<View
|
||||||
@@ -265,7 +267,7 @@ export default function VideoDetailScreen() {
|
|||||||
onEndReachedThreshold={0.3}
|
onEndReachedThreshold={0.3}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
<View style={styles.sortRow}>
|
<View style={[styles.sortRow, { borderBottomColor: theme.border }]}>
|
||||||
<Text style={styles.sortLabel}>排序</Text>
|
<Text style={styles.sortLabel}>排序</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
@@ -352,17 +354,17 @@ function SeasonSection({
|
|||||||
currentBvid: string;
|
currentBvid: string;
|
||||||
onEpisodePress: (bvid: string) => void;
|
onEpisodePress: (bvid: string) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const theme = useTheme();
|
||||||
const episodes = season.sections?.[0]?.episodes ?? [];
|
const episodes = season.sections?.[0]?.episodes ?? [];
|
||||||
const currentIndex = episodes.findIndex((ep) => ep.bvid === currentBvid);
|
const currentIndex = episodes.findIndex((ep) => ep.bvid === currentBvid);
|
||||||
const listRef = useRef<FlatList>(null);
|
const listRef = useRef<FlatList>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentIndex <= 0 || episodes.length === 0) return;
|
if (currentIndex <= 0 || episodes.length === 0) return;
|
||||||
// 等布局完成再滚动
|
|
||||||
const t = setTimeout(() => {
|
const t = setTimeout(() => {
|
||||||
listRef.current?.scrollToIndex({
|
listRef.current?.scrollToIndex({
|
||||||
index: currentIndex,
|
index: currentIndex,
|
||||||
viewPosition: 0.5, // 居中
|
viewPosition: 0.5,
|
||||||
animated: false,
|
animated: false,
|
||||||
});
|
});
|
||||||
}, 200);
|
}, 200);
|
||||||
@@ -370,9 +372,9 @@ function SeasonSection({
|
|||||||
}, [currentIndex, episodes.length]);
|
}, [currentIndex, episodes.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.seasonBox}>
|
<View style={[styles.seasonBox, { borderTopColor: theme.border }]}>
|
||||||
<View style={styles.seasonHeader}>
|
<View style={styles.seasonHeader}>
|
||||||
<Text style={styles.seasonTitle}>合集 · {season.title}</Text>
|
<Text style={[styles.seasonTitle, { color: theme.text }]}>合集 · {season.title}</Text>
|
||||||
<Text style={styles.seasonCount}>{season.ep_count}个视频</Text>
|
<Text style={styles.seasonCount}>{season.ep_count}个视频</Text>
|
||||||
<Ionicons name="chevron-forward" size={14} color="#999" />
|
<Ionicons name="chevron-forward" size={14} color="#999" />
|
||||||
</View>
|
</View>
|
||||||
@@ -383,7 +385,6 @@ function SeasonSection({
|
|||||||
data={episodes}
|
data={episodes}
|
||||||
keyExtractor={(ep) => ep.bvid}
|
keyExtractor={(ep) => ep.bvid}
|
||||||
contentContainerStyle={{ paddingHorizontal: 12, gap: 10 }}
|
contentContainerStyle={{ paddingHorizontal: 12, gap: 10 }}
|
||||||
// 每个卡片宽 120,gap 10,让 FlatList 直接算任意索引的偏移量
|
|
||||||
getItemLayout={(_data, index) => ({
|
getItemLayout={(_data, index) => ({
|
||||||
length: 130,
|
length: 130,
|
||||||
offset: 12 + index * 130,
|
offset: 12 + index * 130,
|
||||||
@@ -394,20 +395,20 @@ function SeasonSection({
|
|||||||
const isCurrent = ep.bvid === currentBvid;
|
const isCurrent = ep.bvid === currentBvid;
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.epCard, isCurrent && styles.epCardActive]}
|
style={[styles.epCard, { backgroundColor: theme.bg }, isCurrent && styles.epCardActive]}
|
||||||
onPress={() => !isCurrent && onEpisodePress(ep.bvid)}
|
onPress={() => !isCurrent && onEpisodePress(ep.bvid)}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
{ep.arc?.pic && (
|
{ep.arc?.pic && (
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: proxyImageUrl(ep.arc.pic) }}
|
source={{ uri: proxyImageUrl(ep.arc.pic) }}
|
||||||
style={styles.epThumb}
|
style={[styles.epThumb, { backgroundColor: theme.card }]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Text style={[styles.epNum, isCurrent && styles.epNumActive]}>
|
<Text style={[styles.epNum, isCurrent && styles.epNumActive]}>
|
||||||
第{index + 1}集
|
第{index + 1}集
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.epTitle} numberOfLines={2}>
|
<Text style={[styles.epTitle, { color: theme.text }]} numberOfLines={2}>
|
||||||
{ep.title}
|
{ep.title}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -419,14 +420,13 @@ function SeasonSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
safe: { flex: 1, backgroundColor: "#fff" },
|
safe: { flex: 1 },
|
||||||
topBar: {
|
topBar: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: "#eee",
|
|
||||||
},
|
},
|
||||||
backBtn: { padding: 4 },
|
backBtn: { padding: 4 },
|
||||||
topTitle: {
|
topTitle: {
|
||||||
@@ -434,19 +434,16 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
color: "#212121",
|
|
||||||
},
|
},
|
||||||
miniBtn: { padding: 4 },
|
miniBtn: { padding: 4 },
|
||||||
loader: { marginVertical: 30 },
|
loader: { marginVertical: 30 },
|
||||||
titleSection: {
|
titleSection: {
|
||||||
padding: 14,
|
padding: 14,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: "#f0f0f0",
|
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
color: "#212121",
|
|
||||||
lineHeight: 22,
|
lineHeight: 22,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
@@ -460,8 +457,8 @@ const styles = StyleSheet.create({
|
|||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
paddingTop: 12,
|
paddingTop: 12,
|
||||||
},
|
},
|
||||||
avatar: { width: 48, height: 48, borderRadius:30, marginRight: 10 },
|
avatar: { width: 48, height: 48, borderRadius: 30, marginRight: 10 },
|
||||||
upName: { flex: 1, fontSize: 14, color: "#212121", fontWeight: "500" },
|
upName: { flex: 1, fontSize: 14, fontWeight: "500" },
|
||||||
followBtn: {
|
followBtn: {
|
||||||
backgroundColor: "#00AEEC",
|
backgroundColor: "#00AEEC",
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
@@ -471,7 +468,6 @@ const styles = StyleSheet.create({
|
|||||||
followTxt: { color: "#fff", fontSize: 12, fontWeight: "500" },
|
followTxt: { color: "#fff", fontSize: 12, fontWeight: "500" },
|
||||||
seasonBox: {
|
seasonBox: {
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
borderTopColor: "#f0f0f0",
|
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
},
|
},
|
||||||
seasonHeader: {
|
seasonHeader: {
|
||||||
@@ -481,23 +477,21 @@ const styles = StyleSheet.create({
|
|||||||
paddingBottom: 8,
|
paddingBottom: 8,
|
||||||
gap: 4,
|
gap: 4,
|
||||||
},
|
},
|
||||||
seasonTitle: { flex: 1, fontSize: 13, fontWeight: "600", color: "#212121" },
|
seasonTitle: { flex: 1, fontSize: 13, fontWeight: "600" },
|
||||||
seasonCount: { fontSize: 12, color: "#999" },
|
seasonCount: { fontSize: 12, color: "#999" },
|
||||||
epCard: {
|
epCard: {
|
||||||
width: 120,
|
width: 120,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
backgroundColor: "#f8f8f8",
|
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
borderColor: "transparent",
|
borderColor: "transparent",
|
||||||
},
|
},
|
||||||
epCardActive: { borderColor: "#00AEEC" },
|
epCardActive: { borderColor: "#00AEEC" },
|
||||||
epThumb: { width: 120, height: 68, backgroundColor: "#eee" },
|
epThumb: { width: 120, height: 68 },
|
||||||
epNum: { fontSize: 11, color: "#999", paddingHorizontal: 6, paddingTop: 4 },
|
epNum: { fontSize: 11, color: "#999", paddingHorizontal: 6, paddingTop: 4 },
|
||||||
epNumActive: { color: "#00AEEC", fontWeight: "600" },
|
epNumActive: { color: "#00AEEC", fontWeight: "600" },
|
||||||
epTitle: {
|
epTitle: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: "#333",
|
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 6,
|
||||||
paddingBottom: 6,
|
paddingBottom: 6,
|
||||||
lineHeight: 16,
|
lineHeight: 16,
|
||||||
@@ -505,7 +499,6 @@ const styles = StyleSheet.create({
|
|||||||
tabBar: {
|
tabBar: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: "#eee",
|
|
||||||
},
|
},
|
||||||
tabItem: {
|
tabItem: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -513,7 +506,7 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
},
|
},
|
||||||
tabLabel: { fontSize: 13, color: "#999" },
|
tabLabel: { fontSize: 13 },
|
||||||
tabActive: { color: "#00AEEC" },
|
tabActive: { color: "#00AEEC" },
|
||||||
tabUnderline: {
|
tabUnderline: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -525,28 +518,23 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
tabScroll: { flex: 1 },
|
tabScroll: { flex: 1 },
|
||||||
descBox: { padding: 16 },
|
descBox: { padding: 16 },
|
||||||
descText: { fontSize: 14, color: "#555", lineHeight: 22 },
|
descText: { fontSize: 14, lineHeight: 22 },
|
||||||
danmakuTab: { flex: 1 },
|
danmakuTab: { flex: 1 },
|
||||||
emptyTxt: { textAlign: "center", color: "#bbb", padding: 30 },
|
emptyTxt: { textAlign: "center", color: "#bbb", padding: 30 },
|
||||||
relatedHeader: {
|
relatedHeader: {
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
borderTopColor: "#f0f0f0",
|
|
||||||
backgroundColor: "#f4f4f4",
|
|
||||||
},
|
},
|
||||||
relatedHeaderText: {
|
relatedHeaderText: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: "600" as const,
|
fontWeight: "600" as const,
|
||||||
color: "#212121",
|
|
||||||
},
|
},
|
||||||
relatedCard: {
|
relatedCard: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
backgroundColor: "#fff",
|
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: "#f0f0f0",
|
|
||||||
gap: 10,
|
gap: 10,
|
||||||
},
|
},
|
||||||
relatedThumbWrap: {
|
relatedThumbWrap: {
|
||||||
@@ -555,7 +543,6 @@ const styles = StyleSheet.create({
|
|||||||
height: 68,
|
height: 68,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
backgroundColor: "#eee",
|
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
relatedThumb: { width: 120, height: 68 },
|
relatedThumb: { width: 120, height: 68 },
|
||||||
@@ -574,7 +561,7 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
paddingVertical: 2,
|
paddingVertical: 2,
|
||||||
},
|
},
|
||||||
relatedTitle: { fontSize: 13, color: "#212121", lineHeight: 18 },
|
relatedTitle: { fontSize: 13, lineHeight: 18 },
|
||||||
relatedOwner: { fontSize: 12, color: "#999" },
|
relatedOwner: { fontSize: 12, color: "#999" },
|
||||||
relatedView: { fontSize: 11, color: "#bbb" },
|
relatedView: { fontSize: 11, color: "#bbb" },
|
||||||
sortRow: {
|
sortRow: {
|
||||||
@@ -584,7 +571,6 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
gap: 8,
|
gap: 8,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: "#f0f0f0",
|
|
||||||
},
|
},
|
||||||
sortLabel: { fontSize: 13, color: "#999", marginRight: 4 },
|
sortLabel: { fontSize: 13, color: "#999", marginRight: 4 },
|
||||||
sortBtn: {
|
sortBtn: {
|
||||||
|
|||||||
Reference in New Issue
Block a user