Skip to content

Commit b4d5d2a

Browse files
author
Developer
committed
Merge branch 'master-bug'
# Conflicts: # CHANGELOG.md # app/video/[bvid].tsx # components/DanmakuList.tsx
2 parents 80109b9 + e3def7d commit b4d5d2a

14 files changed

Lines changed: 522 additions & 175 deletions

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@
55

66
---
77

8+
## [1.0.13] - 2026-03-25
9+
10+
### 修复
11+
- **小窗 PanResponder 闭包过期**`useRef(PanResponder.create(...))` 捕获初始 `roomId=0` / `bvid=""`,导致点击小窗跳转到错误页面;改用 `storeRef` 模式保持最新值
12+
- **直播小窗进入详情无限 loading**`useLiveDetail` 使用 `cancelled` 闭包标志,effect cleanup 后 fetch 被静默丢弃;改用 `latestRoomId` ref 比对替代 cancelled 模式
13+
- **进入播放器页面小窗不关闭**:视频/直播详情页进入时通过 `useLayoutEffect` + `getState().clearLive()` 同步清除小窗,避免双播和资源竞争
14+
- **BigVideoCard 与直播小窗冲突**:首页 BigVideoCard 自动播放与直播小窗竞争解码器资源;小窗活跃时跳过 Video 渲染,仅显示封面图
15+
- **退出全屏视频暂停**:互斥渲染后竖屏播放器重新挂载,react-native-video seek 后不自动恢复播放;`onLoad` 中强制 `paused` 状态切换触发播放
16+
17+
### 优化
18+
- **视频播放器单实例**:竖屏/全屏互斥渲染(`{!fullscreen && ...}` / `{fullscreen && ...}`),不再同时挂载两个 Video 解码器,减半 GPU/内存占用
19+
- **onProgress 节流**`progressUpdateInterval` 从 250ms 调为 500ms,回调内增加 450ms 节流和 seeking 跳过,减少重渲染
20+
- **移除调试日志**:清理 NativeVideoPlayer 中遗留的 `console.log`
21+
- **下载页 UI 优化**:下载管理页交互和暗黑主题适配
22+
23+
---
24+
25+
826
## [1.0.12] - 2026-03-25
927

1028
### 新增

app/_layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useDownloadStore } from '../store/downloadStore';
88
import { useSettingsStore } from '../store/settingsStore';
99
import { useTheme } from '../utils/theme';
1010
import { MiniPlayer } from '../components/MiniPlayer';
11+
import { LiveMiniPlayer } from '../components/LiveMiniPlayer';
1112
import * as Sentry from '@sentry/react-native';
1213
import { ErrorBoundary } from '@sentry/react-native';
1314
import { useFonts } from 'expo-font';
@@ -87,6 +88,7 @@ function RootLayout() {
8788
</Stack>
8889
</ErrorBoundary>
8990
<MiniPlayer />
91+
<LiveMiniPlayer />
9092
</View>
9193
</SafeAreaProvider>
9294
);

app/downloads.tsx

Lines changed: 98 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@ import {
66
StyleSheet,
77
TouchableOpacity,
88
Image,
9-
ActivityIndicator,
109
Modal,
1110
StatusBar,
12-
useWindowDimensions,
11+
Alert,
1312
} from 'react-native';
1413
import { SafeAreaView } from 'react-native-safe-area-context';
1514
import { useRouter } from 'expo-router';
@@ -19,15 +18,15 @@ let ScreenOrientation: typeof import('expo-screen-orientation') | null = null;
1918
try { ScreenOrientation = require('expo-screen-orientation'); } catch {}
2019
import { useDownloadStore, DownloadTask } from '../store/downloadStore';
2120
import { LanShareModal } from '../components/LanShareModal';
21+
import { proxyImageUrl } from '../utils/imageUrl';
22+
import { useTheme } from '../utils/theme';
2223

2324
function formatFileSize(bytes?: number): string {
2425
if (!bytes || bytes <= 0) return '';
2526
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
2627
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2728
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
2829
}
29-
import { proxyImageUrl } from '../utils/imageUrl';
30-
import { useTheme } from '../utils/theme';
3130

3231
export default function DownloadsScreen() {
3332
const router = useRouter();
@@ -36,8 +35,6 @@ export default function DownloadsScreen() {
3635
const [playingUri, setPlayingUri] = useState<string | null>(null);
3736
const [playingTitle, setPlayingTitle] = useState('');
3837
const [shareTask, setShareTask] = useState<(DownloadTask & { key: string }) | null>(null);
39-
const { width, height } = useWindowDimensions();
40-
const isLandscape = width > height;
4138

4239
async function openPlayer(uri: string, title: string) {
4340
setPlayingTitle(title);
@@ -50,6 +47,18 @@ export default function DownloadsScreen() {
5047
await ScreenOrientation?.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
5148
}
5249

50+
function confirmDelete(key: string, status: DownloadTask['status']) {
51+
const isDownloading = status === 'downloading';
52+
Alert.alert(
53+
isDownloading ? '取消下载' : '删除下载',
54+
isDownloading ? '确定取消该下载任务?' : '确定删除该文件?删除后不可恢复。',
55+
[
56+
{ text: '取消', style: 'cancel' },
57+
{ text: isDownloading ? '取消下载' : '删除', style: 'destructive', onPress: () => removeTask(key) },
58+
],
59+
);
60+
}
61+
5362
useEffect(() => {
5463
loadFromStorage();
5564
}, []);
@@ -74,29 +83,33 @@ export default function DownloadsScreen() {
7483

7584
{sections.length === 0 ? (
7685
<View style={styles.empty}>
77-
<Ionicons name="cloud-download-outline" size={56} color="#ccc" />
78-
<Text style={styles.emptyTxt}>暂无下载记录</Text>
86+
<Ionicons name="cloud-download-outline" size={56} color={theme.textSub} />
87+
<Text style={[styles.emptyTxt, { color: theme.textSub }]}>暂无下载记录</Text>
7988
</View>
8089
) : (
8190
<SectionList
8291
sections={sections}
8392
keyExtractor={(item) => item.key}
8493
renderSectionHeader={({ section }) => (
85-
<View style={styles.sectionHeader}>
86-
<Text style={styles.sectionTitle}>{section.title}</Text>
94+
<View style={[styles.sectionHeader, { backgroundColor: theme.bg }]}>
95+
<Text style={[styles.sectionTitle, { color: theme.textSub }]}>{section.title}</Text>
8796
</View>
8897
)}
8998
renderItem={({ item }) => (
9099
<DownloadRow
91100
task={item}
101+
theme={theme}
92102
onPlay={() => {
93103
if (item.localUri) openPlayer(item.localUri, item.title);
94104
}}
95-
onDelete={() => removeTask(item.key)}
105+
onDelete={() => confirmDelete(item.key, item.status)}
96106
onShare={() => setShareTask(item)}
107+
onRetry={() => router.push(`/video/${item.bvid}` as any)}
97108
/>
98109
)}
99-
ItemSeparatorComponent={() => <View style={styles.separator} />}
110+
ItemSeparatorComponent={() => (
111+
<View style={[styles.separator, { backgroundColor: theme.border, marginLeft: 108 }]} />
112+
)}
100113
contentContainerStyle={{ paddingBottom: 32 }}
101114
/>
102115
)}
@@ -119,22 +132,18 @@ export default function DownloadsScreen() {
119132
{playingUri && (
120133
<Video
121134
source={{ uri: playingUri }}
122-
style={isLandscape
123-
? { width, height }
124-
: { width, height: width * 0.5625 }}
135+
style={StyleSheet.absoluteFillObject}
125136
resizeMode="contain"
126137
controls
127138
paused={false}
128139
/>
129140
)}
130-
{!isLandscape && (
131-
<View style={styles.playerBar}>
132-
<TouchableOpacity onPress={closePlayer} style={styles.closeBtn}>
133-
<Ionicons name="chevron-back" size={24} color="#fff" />
134-
</TouchableOpacity>
135-
<Text style={styles.playerTitle} numberOfLines={1}>{playingTitle}</Text>
136-
</View>
137-
)}
141+
<View style={styles.playerBar}>
142+
<TouchableOpacity onPress={closePlayer} style={styles.closeBtn} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
143+
<Ionicons name="chevron-back" size={24} color="#fff" />
144+
</TouchableOpacity>
145+
<Text style={styles.playerTitle} numberOfLines={1}>{playingTitle}</Text>
146+
</View>
138147
</View>
139148
</Modal>
140149
</SafeAreaView>
@@ -143,98 +152,114 @@ export default function DownloadsScreen() {
143152

144153
function DownloadRow({
145154
task,
155+
theme,
146156
onPlay,
147157
onDelete,
148158
onShare,
159+
onRetry,
149160
}: {
150161
task: DownloadTask & { key: string };
162+
theme: ReturnType<typeof useTheme>;
151163
onPlay: () => void;
152164
onDelete: () => void;
153165
onShare: () => void;
166+
onRetry: () => void;
154167
}) {
155-
return (
156-
<View style={styles.row}>
157-
<Image
158-
source={{ uri: proxyImageUrl(task.cover) }}
159-
style={styles.cover}
160-
/>
168+
const isDone = task.status === 'done';
169+
const isError = task.status === 'error';
170+
const isDownloading = task.status === 'downloading';
171+
172+
const rowContent = (
173+
<View style={[styles.row, { backgroundColor: theme.card }]}>
174+
<Image source={{ uri: proxyImageUrl(task.cover) }} style={styles.cover} />
161175
<View style={styles.info}>
162-
<Text style={styles.title} numberOfLines={2}>{task.title}</Text>
163-
<Text style={styles.qdesc}>
176+
<Text style={[styles.title, { color: theme.text }]} numberOfLines={2}>{task.title}</Text>
177+
<Text style={[styles.qdesc, { color: theme.textSub }]}>
164178
{task.qdesc}{task.fileSize ? ` · ${formatFileSize(task.fileSize)}` : ''}
165179
</Text>
166-
{task.status === 'downloading' && (
180+
{isDownloading && (
167181
<View style={styles.progressWrap}>
168182
<View style={styles.progressTrack}>
169183
<View style={[styles.progressFill, { width: `${Math.round(task.progress * 100)}%` as any }]} />
170184
</View>
171-
<ActivityIndicator size="small" color="#00AEEC" style={{ marginLeft: 6 }} />
172185
<Text style={styles.progressTxt}>{Math.round(task.progress * 100)}%</Text>
173186
</View>
174187
)}
175-
{task.status === 'error' && (
176-
<Text style={styles.errorTxt} numberOfLines={1}>{task.error ?? '下载失败'}</Text>
188+
{isError && (
189+
<View style={styles.errorRow}>
190+
<Text style={styles.errorTxt} numberOfLines={1}>{task.error ?? '下载失败'}</Text>
191+
<TouchableOpacity onPress={onRetry} style={styles.retryBtn}>
192+
<Text style={styles.retryTxt}>重新下载</Text>
193+
</TouchableOpacity>
194+
</View>
177195
)}
178196
</View>
179197
<View style={styles.actions}>
180-
{task.status === 'done' && (
181-
<>
182-
<TouchableOpacity style={styles.playBtn} onPress={onPlay}>
183-
<Ionicons name="play-circle" size={20} color="#00AEEC" />
184-
<Text style={styles.playTxt}>播放</Text>
185-
</TouchableOpacity>
186-
<TouchableOpacity style={styles.shareBtn} onPress={onShare}>
187-
<Ionicons name="share-social-outline" size={20} color="#00AEEC" />
188-
</TouchableOpacity>
189-
</>
198+
{isDone && (
199+
<TouchableOpacity style={styles.actionBtn} onPress={onShare}>
200+
<Ionicons name="share-social-outline" size={20} color="#00AEEC" />
201+
</TouchableOpacity>
190202
)}
191-
<TouchableOpacity style={styles.deleteBtn} onPress={onDelete}>
192-
<Ionicons name="trash-outline" size={18} color="#bbb" />
203+
<TouchableOpacity
204+
style={styles.actionBtn}
205+
onPress={isDownloading ? onDelete : onDelete}
206+
>
207+
<Ionicons
208+
name={isDownloading ? 'close-circle-outline' : 'trash-outline'}
209+
size={20}
210+
color={isDownloading ? '#bbb' : '#bbb'}
211+
/>
193212
</TouchableOpacity>
194213
</View>
195214
</View>
196215
);
216+
217+
if (isDone) {
218+
return (
219+
<TouchableOpacity activeOpacity={0.85} onPress={onPlay}>
220+
{rowContent}
221+
</TouchableOpacity>
222+
);
223+
}
224+
225+
return rowContent;
197226
}
198227

199228
const styles = StyleSheet.create({
200-
safe: { flex: 1, backgroundColor: '#fff' },
229+
safe: { flex: 1 },
201230
topBar: {
202231
flexDirection: 'row',
203232
alignItems: 'center',
204233
paddingHorizontal: 8,
205234
paddingVertical: 8,
206235
borderBottomWidth: StyleSheet.hairlineWidth,
207-
borderBottomColor: '#eee',
208236
},
209237
backBtn: { padding: 4 },
210238
topTitle: {
211239
flex: 1,
212240
fontSize: 16,
213241
fontWeight: '700',
214-
color: '#212121',
215242
marginLeft: 4,
216243
},
217244
empty: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12 },
218-
emptyTxt: { fontSize: 14, color: '#bbb' },
245+
emptyTxt: { fontSize: 14 },
219246
sectionHeader: {
220-
backgroundColor: '#f4f4f4',
221247
paddingHorizontal: 16,
222248
paddingVertical: 8,
223249
},
224-
sectionTitle: { fontSize: 13, fontWeight: '600', color: '#555' },
250+
sectionTitle: { fontSize: 13, fontWeight: '600' },
225251
row: {
226252
flexDirection: 'row',
227253
alignItems: 'center',
228254
paddingHorizontal: 16,
229255
paddingVertical: 12,
230-
backgroundColor: '#fff',
231256
gap: 12,
232257
},
233-
cover: { width: 80, height: 54, borderRadius: 6, backgroundColor: '#eee' },
258+
cover: { width: 80, height: 54, borderRadius: 6, backgroundColor: '#eee', flexShrink: 0 },
234259
info: { flex: 1 },
235-
title: { fontSize: 13, color: '#212121', lineHeight: 18, marginBottom: 4 },
236-
qdesc: { fontSize: 12, color: '#999', marginBottom: 4 },
237-
progressWrap: { flexDirection: 'row', alignItems: 'center', marginTop: 2 },
260+
title: { fontSize: 13, lineHeight: 18, marginBottom: 4 },
261+
qdesc: { fontSize: 12, marginBottom: 4 },
262+
progressWrap: { flexDirection: 'row', alignItems: 'center', marginTop: 2, gap: 6 },
238263
progressTrack: {
239264
flex: 1,
240265
height: 3,
@@ -243,14 +268,19 @@ const styles = StyleSheet.create({
243268
overflow: 'hidden',
244269
},
245270
progressFill: { height: 3, backgroundColor: '#00AEEC', borderRadius: 2 },
246-
progressTxt: { fontSize: 11, color: '#999', marginLeft: 4 },
247-
errorTxt: { fontSize: 12, color: '#f44', marginTop: 2 },
248-
actions: { alignItems: 'center', gap: 8 },
249-
playBtn: { flexDirection: 'row', alignItems: 'center', gap: 3 },
250-
playTxt: { fontSize: 13, color: '#00AEEC' },
251-
shareBtn: { padding: 4 },
252-
deleteBtn: { padding: 4 },
253-
separator: { height: StyleSheet.hairlineWidth, backgroundColor: '#f0f0f0', marginLeft: 108 },
271+
progressTxt: { fontSize: 11, color: '#999', minWidth: 30 },
272+
errorRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: 2 },
273+
errorTxt: { fontSize: 12, color: '#f44', flex: 1 },
274+
retryBtn: {
275+
paddingHorizontal: 8,
276+
paddingVertical: 2,
277+
borderRadius: 10,
278+
backgroundColor: '#e8f7fd',
279+
},
280+
retryTxt: { fontSize: 12, color: '#00AEEC', fontWeight: '600' },
281+
actions: { alignItems: 'center', gap: 12 },
282+
actionBtn: { padding: 4 },
283+
separator: { height: StyleSheet.hairlineWidth },
254284
// player modal
255285
playerBg: { flex: 1, backgroundColor: '#000', justifyContent: 'center' },
256286
playerBar: {
@@ -261,6 +291,8 @@ const styles = StyleSheet.create({
261291
flexDirection: 'row',
262292
alignItems: 'center',
263293
paddingHorizontal: 8,
294+
backgroundColor: 'rgba(0,0,0,0.4)',
295+
paddingVertical: 8,
264296
},
265297
closeBtn: { padding: 6 },
266298
playerTitle: { flex: 1, color: '#fff', fontSize: 14, fontWeight: '600', marginLeft: 4 },

app/index.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,6 @@ export default function HomeScreen() {
422422
styles.header,
423423
{
424424
opacity: currentHeaderOpacity,
425-
borderBottomColor: theme.border,
426425
},
427426
]}
428427
>
@@ -507,8 +506,6 @@ const styles = StyleSheet.create({
507506
alignItems: "center",
508507
paddingHorizontal: 16,
509508
gap: 10,
510-
borderBottomWidth: StyleSheet.hairlineWidth,
511-
borderBottomColor: "#eee",
512509
},
513510
logo: {
514511
fontSize: 20,

0 commit comments

Comments
 (0)