feat: 视频推荐关联当前视频、UP主信息展示、直播全屏修复及UI优化

- 推荐列表改用 getVideoRelated 基于当前 bvid 推荐,与首页 feed 解耦
- 视频详情页 UP主名称下方展示粉丝数和视频数
- 推荐视频点击改为 router.replace 避免页面堆叠
- 直播全屏改用 position:absolute,解决退出全屏视频重建暂停问题
- 退出全屏时直播自动暂停
- 直播画质选中状态修复,过滤杜比/4K 画质选项
- 直播画质面板改为居中 Modal 弹出框
- 视频详情 Tab 按钮左对齐,评论排序及设置页按钮统一实心背景风格
This commit is contained in:
Developer
2026-03-25 12:03:28 +08:00
parent 7870c83b12
commit e1fa8cc347
8 changed files with 340 additions and 227 deletions

View File

@@ -211,58 +211,81 @@ export default function DanmakuList({
}, []);
// ─── Live mode render (B站-style chat) ─────────────────────────────────────
const renderLiveItem = useCallback(({ item }: { item: DisplayedDanmaku }) => {
const guard = item.guardLevel ? GUARD_LABELS[item.guardLevel] : null;
const timeStr = formatLiveTime(item.timeline);
return (
<Animated.View style={[liveStyles.row, { opacity: item._fadeAnim, borderBottomColor: theme.border }]}>
{timeStr ? (
<Text style={liveStyles.time}>{timeStr}</Text>
) : null}
<View style={liveStyles.msgBody}>
{guard && (
<View style={[liveStyles.guardTag, { backgroundColor: guard.color }]}>
<Text style={liveStyles.guardTagText}>{guard.text}</Text>
</View>
)}
{item.isAdmin && (
<View style={[liveStyles.guardTag, { backgroundColor: "#e53935" }]}>
<Text style={liveStyles.guardTagText}></Text>
</View>
)}
{item.medalLevel != null && item.medalName && (
<View style={liveStyles.medalTag}>
<Text style={liveStyles.medalName}>{item.medalName}</Text>
<View style={liveStyles.medalLvBox}>
<Text style={liveStyles.medalLv}>{item.medalLevel}</Text>
const renderLiveItem = useCallback(
({ item }: { item: DisplayedDanmaku }) => {
const guard = item.guardLevel ? GUARD_LABELS[item.guardLevel] : null;
const timeStr = formatLiveTime(item.timeline);
return (
<Animated.View
style={[
liveStyles.row,
{ opacity: item._fadeAnim, borderBottomColor: theme.border },
]}
>
{timeStr ? <Text style={liveStyles.time}>{timeStr}</Text> : null}
<View style={liveStyles.msgBody}>
{guard && (
<View
style={[liveStyles.guardTag, { backgroundColor: guard.color }]}
>
<Text style={liveStyles.guardTagText}>{guard.text}</Text>
</View>
</View>
)}
<Text style={liveStyles.uname} numberOfLines={1}>
{item.uname ?? "匿名"}
</Text>
<Text style={liveStyles.colon}></Text>
<Text style={[liveStyles.text, { color: theme.text }]} numberOfLines={2}>
{item.text}
</Text>
</View>
</Animated.View>
);
}, [theme]);
)}
{item.isAdmin && (
<View
style={[liveStyles.guardTag, { backgroundColor: "#e53935" }]}
>
<Text style={liveStyles.guardTagText}></Text>
</View>
)}
{item.medalLevel != null && item.medalName && (
<View style={liveStyles.medalTag}>
<Text style={liveStyles.medalName}>{item.medalName}</Text>
<View style={liveStyles.medalLvBox}>
<Text style={liveStyles.medalLv}>{item.medalLevel}</Text>
</View>
</View>
)}
<Text style={liveStyles.uname} numberOfLines={1}>
{item.uname ?? "匿名"}
</Text>
<Text style={liveStyles.colon}></Text>
<Text
style={[liveStyles.text, { color: theme.text }]}
numberOfLines={2}
>
{item.text}
</Text>
</View>
</Animated.View>
);
},
[theme],
);
// ─── Video mode render (original bubble) ───────────────────────────────────
const renderVideoItem = useCallback(({ item }: { item: DisplayedDanmaku }) => {
const dotColor = danmakuColorToCss(item.color);
return (
<Animated.View style={[styles.bubble, { opacity: item._fadeAnim, backgroundColor: theme.bg }]}>
<View style={[styles.colorDot, { backgroundColor: dotColor }]} />
<Text style={[styles.bubbleText, { color: theme.text }]} numberOfLines={3}>
{item.text}
</Text>
<Text style={styles.timestamp}>{formatTimestamp(item.time)}</Text>
</Animated.View>
);
}, [theme]);
const renderVideoItem = useCallback(
({ item }: { item: DisplayedDanmaku }) => {
const dotColor = danmakuColorToCss(item.color);
return (
<Animated.View
style={[
styles.bubble,
{ opacity: item._fadeAnim, backgroundColor: theme.bg },
]}
>
<Text
style={[styles.bubbleText, { color: dotColor }]}
numberOfLines={3}
>
{item.text}
</Text>
<Text style={styles.timestamp}>{formatTimestamp(item.time)}</Text>
</Animated.View>
);
},
[theme],
);
const keyExtractor = useCallback(
(item: DisplayedDanmaku) => String(item._key),
@@ -270,7 +293,13 @@ export default function DanmakuList({
);
return (
<View style={[styles.container, { backgroundColor: theme.card, borderTopColor: theme.border }, style]}>
<View
style={[
styles.container,
{ backgroundColor: theme.card, borderTopColor: theme.border },
style,
]}
>
{!hideHeader && (
<TouchableOpacity
style={styles.header}
@@ -300,8 +329,13 @@ export default function DanmakuList({
data={displayedItems}
keyExtractor={keyExtractor}
renderItem={isLive ? renderLiveItem : renderVideoItem}
style={[isLive ? liveStyles.list : styles.list, { backgroundColor: theme.bg }]}
contentContainerStyle={isLive ? liveStyles.listContent : styles.listContent}
style={[
isLive ? liveStyles.list : styles.list,
{ backgroundColor: theme.card },
]}
contentContainerStyle={
isLive ? liveStyles.listContent : styles.listContent
}
onScroll={handleScroll}
onScrollBeginDrag={handleScrollBeginDrag}
scrollEventThrottle={16}
@@ -323,7 +357,6 @@ export default function DanmakuList({
)}
</View>
)}
</View>
);
}
@@ -370,13 +403,6 @@ const styles = StyleSheet.create({
marginVertical: 2,
gap: 8,
},
colorDot: {
width: 6,
height: 6,
borderRadius: 3,
marginTop: 6,
flexShrink: 0,
},
bubbleText: {
flex: 1,
fontSize: 13,
@@ -425,8 +451,6 @@ const liveStyles = StyleSheet.create({
flexDirection: "row",
alignItems: "flex-start",
paddingVertical: 5,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: "#f0f0f2",
},
time: {
fontSize: 10,