feat: 暗黑模式 + 节流模式

- settingsStore: 移除 coverQuality,新增 darkMode / trafficSaving 布尔字段(AsyncStorage 持久化)
- utils/theme.ts: 新建 useTheme() hook,返回浅色/深色颜色表
- settings.tsx: 新增外观(浅色/深色)和流量(标准/节流)选项,移除封面图清晰度
- BigVideoCard: 节流模式跳过预加载、禁止自动播放;封面低画质;支持暗黑配色
- useVideoDetail: 节流模式下默认 qn=16(360p)
- VideoCard / LiveCard: 移除 coverQuality 依赖,改用 trafficSaving;支持暗黑配色
- index.tsx: 导航栏/搜索栏/列表背景跟随主题
- search.tsx: 页面背景/输入框/文字跟随主题
- downloads.tsx: 顶栏/背景/文字跟随主题
- _layout.tsx: StatusBar 随暗黑模式切换 light/dark
This commit is contained in:
Developer
2026-03-24 21:04:10 +08:00
parent aeaaa0af01
commit f2fc4693a6
12 changed files with 164 additions and 79 deletions

View File

@@ -6,6 +6,7 @@ import { useEffect } from 'react';
import { useAuthStore } from '../store/authStore';
import { useDownloadStore } from '../store/downloadStore';
import { useSettingsStore } from '../store/settingsStore';
import { useTheme } from '../utils/theme';
import { MiniPlayer } from '../components/MiniPlayer';
import * as Sentry from '@sentry/react-native';
import { ErrorBoundary } from '@sentry/react-native';
@@ -21,6 +22,7 @@ function RootLayout() {
const restore = useAuthStore(s => s.restore);
const loadDownloads = useDownloadStore(s => s.loadFromStorage);
const restoreSettings = useSettingsStore(s => s.restore);
const darkMode = useSettingsStore(s => s.darkMode);
useEffect(() => {
restore();
@@ -31,7 +33,7 @@ function RootLayout() {
return (
<SafeAreaProvider>
<StatusBar style="dark" />
<StatusBar style={darkMode ? 'light' : 'dark'} />
<View style={{ flex: 1 }}>
<ErrorBoundary fallback={<Text style={{ padding: 32, textAlign: 'center' }}> App</Text>}>
<Stack screenOptions={{ headerShown: false }}>

View File

@@ -27,9 +27,11 @@ function formatFileSize(bytes?: number): string {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
import { proxyImageUrl } from '../utils/imageUrl';
import { useTheme } from '../utils/theme';
export default function DownloadsScreen() {
const router = useRouter();
const theme = useTheme();
const { tasks, loadFromStorage, removeTask } = useDownloadStore();
const [playingUri, setPlayingUri] = useState<string | null>(null);
const [playingTitle, setPlayingTitle] = useState('');
@@ -61,12 +63,12 @@ export default function DownloadsScreen() {
if (done.length > 0) sections.push({ title: '已下载', data: done });
return (
<SafeAreaView style={styles.safe}>
<View style={styles.topBar}>
<SafeAreaView style={[styles.safe, { backgroundColor: theme.bg }]}>
<View style={[styles.topBar, { backgroundColor: theme.card, borderBottomColor: theme.border }]}>
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
<Ionicons name="chevron-back" size={24} color="#212121" />
<Ionicons name="chevron-back" size={24} color={theme.text} />
</TouchableOpacity>
<Text style={styles.topTitle}></Text>
<Text style={[styles.topTitle, { color: theme.text }]}></Text>
<View style={{ width: 32 }} />
</View>

View File

@@ -40,6 +40,7 @@ import {
} from "../utils/videoRows";
import { BigVideoCard } from "../components/BigVideoCard";
import { FollowedLiveStrip } from "../components/FollowedLiveStrip";
import { useTheme } from "../utils/theme";
import type { LiveRoom } from "../services/types";
const HEADER_H = 44;
@@ -83,6 +84,7 @@ export default function HomeScreen() {
const [activeTab, setActiveTab] = useState<TabKey>("hot");
const [liveAreaId, setLiveAreaId] = useState(0);
const theme = useTheme();
const [visibleBigKey, setVisibleBigKey] = useState<string | null>(null);
const rows = useMemo(() => toListRows(pages, liveRooms), [pages, liveRooms]);
const pagerRef = useRef<PagerView>(null);
@@ -286,7 +288,7 @@ export default function HomeScreen() {
activeTab === "hot" ? headerOpacity : liveHeaderOpacity;
return (
<SafeAreaView style={styles.safe} edges={["left", "right"]}>
<SafeAreaView style={[styles.safe, { backgroundColor: theme.bg }]} edges={["left", "right"]}>
{/* 滑动切换容器 */}
<PagerView
ref={pagerRef}
@@ -410,6 +412,7 @@ export default function HomeScreen() {
styles.navBar,
{
paddingTop: insets.top,
backgroundColor: theme.card,
transform: [{ translateY: currentHeaderTranslate }],
},
]}
@@ -419,6 +422,7 @@ export default function HomeScreen() {
styles.header,
{
opacity: currentHeaderOpacity,
borderBottomColor: theme.border,
},
]}
>
@@ -439,19 +443,19 @@ export default function HomeScreen() {
</TouchableOpacity>
</View>
<TouchableOpacity
style={styles.searchBar}
style={[styles.searchBar, { backgroundColor: theme.inputBg }]}
onPress={() => router.push("/search" as any)}
activeOpacity={0.7}
>
<Ionicons name="search" size={14} color="#999" />
<Text style={styles.searchPlaceholder}>UP主...</Text>
<Ionicons name="search" size={14} color={theme.textSub} />
<Text style={[styles.searchPlaceholder, { color: theme.textSub }]}>UP主...</Text>
</TouchableOpacity>
<DownloadProgressBtn
onPress={() => router.push("/downloads" as any)}
/>
</Animated.View>
<View style={styles.tabRow}>
<View style={[styles.tabRow, { backgroundColor: theme.card }]}>
{TABS.map((tab) => (
<TouchableOpacity
key={tab.key}
@@ -462,6 +466,7 @@ export default function HomeScreen() {
<Text
style={[
styles.tabText,
{ color: theme.textSub },
activeTab === tab.key && styles.tabTextActive,
]}
>

View File

@@ -13,11 +13,13 @@ import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { VideoCard } from '../components/VideoCard';
import { useSearch } from '../hooks/useSearch';
import { useTheme } from '../utils/theme';
import type { VideoItem } from '../services/types';
export default function SearchScreen() {
const router = useRouter();
const { keyword, setKeyword, results, loading, hasMore, search, loadMore } = useSearch();
const theme = useTheme();
const inputRef = useRef<TextInput>(null);
const handleSearch = useCallback(() => {
@@ -64,7 +66,7 @@ export default function SearchScreen() {
return (
<View style={styles.emptyBox}>
<Ionicons name="search-outline" size={48} color="#ddd" />
<Text style={styles.emptyText}>
<Text style={[styles.emptyText, { color: theme.textSub }]}>
{results.length === 0 && keyword.trim()
? '没有找到相关视频'
: '输入关键词搜索'}
@@ -74,16 +76,16 @@ export default function SearchScreen() {
};
return (
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
<SafeAreaView style={[styles.safe, { backgroundColor: theme.bg }]} edges={['top', 'left', 'right']}>
{/* Search header */}
<View style={styles.header}>
<View style={[styles.header, { backgroundColor: theme.card, borderBottomColor: theme.border }]}>
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
<Ionicons name="chevron-back" size={24} color="#212121" />
<Ionicons name="chevron-back" size={24} color={theme.text} />
</TouchableOpacity>
<View style={styles.inputWrap}>
<View style={[styles.inputWrap, { backgroundColor: theme.inputBg }]}>
<TextInput
ref={inputRef}
style={styles.input}
style={[styles.input, { color: theme.text }]}
placeholder="搜索视频、UP主..."
placeholderTextColor="#999"
value={keyword}

View File

@@ -5,12 +5,14 @@ import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '../store/authStore';
import { useSettingsStore } from '../store/settingsStore';
import { useTheme } from '../utils/theme';
import { useCheckUpdate } from '../hooks/useCheckUpdate';
export default function SettingsScreen() {
const router = useRouter();
const { isLoggedIn, logout } = useAuthStore();
const { coverQuality, setCoverQuality } = useSettingsStore();
const { darkMode, setDarkMode, trafficSaving, setTrafficSaving } = useSettingsStore();
const theme = useTheme();
const { currentVersion, isChecking, downloadProgress, checkUpdate } = useCheckUpdate();
const handleLogout = async () => {
@@ -19,25 +21,25 @@ export default function SettingsScreen() {
};
return (
<SafeAreaView style={styles.safe}>
<View style={styles.topBar}>
<SafeAreaView style={[styles.safe, { backgroundColor: theme.bg }]}>
<View style={[styles.topBar, { backgroundColor: theme.card, borderBottomColor: theme.border }]}>
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
<Ionicons name="chevron-back" size={24} color="#212121" />
<Ionicons name="chevron-back" size={24} color={theme.text} />
</TouchableOpacity>
<Text style={styles.topTitle}></Text>
<Text style={[styles.topTitle, { color: theme.text }]}></Text>
<View style={styles.spacer} />
</View>
<View style={styles.section}>
<Text style={styles.sectionLabel}></Text>
<View style={[styles.section, { backgroundColor: theme.card, borderColor: theme.border }]}>
<Text style={[styles.sectionLabel, { color: theme.textSub }]}></Text>
<View style={styles.versionRow}>
<Text style={styles.versionLabel}></Text>
<Text style={styles.versionValue}>v{currentVersion}</Text>
<Text style={[styles.versionLabel, { color: theme.text }]}></Text>
<Text style={[styles.versionValue, { color: theme.textSub }]}>v{currentVersion}</Text>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionLabel}></Text>
<View style={[styles.section, { backgroundColor: theme.card, borderColor: theme.border }]}>
<Text style={[styles.sectionLabel, { color: theme.textSub }]}></Text>
<TouchableOpacity
style={styles.updateBtn}
onPress={checkUpdate}
@@ -57,30 +59,51 @@ export default function SettingsScreen() {
</TouchableOpacity>
</View>
<View style={styles.section}>
<Text style={styles.sectionLabel}></Text>
<View style={[styles.section, { backgroundColor: theme.card, borderColor: theme.border }]}>
<Text style={[styles.sectionLabel, { color: theme.textSub }]}></Text>
<View style={styles.optionRow}>
<TouchableOpacity
style={[styles.option, coverQuality === 'hd' && styles.optionActive]}
onPress={() => setCoverQuality('hd')}
style={[styles.option, !darkMode && styles.optionActive]}
onPress={() => setDarkMode(false)}
activeOpacity={0.7}
>
<Text style={[styles.optionText, coverQuality === 'hd' && styles.optionTextActive]}>
</Text>
<Text style={[styles.optionText, !darkMode && styles.optionTextActive]}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.option, coverQuality === 'normal' && styles.optionActive]}
onPress={() => setCoverQuality('normal')}
style={[styles.option, darkMode && styles.optionActive]}
onPress={() => setDarkMode(true)}
activeOpacity={0.7}
>
<Text style={[styles.optionText, coverQuality === 'normal' && styles.optionTextActive]}>
</Text>
<Text style={[styles.optionText, darkMode && styles.optionTextActive]}></Text>
</TouchableOpacity>
</View>
</View>
<View style={[styles.section, { backgroundColor: theme.card, borderColor: theme.border }]}>
<Text style={[styles.sectionLabel, { color: theme.textSub }]}></Text>
<View style={styles.optionRow}>
<TouchableOpacity
style={[styles.option, !trafficSaving && styles.optionActive]}
onPress={() => setTrafficSaving(false)}
activeOpacity={0.7}
>
<Text style={[styles.optionText, !trafficSaving && styles.optionTextActive]}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.option, trafficSaving && styles.optionActive]}
onPress={() => setTrafficSaving(true)}
activeOpacity={0.7}
>
<Text style={[styles.optionText, trafficSaving && styles.optionTextActive]}></Text>
</TouchableOpacity>
</View>
{trafficSaving && (
<Text style={[styles.hint, { color: theme.textSub }]}>
· · 360p
</Text>
)}
</View>
{isLoggedIn && (
<TouchableOpacity style={styles.logoutBtn} onPress={handleLogout} activeOpacity={0.8}>
<Text style={styles.logoutText}>退</Text>
@@ -91,15 +114,13 @@ export default function SettingsScreen() {
}
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: '#f4f4f4' },
safe: { flex: 1 },
topBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingVertical: 8,
backgroundColor: '#fff',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#eee',
},
backBtn: { padding: 4, width: 32 },
spacer: { width: 32 },
@@ -107,20 +128,16 @@ const styles = StyleSheet.create({
flex: 1,
fontSize: 16,
fontWeight: '600',
color: '#212121',
textAlign: 'center',
},
section: {
backgroundColor: '#fff',
marginTop: 16,
paddingHorizontal: 16,
paddingVertical: 14,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: '#eee',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#eee',
},
sectionLabel: { fontSize: 13, color: '#999', marginBottom: 10 },
sectionLabel: { fontSize: 13, marginBottom: 10 },
optionRow: { flexDirection: 'row', gap: 10 },
option: {
paddingHorizontal: 20,
@@ -128,18 +145,19 @@ const styles = StyleSheet.create({
borderRadius: 20,
borderWidth: 1,
borderColor: '#e0e0e0',
backgroundColor: '#fff',
backgroundColor: 'transparent',
},
optionActive: { borderColor: '#00AEEC', backgroundColor: '#e8f7fd' },
optionText: { fontSize: 14, color: '#666' },
optionTextActive: { color: '#00AEEC', fontWeight: '600' },
hint: { fontSize: 12, marginTop: 8 },
versionRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
versionLabel: { fontSize: 14, color: '#212121' },
versionValue: { fontSize: 14, color: '#999' },
versionLabel: { fontSize: 14 },
versionValue: { fontSize: 14 },
updateBtn: {
flexDirection: 'row',
alignItems: 'center',
@@ -150,7 +168,6 @@ const styles = StyleSheet.create({
margin: 24,
paddingVertical: 12,
borderRadius: 8,
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#ff4757',
alignItems: 'center',