Skip to content

Commit 80109b9

Browse files
author
Developer
committed
feat: 视频推荐关联当前视频、UP主信息展示、直播全屏修复及UI优化
- 推荐列表改用 getVideoRelated 基于当前 bvid 推荐,与首页 feed 解耦 - 视频详情页 UP主名称下方展示粉丝数和视频数 - 推荐视频点击改为 router.replace 避免页面堆叠 - 直播全屏改用 position:absolute,解决退出全屏视频重建暂停问题 - 退出全屏时直播自动暂停 - 直播画质选中状态修复,过滤杜比/4K 画质选项 - 直播画质面板改为居中 Modal 弹出框 - 视频详情 Tab 按钮左对齐,评论排序及设置页按钮统一实心背景风格
1 parent e508893 commit 80109b9

8 files changed

Lines changed: 340 additions & 227 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,25 @@
55

66
---
77

8-
## [1.0.8] - 2026-03-24
8+
## [1.0.12] - 2026-03-25
9+
10+
### 新增
11+
- **UP主信息**:视频详情页博主名称下方展示粉丝数和视频数(`getUploaderStat``/x/web-interface/card`
12+
- **视频相关推荐**:详情页推荐列表改为基于当前视频(`getVideoRelated``/x/web-interface/archive/related`),不再与首页 feed 共用
13+
14+
### 修复
15+
- **直播全屏退出暂停**:全屏改用 `position:absolute` 覆盖,Video 组件始终在同一棵 React 树中,不再因 Modal 切换导致重建暂停;退出全屏时直播自动暂停
16+
- **直播画质选中**`changeQuality` 强制用请求的 `qn` 覆盖服务端协商值,画质面板高亮与用户选择一致
17+
- **直播画质过滤**:过滤 `qn > 10000` 的选项(杜比/4K),最高仅展示原画
18+
- **推荐视频导航**:点击推荐列表改用 `router.replace`,避免详情页无限堆叠
19+
20+
### 优化
21+
- **直播画质面板**:改为居中 Modal 弹出框
22+
- **视频详情 Tab**:按钮向左靠齐,移除均分宽度
23+
- **评论排序按钮**:统一为实心背景风格(`#f0f0f0``#00AEEC`),与直播分区 Tab 一致
24+
- **设置页按钮**:外观/流量选项按钮统一为实心背景风格
25+
26+
## [1.0.11] - 2026-03-24
927

1028
### 新增
1129
- **暗黑模式**:全局主题系统(`utils/theme.ts`),支持亮色 / 暗色一键切换,覆盖所有页面和组件

app/settings.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,16 +140,14 @@ const styles = StyleSheet.create({
140140
sectionLabel: { fontSize: 13, marginBottom: 10 },
141141
optionRow: { flexDirection: 'row', gap: 10 },
142142
option: {
143-
paddingHorizontal: 20,
144-
paddingVertical: 6,
145-
borderRadius: 20,
146-
borderWidth: 1,
147-
borderColor: '#e0e0e0',
148-
backgroundColor: 'transparent',
143+
paddingHorizontal: 10,
144+
paddingVertical: 2,
145+
borderRadius: 16,
146+
backgroundColor: '#f0f0f0',
149147
},
150-
optionActive: { borderColor: '#00AEEC', backgroundColor: '#e8f7fd' },
151-
optionText: { fontSize: 14, color: '#666' },
152-
optionTextActive: { color: '#00AEEC', fontWeight: '600' },
148+
optionActive: { backgroundColor: '#00AEEC' },
149+
optionText: { fontSize: 13, color: '#333', fontWeight: '500' },
150+
optionTextActive: { color: '#fff', fontWeight: '600' },
153151
hint: { fontSize: 12, marginTop: 8 },
154152
versionRow: {
155153
flexDirection: 'row',

app/video/[bvid].tsx

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useLocalSearchParams, useRouter } from "expo-router";
1313
import { Ionicons } from "@expo/vector-icons";
1414
import { VideoPlayer } from "../../components/VideoPlayer";
1515
import { CommentItem } from "../../components/CommentItem";
16-
import { getDanmaku } from "../../services/bilibili";
16+
import { getDanmaku, getUploaderStat } from "../../services/bilibili";
1717
import { DanmakuItem } from "../../services/types";
1818
import DanmakuList from "../../components/DanmakuList";
1919
import { useVideoDetail } from "../../hooks/useVideoDetail";
@@ -49,11 +49,12 @@ export default function VideoDetailScreen() {
4949
const [danmakus, setDanmakus] = useState<DanmakuItem[]>([]);
5050
const [currentTime, setCurrentTime] = useState(0);
5151
const [showDownload, setShowDownload] = useState(false);
52+
const [uploaderStat, setUploaderStat] = useState<{ follower: number; archiveCount: number } | null>(null);
5253
const {
5354
videos: relatedVideos,
5455
loading: relatedLoading,
5556
load: loadRelated,
56-
} = useRelatedVideos();
57+
} = useRelatedVideos(bvid as string);
5758

5859
useEffect(() => {
5960
loadRelated();
@@ -68,6 +69,11 @@ export default function VideoDetailScreen() {
6869
getDanmaku(video.cid).then(setDanmakus);
6970
}, [video?.cid]);
7071

72+
useEffect(() => {
73+
if (!video?.owner?.mid) return;
74+
getUploaderStat(video.owner.mid).then(setUploaderStat).catch(() => {});
75+
}, [video?.owner?.mid]);
76+
7177
return (
7278
<SafeAreaView style={[styles.safe, { backgroundColor: theme.card }]}>
7379
{/* TopBar */}
@@ -178,20 +184,23 @@ export default function VideoDetailScreen() {
178184
data={relatedVideos}
179185
keyExtractor={(item) => item.bvid}
180186
showsVerticalScrollIndicator={false}
181-
onEndReached={() => {
182-
if (!relatedLoading) loadRelated();
183-
}}
184-
onEndReachedThreshold={0.5}
185187
ListHeaderComponent={
186188
<>
187189
<View style={styles.upRow}>
188190
<Image
189191
source={{ uri: proxyImageUrl(video.owner.face) }}
190192
style={styles.avatar}
191193
/>
192-
<Text style={[styles.upName, { color: theme.text }]}>
193-
{video.owner.name}
194-
</Text>
194+
<View style={styles.upInfo}>
195+
<Text style={[styles.upName, { color: theme.text }]}>
196+
{video.owner.name}
197+
</Text>
198+
{uploaderStat && (
199+
<Text style={styles.upStat}>
200+
{formatCount(uploaderStat.follower)}粉丝 · {formatCount(uploaderStat.archiveCount)}视频
201+
</Text>
202+
)}
203+
</View>
195204
<TouchableOpacity style={styles.followBtn}>
196205
<Text style={styles.followTxt}>+ 关注</Text>
197206
</TouchableOpacity>
@@ -249,7 +258,7 @@ export default function VideoDetailScreen() {
249258
borderBottomColor: theme.border,
250259
},
251260
]}
252-
onPress={() => router.push(`/video/${item.bvid}` as any)}
261+
onPress={() => router.replace(`/video/${item.bvid}` as any)}
253262
activeOpacity={0.85}
254263
>
255264
<View
@@ -321,7 +330,6 @@ export default function VideoDetailScreen() {
321330
<View
322331
style={[styles.sortRow, { borderBottomColor: theme.border }]}
323332
>
324-
<Text style={styles.sortLabel}>排序</Text>
325333
<TouchableOpacity
326334
style={[
327335
styles.sortBtn,
@@ -447,7 +455,7 @@ function SeasonSection({
447455
contentContainerStyle={{ paddingHorizontal: 12, gap: 10 }}
448456
getItemLayout={(_data, index) => ({
449457
length: 130,
450-
offset: 12 + index * 140,
458+
offset: 12 + index * 130,
451459
index,
452460
})}
453461
onScrollToIndexFailed={() => {}}
@@ -528,8 +536,10 @@ const styles = StyleSheet.create({
528536
paddingBottom: 0,
529537
paddingTop: 12,
530538
},
531-
avatar: { width: 48, height: 48, borderRadius: 30, marginRight: 10 },
532-
upName: { flex: 1, fontSize: 14, fontWeight: "500" },
539+
avatar: { width: 40, height: 40, borderRadius: 30, marginRight: 10 },
540+
upInfo: { flex: 1, justifyContent: "center" },
541+
upName: { fontSize: 14, fontWeight: "500" },
542+
upStat: { fontSize: 11, color: "#999", marginTop: 2 },
533543
followBtn: {
534544
backgroundColor: "#00AEEC",
535545
paddingHorizontal: 10,
@@ -570,11 +580,12 @@ const styles = StyleSheet.create({
570580
tabBar: {
571581
flexDirection: "row",
572582
borderBottomWidth: StyleSheet.hairlineWidth,
583+
paddingLeft: 3,
573584
},
574585
tabItem: {
575-
flex: 1,
576586
alignItems: "center",
577587
paddingVertical: 12,
588+
paddingHorizontal: 12,
578589
position: "relative",
579590
},
580591
tabLabel: { fontSize: 13 },
@@ -593,7 +604,7 @@ const styles = StyleSheet.create({
593604
danmakuTab: { flex: 1 },
594605
emptyTxt: { textAlign: "center", color: "#bbb", padding: 30 },
595606
relatedHeader: {
596-
paddingLeft: 12,
607+
paddingLeft: 13,
597608
paddingBottom: 8,
598609
paddingTop: 8,
599610
},
@@ -643,15 +654,13 @@ const styles = StyleSheet.create({
643654
gap: 8,
644655
borderBottomWidth: StyleSheet.hairlineWidth,
645656
},
646-
sortLabel: { fontSize: 13, color: "#999", marginRight: 4 },
647657
sortBtn: {
648-
paddingHorizontal: 14,
649-
paddingVertical: 3,
650-
borderRadius: 20,
651-
borderWidth: 1,
652-
borderColor: "#e0e0e0",
658+
paddingHorizontal: 10,
659+
paddingVertical: 2,
660+
borderRadius: 16,
661+
backgroundColor: "#f0f0f0",
653662
},
654-
sortBtnActive: { borderColor: "#00AEEC", backgroundColor: "#e8f7fd" },
655-
sortBtnTxt: { fontSize: 12, color: "#666" },
656-
sortBtnTxtActive: { color: "#00AEEC", fontWeight: "600" as const },
663+
sortBtnActive: { backgroundColor: "#00AEEC" },
664+
sortBtnTxt: { fontSize: 13, color: "#333", fontWeight: "500" },
665+
sortBtnTxtActive: { color: "#fff", fontWeight: "600" as const },
657666
});

components/DanmakuList.tsx

Lines changed: 86 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -211,66 +211,95 @@ export default function DanmakuList({
211211
}, []);
212212

213213
// ─── Live mode render (B站-style chat) ─────────────────────────────────────
214-
const renderLiveItem = useCallback(({ item }: { item: DisplayedDanmaku }) => {
215-
const guard = item.guardLevel ? GUARD_LABELS[item.guardLevel] : null;
216-
const timeStr = formatLiveTime(item.timeline);
217-
return (
218-
<Animated.View style={[liveStyles.row, { opacity: item._fadeAnim, borderBottomColor: theme.border }]}>
219-
{timeStr ? (
220-
<Text style={liveStyles.time}>{timeStr}</Text>
221-
) : null}
222-
<View style={liveStyles.msgBody}>
223-
{guard && (
224-
<View style={[liveStyles.guardTag, { backgroundColor: guard.color }]}>
225-
<Text style={liveStyles.guardTagText}>{guard.text}</Text>
226-
</View>
227-
)}
228-
{item.isAdmin && (
229-
<View style={[liveStyles.guardTag, { backgroundColor: "#e53935" }]}>
230-
<Text style={liveStyles.guardTagText}>房管</Text>
231-
</View>
232-
)}
233-
{item.medalLevel != null && item.medalName && (
234-
<View style={liveStyles.medalTag}>
235-
<Text style={liveStyles.medalName}>{item.medalName}</Text>
236-
<View style={liveStyles.medalLvBox}>
237-
<Text style={liveStyles.medalLv}>{item.medalLevel}</Text>
214+
const renderLiveItem = useCallback(
215+
({ item }: { item: DisplayedDanmaku }) => {
216+
const guard = item.guardLevel ? GUARD_LABELS[item.guardLevel] : null;
217+
const timeStr = formatLiveTime(item.timeline);
218+
return (
219+
<Animated.View
220+
style={[
221+
liveStyles.row,
222+
{ opacity: item._fadeAnim, borderBottomColor: theme.border },
223+
]}
224+
>
225+
{timeStr ? <Text style={liveStyles.time}>{timeStr}</Text> : null}
226+
<View style={liveStyles.msgBody}>
227+
{guard && (
228+
<View
229+
style={[liveStyles.guardTag, { backgroundColor: guard.color }]}
230+
>
231+
<Text style={liveStyles.guardTagText}>{guard.text}</Text>
238232
</View>
239-
</View>
240-
)}
241-
<Text style={liveStyles.uname} numberOfLines={1}>
242-
{item.uname ?? "匿名"}
243-
</Text>
244-
<Text style={liveStyles.colon}></Text>
245-
<Text style={[liveStyles.text, { color: theme.text }]} numberOfLines={2}>
246-
{item.text}
247-
</Text>
248-
</View>
249-
</Animated.View>
250-
);
251-
}, [theme]);
233+
)}
234+
{item.isAdmin && (
235+
<View
236+
style={[liveStyles.guardTag, { backgroundColor: "#e53935" }]}
237+
>
238+
<Text style={liveStyles.guardTagText}>房管</Text>
239+
</View>
240+
)}
241+
{item.medalLevel != null && item.medalName && (
242+
<View style={liveStyles.medalTag}>
243+
<Text style={liveStyles.medalName}>{item.medalName}</Text>
244+
<View style={liveStyles.medalLvBox}>
245+
<Text style={liveStyles.medalLv}>{item.medalLevel}</Text>
246+
</View>
247+
</View>
248+
)}
249+
<Text style={liveStyles.uname} numberOfLines={1}>
250+
{item.uname ?? "匿名"}
251+
</Text>
252+
<Text style={liveStyles.colon}></Text>
253+
<Text
254+
style={[liveStyles.text, { color: theme.text }]}
255+
numberOfLines={2}
256+
>
257+
{item.text}
258+
</Text>
259+
</View>
260+
</Animated.View>
261+
);
262+
},
263+
[theme],
264+
);
252265

253266
// ─── Video mode render (original bubble) ───────────────────────────────────
254-
const renderVideoItem = useCallback(({ item }: { item: DisplayedDanmaku }) => {
255-
const dotColor = danmakuColorToCss(item.color);
256-
return (
257-
<Animated.View style={[styles.bubble, { opacity: item._fadeAnim, backgroundColor: theme.bg }]}>
258-
<View style={[styles.colorDot, { backgroundColor: dotColor }]} />
259-
<Text style={[styles.bubbleText, { color: theme.text }]} numberOfLines={3}>
260-
{item.text}
261-
</Text>
262-
<Text style={styles.timestamp}>{formatTimestamp(item.time)}</Text>
263-
</Animated.View>
264-
);
265-
}, [theme]);
267+
const renderVideoItem = useCallback(
268+
({ item }: { item: DisplayedDanmaku }) => {
269+
const dotColor = danmakuColorToCss(item.color);
270+
return (
271+
<Animated.View
272+
style={[
273+
styles.bubble,
274+
{ opacity: item._fadeAnim, backgroundColor: theme.bg },
275+
]}
276+
>
277+
<Text
278+
style={[styles.bubbleText, { color: dotColor }]}
279+
numberOfLines={3}
280+
>
281+
{item.text}
282+
</Text>
283+
<Text style={styles.timestamp}>{formatTimestamp(item.time)}</Text>
284+
</Animated.View>
285+
);
286+
},
287+
[theme],
288+
);
266289

267290
const keyExtractor = useCallback(
268291
(item: DisplayedDanmaku) => String(item._key),
269292
[],
270293
);
271294

272295
return (
273-
<View style={[styles.container, { backgroundColor: theme.card, borderTopColor: theme.border }, style]}>
296+
<View
297+
style={[
298+
styles.container,
299+
{ backgroundColor: theme.card, borderTopColor: theme.border },
300+
style,
301+
]}
302+
>
274303
{!hideHeader && (
275304
<TouchableOpacity
276305
style={styles.header}
@@ -300,8 +329,13 @@ export default function DanmakuList({
300329
data={displayedItems}
301330
keyExtractor={keyExtractor}
302331
renderItem={isLive ? renderLiveItem : renderVideoItem}
303-
style={[isLive ? liveStyles.list : styles.list, { backgroundColor: theme.bg }]}
304-
contentContainerStyle={isLive ? liveStyles.listContent : styles.listContent}
332+
style={[
333+
isLive ? liveStyles.list : styles.list,
334+
{ backgroundColor: theme.card },
335+
]}
336+
contentContainerStyle={
337+
isLive ? liveStyles.listContent : styles.listContent
338+
}
305339
onScroll={handleScroll}
306340
onScrollBeginDrag={handleScrollBeginDrag}
307341
scrollEventThrottle={16}
@@ -323,7 +357,6 @@ export default function DanmakuList({
323357
)}
324358
</View>
325359
)}
326-
327360
</View>
328361
);
329362
}
@@ -370,13 +403,6 @@ const styles = StyleSheet.create({
370403
marginVertical: 2,
371404
gap: 8,
372405
},
373-
colorDot: {
374-
width: 6,
375-
height: 6,
376-
borderRadius: 3,
377-
marginTop: 6,
378-
flexShrink: 0,
379-
},
380406
bubbleText: {
381407
flex: 1,
382408
fontSize: 13,
@@ -425,8 +451,6 @@ const liveStyles = StyleSheet.create({
425451
flexDirection: "row",
426452
alignItems: "flex-start",
427453
paddingVertical: 5,
428-
borderBottomWidth: StyleSheet.hairlineWidth,
429-
borderBottomColor: "#f0f0f2",
430454
},
431455
time: {
432456
fontSize: 10,

0 commit comments

Comments
 (0)