Skip to content

Commit ea96c01

Browse files
author
Developer
committed
chore: merge master-bug → master (v1.0.8 暗黑模式 + 节流模式 + 本地二维码)
2 parents db3f2db + 7870c83 commit ea96c01

21 files changed

Lines changed: 712 additions & 278 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ docs/
4949
feature.md
5050
livePlan.md
5151
Promotion.md
52+
wordsFilter.md
5253
.env.local

CHANGELOG.md

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

66
---
77

8+
## [1.0.8] - 2026-03-24
9+
10+
### 新增
11+
- **暗黑模式**:全局主题系统(`utils/theme.ts`),支持亮色 / 暗色一键切换,覆盖所有页面和组件
12+
- **节流模式**:设置页新增流量节省开关,开启后使用低画质封面、首页视频不自动播放、视频默认 360p 画质
13+
- **本地二维码生成**:登录二维码改用 `react-native-qrcode-svg` 本地渲染,移除对 `api.qrserver.com` 的外部依赖,提升可靠性
14+
15+
### 修复
16+
- **SeasonSection 背景色**:合集组件背景色与父容器不一致,现跟随主题色 (`theme.card`) 正确显示
17+
- **推荐列表 Loading 状态**:空列表加载中未显示 spinner(`ListEmptyComponent` 条件逻辑反转)
18+
- **合集滚动定位偏移**`getItemLayout` offset 计算未计入卡片间距(`gap: 10`),导致 `scrollToIndex` 定位不准
19+
- **推荐视频卡片双边框**:相邻推荐视频卡片之间出现双分割线
20+
821
## [1.0.0] — 2026-03-20
922

1023
### 首个正式版本

app/_layout.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useEffect } from 'react';
66
import { useAuthStore } from '../store/authStore';
77
import { useDownloadStore } from '../store/downloadStore';
88
import { useSettingsStore } from '../store/settingsStore';
9+
import { useTheme } from '../utils/theme';
910
import { MiniPlayer } from '../components/MiniPlayer';
1011
import * as Sentry from '@sentry/react-native';
1112
import { ErrorBoundary } from '@sentry/react-native';
@@ -21,6 +22,7 @@ function RootLayout() {
2122
const restore = useAuthStore(s => s.restore);
2223
const loadDownloads = useDownloadStore(s => s.loadFromStorage);
2324
const restoreSettings = useSettingsStore(s => s.restore);
25+
const darkMode = useSettingsStore(s => s.darkMode);
2426

2527
useEffect(() => {
2628
restore();
@@ -31,7 +33,7 @@ function RootLayout() {
3133

3234
return (
3335
<SafeAreaProvider>
34-
<StatusBar style="dark" />
36+
<StatusBar style={darkMode ? 'light' : 'dark'} />
3537
<View style={{ flex: 1 }}>
3638
<ErrorBoundary fallback={<Text style={{ padding: 32, textAlign: 'center' }}>发生错误,请重启 App</Text>}>
3739
<Stack screenOptions={{ headerShown: false }}>

app/downloads.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ function formatFileSize(bytes?: number): string {
2727
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
2828
}
2929
import { proxyImageUrl } from '../utils/imageUrl';
30+
import { useTheme } from '../utils/theme';
3031

3132
export default function DownloadsScreen() {
3233
const router = useRouter();
34+
const theme = useTheme();
3335
const { tasks, loadFromStorage, removeTask } = useDownloadStore();
3436
const [playingUri, setPlayingUri] = useState<string | null>(null);
3537
const [playingTitle, setPlayingTitle] = useState('');
@@ -61,12 +63,12 @@ export default function DownloadsScreen() {
6163
if (done.length > 0) sections.push({ title: '已下载', data: done });
6264

6365
return (
64-
<SafeAreaView style={styles.safe}>
65-
<View style={styles.topBar}>
66+
<SafeAreaView style={[styles.safe, { backgroundColor: theme.bg }]}>
67+
<View style={[styles.topBar, { backgroundColor: theme.card, borderBottomColor: theme.border }]}>
6668
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
67-
<Ionicons name="chevron-back" size={24} color="#212121" />
69+
<Ionicons name="chevron-back" size={24} color={theme.text} />
6870
</TouchableOpacity>
69-
<Text style={styles.topTitle}>我的下载</Text>
71+
<Text style={[styles.topTitle, { color: theme.text }]}>我的下载</Text>
7072
<View style={{ width: 32 }} />
7173
</View>
7274

app/index.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
} from "../utils/videoRows";
4141
import { BigVideoCard } from "../components/BigVideoCard";
4242
import { FollowedLiveStrip } from "../components/FollowedLiveStrip";
43+
import { useTheme } from "../utils/theme";
4344
import type { LiveRoom } from "../services/types";
4445

4546
const HEADER_H = 44;
@@ -83,6 +84,7 @@ export default function HomeScreen() {
8384
const [activeTab, setActiveTab] = useState<TabKey>("hot");
8485
const [liveAreaId, setLiveAreaId] = useState(0);
8586

87+
const theme = useTheme();
8688
const [visibleBigKey, setVisibleBigKey] = useState<string | null>(null);
8789
const rows = useMemo(() => toListRows(pages, liveRooms), [pages, liveRooms]);
8890
const pagerRef = useRef<PagerView>(null);
@@ -286,7 +288,7 @@ export default function HomeScreen() {
286288
activeTab === "hot" ? headerOpacity : liveHeaderOpacity;
287289

288290
return (
289-
<SafeAreaView style={styles.safe} edges={["left", "right"]}>
291+
<SafeAreaView style={[styles.safe, { backgroundColor: theme.bg }]} edges={["left", "right"]}>
290292
{/* 滑动切换容器 */}
291293
<PagerView
292294
ref={pagerRef}
@@ -410,6 +412,7 @@ export default function HomeScreen() {
410412
styles.navBar,
411413
{
412414
paddingTop: insets.top,
415+
backgroundColor: theme.card,
413416
transform: [{ translateY: currentHeaderTranslate }],
414417
},
415418
]}
@@ -419,6 +422,7 @@ export default function HomeScreen() {
419422
styles.header,
420423
{
421424
opacity: currentHeaderOpacity,
425+
borderBottomColor: theme.border,
422426
},
423427
]}
424428
>
@@ -439,19 +443,19 @@ export default function HomeScreen() {
439443
</TouchableOpacity>
440444
</View>
441445
<TouchableOpacity
442-
style={styles.searchBar}
446+
style={[styles.searchBar, { backgroundColor: theme.inputBg }]}
443447
onPress={() => router.push("/search" as any)}
444448
activeOpacity={0.7}
445449
>
446-
<Ionicons name="search" size={14} color="#999" />
447-
<Text style={styles.searchPlaceholder}>搜索视频、UP主...</Text>
450+
<Ionicons name="search" size={14} color={theme.textSub} />
451+
<Text style={[styles.searchPlaceholder, { color: theme.textSub }]}>搜索视频、UP主...</Text>
448452
</TouchableOpacity>
449453
<DownloadProgressBtn
450454
onPress={() => router.push("/downloads" as any)}
451455
/>
452456
</Animated.View>
453457

454-
<View style={styles.tabRow}>
458+
<View style={[styles.tabRow, { backgroundColor: theme.card }]}>
455459
{TABS.map((tab) => (
456460
<TouchableOpacity
457461
key={tab.key}
@@ -462,6 +466,7 @@ export default function HomeScreen() {
462466
<Text
463467
style={[
464468
styles.tabText,
469+
{ color: theme.textSub },
465470
activeTab === tab.key && styles.tabTextActive,
466471
]}
467472
>

app/live/[roomId].tsx

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@ import { LivePlayer } from "../../components/LivePlayer";
1717
import DanmakuList from "../../components/DanmakuList";
1818
import { formatCount } from "../../utils/format";
1919
import { proxyImageUrl } from "../../utils/imageUrl";
20+
import { useTheme } from "../../utils/theme";
2021

2122
type Tab = "intro" | "danmaku";
2223

2324
export default function LiveDetailScreen() {
2425
const { roomId } = useLocalSearchParams<{ roomId: string }>();
2526
const router = useRouter();
27+
const theme = useTheme();
2628
const id = parseInt(roomId ?? "0", 10);
2729
const { room, anchor, stream, loading, error, changeQuality } =
2830
useLiveDetail(id);
@@ -34,17 +36,17 @@ export default function LiveDetailScreen() {
3436
const qualities = stream?.qualities ?? [];
3537
const currentQn = stream?.qn ?? 0;
3638

37-
// Use actual roomid from room detail (not the short/alias ID from the URL)
3839
const actualRoomId = room?.roomid ?? id;
3940
const { danmakus, giftCounts } = useLiveDanmaku(isLive ? actualRoomId : 0);
41+
4042
return (
41-
<SafeAreaView style={styles.safe}>
43+
<SafeAreaView style={[styles.safe, { backgroundColor: theme.card }]}>
4244
{/* TopBar */}
43-
<View style={styles.topBar}>
45+
<View style={[styles.topBar, { borderBottomColor: theme.border }]}>
4446
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
45-
<Ionicons name="chevron-back" size={24} color="#212121" />
47+
<Ionicons name="chevron-back" size={24} color={theme.text} />
4648
</TouchableOpacity>
47-
<Text style={styles.topTitle} numberOfLines={1}>
49+
<Text style={[styles.topTitle, { color: theme.text }]} numberOfLines={1}>
4850
{room?.title ?? "直播间"}
4951
</Text>
5052
</View>
@@ -60,12 +62,12 @@ export default function LiveDetailScreen() {
6062
/>
6163

6264
{/* TabBar */}
63-
<View style={styles.tabBar}>
65+
<View style={[styles.tabBar, { backgroundColor: theme.card, borderBottomColor: theme.border }]}>
6466
<TouchableOpacity
6567
style={styles.tabItem}
6668
onPress={() => setTab("intro")}
6769
>
68-
<Text style={[styles.tabLabel, tab === "intro" && styles.tabActive]}>
70+
<Text style={[styles.tabLabel, { color: theme.textSub }, tab === "intro" && styles.tabActive]}>
6971
简介
7072
</Text>
7173
{tab === "intro" && <View style={styles.tabUnderline} />}
@@ -75,15 +77,15 @@ export default function LiveDetailScreen() {
7577
onPress={() => setTab("danmaku")}
7678
>
7779
<Text
78-
style={[styles.tabLabel, tab === "danmaku" && styles.tabActive]}
80+
style={[styles.tabLabel, { color: theme.textSub }, tab === "danmaku" && styles.tabActive]}
7981
>
8082
弹幕{danmakus.length > 0 ? ` ${danmakus.length}` : ""}
8183
</Text>
8284
{tab === "danmaku" && <View style={styles.tabUnderline} />}
8385
</TouchableOpacity>
8486
</View>
8587

86-
{/* Content — 两个面板始终挂载,通过 display 切换,保留弹幕列表状态 */}
88+
{/* Content */}
8789
{loading ? (
8890
<ActivityIndicator style={styles.loader} color="#00AEEC" />
8991
) : error ? (
@@ -95,7 +97,7 @@ export default function LiveDetailScreen() {
9597
showsVerticalScrollIndicator={false}
9698
>
9799
<View style={styles.titleSection}>
98-
<Text style={styles.title}>{room?.title}</Text>
100+
<Text style={[styles.title, { color: theme.text }]}>{room?.title}</Text>
99101
<View style={styles.metaRow}>
100102
{isLive ? (
101103
<View style={styles.livePill}>
@@ -126,15 +128,15 @@ export default function LiveDetailScreen() {
126128
</View>
127129
</View>
128130

129-
<View style={styles.divider} />
131+
<View style={[styles.divider, { backgroundColor: theme.border }]} />
130132

131133
{anchor && (
132134
<View style={styles.anchorRow}>
133135
<Image
134136
source={{ uri: proxyImageUrl(anchor.face) }}
135137
style={styles.avatar}
136138
/>
137-
<Text style={styles.anchorName}>{anchor.uname}</Text>
139+
<Text style={[styles.anchorName, { color: theme.text }]}>{anchor.uname}</Text>
138140
<TouchableOpacity style={styles.followBtn}>
139141
<Text style={styles.followTxt}>+ 关注</Text>
140142
</TouchableOpacity>
@@ -143,7 +145,7 @@ export default function LiveDetailScreen() {
143145

144146
{!!room?.description && (
145147
<View style={styles.descBox}>
146-
<Text style={styles.descText}>{room.description}</Text>
148+
<Text style={[styles.descText, { color: theme.text }]}>{room.description}</Text>
147149
</View>
148150
)}
149151
</ScrollView>
@@ -166,38 +168,34 @@ export default function LiveDetailScreen() {
166168
}
167169

168170
const styles = StyleSheet.create({
169-
safe: { flex: 1, backgroundColor: "#fff" },
171+
safe: { flex: 1 },
170172
topBar: {
171173
flexDirection: "row",
172174
alignItems: "center",
173175
paddingHorizontal: 8,
174176
paddingVertical: 8,
175177
borderBottomWidth: StyleSheet.hairlineWidth,
176-
borderBottomColor: "#eee",
177178
},
178179
backBtn: { padding: 4 },
179180
topTitle: {
180181
flex: 1,
181182
fontSize: 15,
182183
fontWeight: "600",
183184
marginLeft: 4,
184-
color: "#212121",
185185
},
186186
loader: { marginVertical: 30 },
187187
errorText: { textAlign: "center", color: "#f00", padding: 20 },
188188
tabBar: {
189189
flexDirection: "row",
190190
borderBottomWidth: StyleSheet.hairlineWidth,
191-
borderBottomColor: "#eee",
192-
backgroundColor: "#fff",
193191
},
194192
tabItem: {
195193
paddingHorizontal: 20,
196194
paddingVertical: 10,
197195
alignItems: "center",
198196
position: "relative",
199197
},
200-
tabLabel: { fontSize: 14, color: "#999", fontWeight: "500" },
198+
tabLabel: { fontSize: 14, fontWeight: "500" },
201199
tabActive: { color: "#00AEEC", fontWeight: "700" },
202200
tabUnderline: {
203201
position: "absolute",
@@ -212,7 +210,6 @@ const styles = StyleSheet.create({
212210
title: {
213211
fontSize: 16,
214212
fontWeight: "600",
215-
color: "#212121",
216213
lineHeight: 22,
217214
marginBottom: 8,
218215
},
@@ -248,7 +245,6 @@ const styles = StyleSheet.create({
248245
},
249246
divider: {
250247
height: StyleSheet.hairlineWidth,
251-
backgroundColor: "#f0f0f0",
252248
marginHorizontal: 14,
253249
},
254250
anchorRow: {
@@ -258,7 +254,7 @@ const styles = StyleSheet.create({
258254
paddingVertical: 12,
259255
},
260256
avatar: { width: 40, height: 40, borderRadius: 20, marginRight: 10 },
261-
anchorName: { flex: 1, fontSize: 14, color: "#212121", fontWeight: "500" },
257+
anchorName: { flex: 1, fontSize: 14, fontWeight: "500" },
262258
followBtn: {
263259
backgroundColor: "#00AEEC",
264260
paddingHorizontal: 14,
@@ -267,7 +263,7 @@ const styles = StyleSheet.create({
267263
},
268264
followTxt: { color: "#fff", fontSize: 12, fontWeight: "600" },
269265
descBox: { padding: 14, paddingTop: 4 },
270-
descText: { fontSize: 14, color: "#555", lineHeight: 22 },
266+
descText: { fontSize: 14, lineHeight: 22 },
271267
danmakuFull: { flex: 1 },
272268
hidden: { display: "none" },
273269
});

app/search.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ import { useRouter } from 'expo-router';
1313
import { Ionicons } from '@expo/vector-icons';
1414
import { VideoCard } from '../components/VideoCard';
1515
import { useSearch } from '../hooks/useSearch';
16+
import { useTheme } from '../utils/theme';
1617
import type { VideoItem } from '../services/types';
1718

1819
export default function SearchScreen() {
1920
const router = useRouter();
2021
const { keyword, setKeyword, results, loading, hasMore, search, loadMore } = useSearch();
22+
const theme = useTheme();
2123
const inputRef = useRef<TextInput>(null);
2224

2325
const handleSearch = useCallback(() => {
@@ -64,7 +66,7 @@ export default function SearchScreen() {
6466
return (
6567
<View style={styles.emptyBox}>
6668
<Ionicons name="search-outline" size={48} color="#ddd" />
67-
<Text style={styles.emptyText}>
69+
<Text style={[styles.emptyText, { color: theme.textSub }]}>
6870
{results.length === 0 && keyword.trim()
6971
? '没有找到相关视频'
7072
: '输入关键词搜索'}
@@ -74,16 +76,16 @@ export default function SearchScreen() {
7476
};
7577

7678
return (
77-
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
79+
<SafeAreaView style={[styles.safe, { backgroundColor: theme.bg }]} edges={['top', 'left', 'right']}>
7880
{/* Search header */}
79-
<View style={styles.header}>
81+
<View style={[styles.header, { backgroundColor: theme.card, borderBottomColor: theme.border }]}>
8082
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
81-
<Ionicons name="chevron-back" size={24} color="#212121" />
83+
<Ionicons name="chevron-back" size={24} color={theme.text} />
8284
</TouchableOpacity>
83-
<View style={styles.inputWrap}>
85+
<View style={[styles.inputWrap, { backgroundColor: theme.inputBg }]}>
8486
<TextInput
8587
ref={inputRef}
86-
style={styles.input}
88+
style={[styles.input, { color: theme.text }]}
8789
placeholder="搜索视频、UP主..."
8890
placeholderTextColor="#999"
8991
value={keyword}

0 commit comments

Comments
 (0)