直播弹幕与界面优化

- useLiveDanmaku: 重写协议解析,支持 zlib 压缩消息、token 认证、base64 帧处理
- services/bilibili: 新增 getLiveDanmakuInfo 获取弹幕服务器信息
- live/[roomId]: 使用真实 roomid 而非 URL 别名连接弹幕
- NativeVideoPlayer: 增加中文注释,移除调试日志
- video/[bvid]: UP主头像移至标题前,调整字号与样式
- index: 替换 logo 文字为下载按钮图标
- videoRows: 修复奇数视频配对,调整 BigRow 位置策略
- package.json: 移除未使用的 fast-xml-parser 和 xml2js 依赖
This commit is contained in:
Developer
2026-03-17 15:14:23 +08:00
parent a46e63f0ba
commit 6275fd0930
8 changed files with 316 additions and 155 deletions

View File

@@ -432,7 +432,9 @@ export default function HomeScreen() {
<Ionicons name="search" size={14} color="#999" />
<Text style={styles.searchPlaceholder}>UP主...</Text>
</TouchableOpacity>
<Text style={styles.logo}></Text>
<TouchableOpacity style={styles.downloadBtn} activeOpacity={0.7}>
<Ionicons name="cloud-download-outline" size={24} color="#999" />
</TouchableOpacity>
</Animated.View>
<View style={styles.tabRow}>
@@ -506,6 +508,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 10,
gap: 5,
},
downloadBtn: {},
searchPlaceholder: {
fontSize: 13,
color: "#999",

View File

@@ -30,8 +30,9 @@ export default function LiveDetailScreen() {
const qualities = stream?.qualities ?? [];
const currentQn = stream?.qn ?? 0;
const danmakus = useLiveDanmaku(isLive ? id : 0);
// Use actual roomid from room detail (not the short/alias ID from the URL)
const actualRoomId = room?.roomid ?? id;
const danmakus = useLiveDanmaku(isLive ? actualRoomId : 0);
return (
<SafeAreaView style={styles.safe}>
{/* TopBar */}

View File

@@ -99,21 +99,38 @@ export default function VideoDetailScreen() {
{/* TabBar — sits directly below player, always visible once video loads */}
{video && (
<View style={styles.tabBar}>
<TouchableOpacity style={styles.tabItem} onPress={() => setTab("intro")}>
<Text style={[styles.tabLabel, tab === "intro" && styles.tabActive]}>
<TouchableOpacity
style={styles.tabItem}
onPress={() => setTab("intro")}
>
<Text
style={[styles.tabLabel, tab === "intro" && styles.tabActive]}
>
</Text>
{tab === "intro" && <View style={styles.tabUnderline} />}
</TouchableOpacity>
<TouchableOpacity style={styles.tabItem} onPress={() => setTab("comments")}>
<Text style={[styles.tabLabel, tab === "comments" && styles.tabActive]}>
{video.stat?.reply > 0 ? ` ${formatCount(video.stat.reply)}` : ""}
<TouchableOpacity
style={styles.tabItem}
onPress={() => setTab("comments")}
>
<Text
style={[styles.tabLabel, tab === "comments" && styles.tabActive]}
>
{video.stat?.reply > 0 ? ` ${formatCount(video.stat.reply)}` : ""}
</Text>
{tab === "comments" && <View style={styles.tabUnderline} />}
</TouchableOpacity>
<TouchableOpacity style={styles.tabItem} onPress={() => setTab("danmaku")}>
<Text style={[styles.tabLabel, tab === "danmaku" && styles.tabActive]}>
{danmakus.length > 0 ? ` ${formatCount(danmakus.length)}` : ""}
<TouchableOpacity
style={styles.tabItem}
onPress={() => setTab("danmaku")}
>
<Text
style={[styles.tabLabel, tab === "danmaku" && styles.tabActive]}
>
{danmakus.length > 0 ? ` ${formatCount(danmakus.length)}` : ""}
</Text>
{tab === "danmaku" && <View style={styles.tabUnderline} />}
</TouchableOpacity>
@@ -126,16 +143,10 @@ export default function VideoDetailScreen() {
) : video ? (
tab === "intro" ? (
// 简介:视频信息 + 合集 + 简介文本
<ScrollView style={styles.tabScroll} showsVerticalScrollIndicator={false}>
<View style={styles.titleSection}>
<Text style={styles.title}>{video.title}</Text>
<View style={styles.statsRow}>
<StatBadge icon="play" count={video.stat.view} />
<StatBadge icon="heart" count={video.stat.like} />
<StatBadge icon="star" count={video.stat.favorite} />
<StatBadge icon="chatbubble" count={video.stat.reply} />
</View>
</View>
<ScrollView
style={styles.tabScroll}
showsVerticalScrollIndicator={false}
>
<View style={styles.upRow}>
<Image
source={{ uri: proxyImageUrl(video.owner.face) }}
@@ -146,6 +157,15 @@ export default function VideoDetailScreen() {
<Text style={styles.followTxt}>+ </Text>
</TouchableOpacity>
</View>
<View style={styles.titleSection}>
<Text style={styles.title}>{video.title}</Text>
<View style={styles.statsRow}>
<StatBadge icon="play" count={video.stat.view} />
<StatBadge icon="heart" count={video.stat.like} />
<StatBadge icon="star" count={video.stat.favorite} />
<StatBadge icon="chatbubble" count={video.stat.reply} />
</View>
</View>
{video.ugc_season && (
<SeasonSection
season={video.ugc_season}
@@ -171,23 +191,45 @@ export default function VideoDetailScreen() {
data={comments}
keyExtractor={(c) => String(c.rpid)}
renderItem={({ item }) => <CommentItem item={item} />}
onEndReached={() => { if (cmtHasMore && !cmtLoading) loadComments(); }}
onEndReached={() => {
if (cmtHasMore && !cmtLoading) loadComments();
}}
onEndReachedThreshold={0.3}
showsVerticalScrollIndicator={false}
ListHeaderComponent={
<View style={styles.sortRow}>
<Text style={styles.sortLabel}></Text>
<TouchableOpacity
style={[styles.sortBtn, commentSort === 2 && styles.sortBtnActive]}
style={[
styles.sortBtn,
commentSort === 2 && styles.sortBtnActive,
]}
onPress={() => setCommentSort(2)}
>
<Text style={[styles.sortBtnTxt, commentSort === 2 && styles.sortBtnTxtActive]}></Text>
<Text
style={[
styles.sortBtnTxt,
commentSort === 2 && styles.sortBtnTxtActive,
]}
>
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.sortBtn, commentSort === 0 && styles.sortBtnActive]}
style={[
styles.sortBtn,
commentSort === 0 && styles.sortBtnActive,
]}
onPress={() => setCommentSort(0)}
>
<Text style={[styles.sortBtnTxt, commentSort === 0 && styles.sortBtnTxtActive]}></Text>
<Text
style={[
styles.sortBtnTxt,
commentSort === 0 && styles.sortBtnTxtActive,
]}
>
</Text>
</TouchableOpacity>
</View>
}
@@ -312,9 +354,13 @@ const styles = StyleSheet.create({
},
miniBtn: { padding: 4 },
loader: { marginVertical: 30 },
titleSection: { padding: 14 },
titleSection: {
padding: 14,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: "#f0f0f0",
},
title: {
fontSize: 16,
fontSize: 13,
fontWeight: "600",
color: "#212121",
lineHeight: 22,
@@ -326,20 +372,19 @@ const styles = StyleSheet.create({
upRow: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 14,
paddingBottom: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: "#f0f0f0",
paddingHorizontal: 10,
paddingBottom: 0,
paddingTop: 12,
},
avatar: { width: 38, height: 38, borderRadius: 19, marginRight: 10 },
avatar: { width: 48, height: 48, borderRadius: 19, marginRight: 10 },
upName: { flex: 1, fontSize: 14, color: "#212121", fontWeight: "500" },
followBtn: {
backgroundColor: "#00AEEC",
paddingHorizontal: 14,
paddingVertical: 5,
paddingHorizontal: 10,
paddingVertical: 3,
borderRadius: 14,
},
followTxt: { color: "#fff", fontSize: 12, fontWeight: "600" },
followTxt: { color: "#fff", fontSize: 12, fontWeight: "500" },
seasonBox: {
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: "#f0f0f0",
@@ -384,8 +429,8 @@ const styles = StyleSheet.create({
paddingVertical: 12,
position: "relative",
},
tabLabel: { fontSize: 14, color: "#999" },
tabActive: { color: "#00AEEC", fontWeight: "700" },
tabLabel: { fontSize: 13, color: "#999" },
tabActive: { color: "#00AEEC" },
tabUnderline: {
position: "absolute",
bottom: 0,