mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-08 23:43:52 +08:00
chore: merge master-bug → master (v1.0.8 暗黑模式 + 节流模式 + 本地二维码)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -49,4 +49,5 @@ docs/
|
|||||||
feature.md
|
feature.md
|
||||||
livePlan.md
|
livePlan.md
|
||||||
Promotion.md
|
Promotion.md
|
||||||
|
wordsFilter.md
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -5,6 +5,19 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.0.8] - 2026-03-24
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
- **暗黑模式**:全局主题系统(`utils/theme.ts`),支持亮色 / 暗色一键切换,覆盖所有页面和组件
|
||||||
|
- **节流模式**:设置页新增流量节省开关,开启后使用低画质封面、首页视频不自动播放、视频默认 360p 画质
|
||||||
|
- **本地二维码生成**:登录二维码改用 `react-native-qrcode-svg` 本地渲染,移除对 `api.qrserver.com` 的外部依赖,提升可靠性
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- **SeasonSection 背景色**:合集组件背景色与父容器不一致,现跟随主题色 (`theme.card`) 正确显示
|
||||||
|
- **推荐列表 Loading 状态**:空列表加载中未显示 spinner(`ListEmptyComponent` 条件逻辑反转)
|
||||||
|
- **合集滚动定位偏移**:`getItemLayout` offset 计算未计入卡片间距(`gap: 10`),导致 `scrollToIndex` 定位不准
|
||||||
|
- **推荐视频卡片双边框**:相邻推荐视频卡片之间出现双分割线
|
||||||
|
|
||||||
## [1.0.0] — 2026-03-20
|
## [1.0.0] — 2026-03-20
|
||||||
|
|
||||||
### 首个正式版本
|
### 首个正式版本
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useEffect } from 'react';
|
|||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
import { useDownloadStore } from '../store/downloadStore';
|
import { useDownloadStore } from '../store/downloadStore';
|
||||||
import { useSettingsStore } from '../store/settingsStore';
|
import { useSettingsStore } from '../store/settingsStore';
|
||||||
|
import { useTheme } from '../utils/theme';
|
||||||
import { MiniPlayer } from '../components/MiniPlayer';
|
import { MiniPlayer } from '../components/MiniPlayer';
|
||||||
import * as Sentry from '@sentry/react-native';
|
import * as Sentry from '@sentry/react-native';
|
||||||
import { ErrorBoundary } from '@sentry/react-native';
|
import { ErrorBoundary } from '@sentry/react-native';
|
||||||
@@ -21,6 +22,7 @@ function RootLayout() {
|
|||||||
const restore = useAuthStore(s => s.restore);
|
const restore = useAuthStore(s => s.restore);
|
||||||
const loadDownloads = useDownloadStore(s => s.loadFromStorage);
|
const loadDownloads = useDownloadStore(s => s.loadFromStorage);
|
||||||
const restoreSettings = useSettingsStore(s => s.restore);
|
const restoreSettings = useSettingsStore(s => s.restore);
|
||||||
|
const darkMode = useSettingsStore(s => s.darkMode);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
restore();
|
restore();
|
||||||
@@ -31,7 +33,7 @@ function RootLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<StatusBar style="dark" />
|
<StatusBar style={darkMode ? 'light' : 'dark'} />
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<ErrorBoundary fallback={<Text style={{ padding: 32, textAlign: 'center' }}>发生错误,请重启 App</Text>}>
|
<ErrorBoundary fallback={<Text style={{ padding: 32, textAlign: 'center' }}>发生错误,请重启 App</Text>}>
|
||||||
<Stack screenOptions={{ headerShown: false }}>
|
<Stack screenOptions={{ headerShown: false }}>
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ function formatFileSize(bytes?: number): string {
|
|||||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||||
}
|
}
|
||||||
import { proxyImageUrl } from '../utils/imageUrl';
|
import { proxyImageUrl } from '../utils/imageUrl';
|
||||||
|
import { useTheme } from '../utils/theme';
|
||||||
|
|
||||||
export default function DownloadsScreen() {
|
export default function DownloadsScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const theme = useTheme();
|
||||||
const { tasks, loadFromStorage, removeTask } = useDownloadStore();
|
const { tasks, loadFromStorage, removeTask } = useDownloadStore();
|
||||||
const [playingUri, setPlayingUri] = useState<string | null>(null);
|
const [playingUri, setPlayingUri] = useState<string | null>(null);
|
||||||
const [playingTitle, setPlayingTitle] = useState('');
|
const [playingTitle, setPlayingTitle] = useState('');
|
||||||
@@ -61,12 +63,12 @@ export default function DownloadsScreen() {
|
|||||||
if (done.length > 0) sections.push({ title: '已下载', data: done });
|
if (done.length > 0) sections.push({ title: '已下载', data: done });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe}>
|
<SafeAreaView style={[styles.safe, { backgroundColor: theme.bg }]}>
|
||||||
<View style={styles.topBar}>
|
<View style={[styles.topBar, { backgroundColor: theme.card, borderBottomColor: theme.border }]}>
|
||||||
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
<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>
|
</TouchableOpacity>
|
||||||
<Text style={styles.topTitle}>我的下载</Text>
|
<Text style={[styles.topTitle, { color: theme.text }]}>我的下载</Text>
|
||||||
<View style={{ width: 32 }} />
|
<View style={{ width: 32 }} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
} from "../utils/videoRows";
|
} from "../utils/videoRows";
|
||||||
import { BigVideoCard } from "../components/BigVideoCard";
|
import { BigVideoCard } from "../components/BigVideoCard";
|
||||||
import { FollowedLiveStrip } from "../components/FollowedLiveStrip";
|
import { FollowedLiveStrip } from "../components/FollowedLiveStrip";
|
||||||
|
import { useTheme } from "../utils/theme";
|
||||||
import type { LiveRoom } from "../services/types";
|
import type { LiveRoom } from "../services/types";
|
||||||
|
|
||||||
const HEADER_H = 44;
|
const HEADER_H = 44;
|
||||||
@@ -83,6 +84,7 @@ export default function HomeScreen() {
|
|||||||
const [activeTab, setActiveTab] = useState<TabKey>("hot");
|
const [activeTab, setActiveTab] = useState<TabKey>("hot");
|
||||||
const [liveAreaId, setLiveAreaId] = useState(0);
|
const [liveAreaId, setLiveAreaId] = useState(0);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
const [visibleBigKey, setVisibleBigKey] = useState<string | null>(null);
|
const [visibleBigKey, setVisibleBigKey] = useState<string | null>(null);
|
||||||
const rows = useMemo(() => toListRows(pages, liveRooms), [pages, liveRooms]);
|
const rows = useMemo(() => toListRows(pages, liveRooms), [pages, liveRooms]);
|
||||||
const pagerRef = useRef<PagerView>(null);
|
const pagerRef = useRef<PagerView>(null);
|
||||||
@@ -286,7 +288,7 @@ export default function HomeScreen() {
|
|||||||
activeTab === "hot" ? headerOpacity : liveHeaderOpacity;
|
activeTab === "hot" ? headerOpacity : liveHeaderOpacity;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe} edges={["left", "right"]}>
|
<SafeAreaView style={[styles.safe, { backgroundColor: theme.bg }]} edges={["left", "right"]}>
|
||||||
{/* 滑动切换容器 */}
|
{/* 滑动切换容器 */}
|
||||||
<PagerView
|
<PagerView
|
||||||
ref={pagerRef}
|
ref={pagerRef}
|
||||||
@@ -410,6 +412,7 @@ export default function HomeScreen() {
|
|||||||
styles.navBar,
|
styles.navBar,
|
||||||
{
|
{
|
||||||
paddingTop: insets.top,
|
paddingTop: insets.top,
|
||||||
|
backgroundColor: theme.card,
|
||||||
transform: [{ translateY: currentHeaderTranslate }],
|
transform: [{ translateY: currentHeaderTranslate }],
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@@ -419,6 +422,7 @@ export default function HomeScreen() {
|
|||||||
styles.header,
|
styles.header,
|
||||||
{
|
{
|
||||||
opacity: currentHeaderOpacity,
|
opacity: currentHeaderOpacity,
|
||||||
|
borderBottomColor: theme.border,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -439,19 +443,19 @@ export default function HomeScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.searchBar}
|
style={[styles.searchBar, { backgroundColor: theme.inputBg }]}
|
||||||
onPress={() => router.push("/search" as any)}
|
onPress={() => router.push("/search" as any)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Ionicons name="search" size={14} color="#999" />
|
<Ionicons name="search" size={14} color={theme.textSub} />
|
||||||
<Text style={styles.searchPlaceholder}>搜索视频、UP主...</Text>
|
<Text style={[styles.searchPlaceholder, { color: theme.textSub }]}>搜索视频、UP主...</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<DownloadProgressBtn
|
<DownloadProgressBtn
|
||||||
onPress={() => router.push("/downloads" as any)}
|
onPress={() => router.push("/downloads" as any)}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
<View style={styles.tabRow}>
|
<View style={[styles.tabRow, { backgroundColor: theme.card }]}>
|
||||||
{TABS.map((tab) => (
|
{TABS.map((tab) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
@@ -462,6 +466,7 @@ export default function HomeScreen() {
|
|||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.tabText,
|
styles.tabText,
|
||||||
|
{ color: theme.textSub },
|
||||||
activeTab === tab.key && styles.tabTextActive,
|
activeTab === tab.key && styles.tabTextActive,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ import { LivePlayer } from "../../components/LivePlayer";
|
|||||||
import DanmakuList from "../../components/DanmakuList";
|
import DanmakuList from "../../components/DanmakuList";
|
||||||
import { formatCount } from "../../utils/format";
|
import { formatCount } from "../../utils/format";
|
||||||
import { proxyImageUrl } from "../../utils/imageUrl";
|
import { proxyImageUrl } from "../../utils/imageUrl";
|
||||||
|
import { useTheme } from "../../utils/theme";
|
||||||
|
|
||||||
type Tab = "intro" | "danmaku";
|
type Tab = "intro" | "danmaku";
|
||||||
|
|
||||||
export default function LiveDetailScreen() {
|
export default function LiveDetailScreen() {
|
||||||
const { roomId } = useLocalSearchParams<{ roomId: string }>();
|
const { roomId } = useLocalSearchParams<{ roomId: string }>();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const theme = useTheme();
|
||||||
const id = parseInt(roomId ?? "0", 10);
|
const id = parseInt(roomId ?? "0", 10);
|
||||||
const { room, anchor, stream, loading, error, changeQuality } =
|
const { room, anchor, stream, loading, error, changeQuality } =
|
||||||
useLiveDetail(id);
|
useLiveDetail(id);
|
||||||
@@ -34,17 +36,17 @@ export default function LiveDetailScreen() {
|
|||||||
const qualities = stream?.qualities ?? [];
|
const qualities = stream?.qualities ?? [];
|
||||||
const currentQn = stream?.qn ?? 0;
|
const currentQn = stream?.qn ?? 0;
|
||||||
|
|
||||||
// Use actual roomid from room detail (not the short/alias ID from the URL)
|
|
||||||
const actualRoomId = room?.roomid ?? id;
|
const actualRoomId = room?.roomid ?? id;
|
||||||
const { danmakus, giftCounts } = useLiveDanmaku(isLive ? actualRoomId : 0);
|
const { danmakus, giftCounts } = useLiveDanmaku(isLive ? actualRoomId : 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe}>
|
<SafeAreaView style={[styles.safe, { backgroundColor: theme.card }]}>
|
||||||
{/* TopBar */}
|
{/* TopBar */}
|
||||||
<View style={styles.topBar}>
|
<View style={[styles.topBar, { borderBottomColor: theme.border }]}>
|
||||||
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
<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>
|
</TouchableOpacity>
|
||||||
<Text style={styles.topTitle} numberOfLines={1}>
|
<Text style={[styles.topTitle, { color: theme.text }]} numberOfLines={1}>
|
||||||
{room?.title ?? "直播间"}
|
{room?.title ?? "直播间"}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -60,12 +62,12 @@ export default function LiveDetailScreen() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* TabBar */}
|
{/* TabBar */}
|
||||||
<View style={styles.tabBar}>
|
<View style={[styles.tabBar, { backgroundColor: theme.card, borderBottomColor: theme.border }]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.tabItem}
|
style={styles.tabItem}
|
||||||
onPress={() => setTab("intro")}
|
onPress={() => setTab("intro")}
|
||||||
>
|
>
|
||||||
<Text style={[styles.tabLabel, tab === "intro" && styles.tabActive]}>
|
<Text style={[styles.tabLabel, { color: theme.textSub }, tab === "intro" && styles.tabActive]}>
|
||||||
简介
|
简介
|
||||||
</Text>
|
</Text>
|
||||||
{tab === "intro" && <View style={styles.tabUnderline} />}
|
{tab === "intro" && <View style={styles.tabUnderline} />}
|
||||||
@@ -75,7 +77,7 @@ export default function LiveDetailScreen() {
|
|||||||
onPress={() => setTab("danmaku")}
|
onPress={() => setTab("danmaku")}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[styles.tabLabel, tab === "danmaku" && styles.tabActive]}
|
style={[styles.tabLabel, { color: theme.textSub }, tab === "danmaku" && styles.tabActive]}
|
||||||
>
|
>
|
||||||
弹幕{danmakus.length > 0 ? ` ${danmakus.length}` : ""}
|
弹幕{danmakus.length > 0 ? ` ${danmakus.length}` : ""}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -83,7 +85,7 @@ export default function LiveDetailScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Content — 两个面板始终挂载,通过 display 切换,保留弹幕列表状态 */}
|
{/* Content */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
||||||
) : error ? (
|
) : error ? (
|
||||||
@@ -95,7 +97,7 @@ export default function LiveDetailScreen() {
|
|||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<View style={styles.titleSection}>
|
<View style={styles.titleSection}>
|
||||||
<Text style={styles.title}>{room?.title}</Text>
|
<Text style={[styles.title, { color: theme.text }]}>{room?.title}</Text>
|
||||||
<View style={styles.metaRow}>
|
<View style={styles.metaRow}>
|
||||||
{isLive ? (
|
{isLive ? (
|
||||||
<View style={styles.livePill}>
|
<View style={styles.livePill}>
|
||||||
@@ -126,7 +128,7 @@ export default function LiveDetailScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.divider} />
|
<View style={[styles.divider, { backgroundColor: theme.border }]} />
|
||||||
|
|
||||||
{anchor && (
|
{anchor && (
|
||||||
<View style={styles.anchorRow}>
|
<View style={styles.anchorRow}>
|
||||||
@@ -134,7 +136,7 @@ export default function LiveDetailScreen() {
|
|||||||
source={{ uri: proxyImageUrl(anchor.face) }}
|
source={{ uri: proxyImageUrl(anchor.face) }}
|
||||||
style={styles.avatar}
|
style={styles.avatar}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.anchorName}>{anchor.uname}</Text>
|
<Text style={[styles.anchorName, { color: theme.text }]}>{anchor.uname}</Text>
|
||||||
<TouchableOpacity style={styles.followBtn}>
|
<TouchableOpacity style={styles.followBtn}>
|
||||||
<Text style={styles.followTxt}>+ 关注</Text>
|
<Text style={styles.followTxt}>+ 关注</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -143,7 +145,7 @@ export default function LiveDetailScreen() {
|
|||||||
|
|
||||||
{!!room?.description && (
|
{!!room?.description && (
|
||||||
<View style={styles.descBox}>
|
<View style={styles.descBox}>
|
||||||
<Text style={styles.descText}>{room.description}</Text>
|
<Text style={[styles.descText, { color: theme.text }]}>{room.description}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -166,14 +168,13 @@ export default function LiveDetailScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
safe: { flex: 1, backgroundColor: "#fff" },
|
safe: { flex: 1 },
|
||||||
topBar: {
|
topBar: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: "#eee",
|
|
||||||
},
|
},
|
||||||
backBtn: { padding: 4 },
|
backBtn: { padding: 4 },
|
||||||
topTitle: {
|
topTitle: {
|
||||||
@@ -181,15 +182,12 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
color: "#212121",
|
|
||||||
},
|
},
|
||||||
loader: { marginVertical: 30 },
|
loader: { marginVertical: 30 },
|
||||||
errorText: { textAlign: "center", color: "#f00", padding: 20 },
|
errorText: { textAlign: "center", color: "#f00", padding: 20 },
|
||||||
tabBar: {
|
tabBar: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: "#eee",
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
},
|
},
|
||||||
tabItem: {
|
tabItem: {
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
@@ -197,7 +195,7 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
position: "relative",
|
position: "relative",
|
||||||
},
|
},
|
||||||
tabLabel: { fontSize: 14, color: "#999", fontWeight: "500" },
|
tabLabel: { fontSize: 14, fontWeight: "500" },
|
||||||
tabActive: { color: "#00AEEC", fontWeight: "700" },
|
tabActive: { color: "#00AEEC", fontWeight: "700" },
|
||||||
tabUnderline: {
|
tabUnderline: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -212,7 +210,6 @@ const styles = StyleSheet.create({
|
|||||||
title: {
|
title: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
color: "#212121",
|
|
||||||
lineHeight: 22,
|
lineHeight: 22,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
@@ -248,7 +245,6 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
divider: {
|
divider: {
|
||||||
height: StyleSheet.hairlineWidth,
|
height: StyleSheet.hairlineWidth,
|
||||||
backgroundColor: "#f0f0f0",
|
|
||||||
marginHorizontal: 14,
|
marginHorizontal: 14,
|
||||||
},
|
},
|
||||||
anchorRow: {
|
anchorRow: {
|
||||||
@@ -258,7 +254,7 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
},
|
},
|
||||||
avatar: { width: 40, height: 40, borderRadius: 20, marginRight: 10 },
|
avatar: { width: 40, height: 40, borderRadius: 20, marginRight: 10 },
|
||||||
anchorName: { flex: 1, fontSize: 14, color: "#212121", fontWeight: "500" },
|
anchorName: { flex: 1, fontSize: 14, fontWeight: "500" },
|
||||||
followBtn: {
|
followBtn: {
|
||||||
backgroundColor: "#00AEEC",
|
backgroundColor: "#00AEEC",
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
@@ -267,7 +263,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
followTxt: { color: "#fff", fontSize: 12, fontWeight: "600" },
|
followTxt: { color: "#fff", fontSize: 12, fontWeight: "600" },
|
||||||
descBox: { padding: 14, paddingTop: 4 },
|
descBox: { padding: 14, paddingTop: 4 },
|
||||||
descText: { fontSize: 14, color: "#555", lineHeight: 22 },
|
descText: { fontSize: 14, lineHeight: 22 },
|
||||||
danmakuFull: { flex: 1 },
|
danmakuFull: { flex: 1 },
|
||||||
hidden: { display: "none" },
|
hidden: { display: "none" },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import { useRouter } from 'expo-router';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { VideoCard } from '../components/VideoCard';
|
import { VideoCard } from '../components/VideoCard';
|
||||||
import { useSearch } from '../hooks/useSearch';
|
import { useSearch } from '../hooks/useSearch';
|
||||||
|
import { useTheme } from '../utils/theme';
|
||||||
import type { VideoItem } from '../services/types';
|
import type { VideoItem } from '../services/types';
|
||||||
|
|
||||||
export default function SearchScreen() {
|
export default function SearchScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { keyword, setKeyword, results, loading, hasMore, search, loadMore } = useSearch();
|
const { keyword, setKeyword, results, loading, hasMore, search, loadMore } = useSearch();
|
||||||
|
const theme = useTheme();
|
||||||
const inputRef = useRef<TextInput>(null);
|
const inputRef = useRef<TextInput>(null);
|
||||||
|
|
||||||
const handleSearch = useCallback(() => {
|
const handleSearch = useCallback(() => {
|
||||||
@@ -64,7 +66,7 @@ export default function SearchScreen() {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.emptyBox}>
|
<View style={styles.emptyBox}>
|
||||||
<Ionicons name="search-outline" size={48} color="#ddd" />
|
<Ionicons name="search-outline" size={48} color="#ddd" />
|
||||||
<Text style={styles.emptyText}>
|
<Text style={[styles.emptyText, { color: theme.textSub }]}>
|
||||||
{results.length === 0 && keyword.trim()
|
{results.length === 0 && keyword.trim()
|
||||||
? '没有找到相关视频'
|
? '没有找到相关视频'
|
||||||
: '输入关键词搜索'}
|
: '输入关键词搜索'}
|
||||||
@@ -74,16 +76,16 @@ export default function SearchScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe} edges={['top', 'left', 'right']}>
|
<SafeAreaView style={[styles.safe, { backgroundColor: theme.bg }]} edges={['top', 'left', 'right']}>
|
||||||
{/* Search header */}
|
{/* Search header */}
|
||||||
<View style={styles.header}>
|
<View style={[styles.header, { backgroundColor: theme.card, borderBottomColor: theme.border }]}>
|
||||||
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
<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>
|
</TouchableOpacity>
|
||||||
<View style={styles.inputWrap}>
|
<View style={[styles.inputWrap, { backgroundColor: theme.inputBg }]}>
|
||||||
<TextInput
|
<TextInput
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
style={styles.input}
|
style={[styles.input, { color: theme.text }]}
|
||||||
placeholder="搜索视频、UP主..."
|
placeholder="搜索视频、UP主..."
|
||||||
placeholderTextColor="#999"
|
placeholderTextColor="#999"
|
||||||
value={keyword}
|
value={keyword}
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import { useRouter } from 'expo-router';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
import { useSettingsStore } from '../store/settingsStore';
|
import { useSettingsStore } from '../store/settingsStore';
|
||||||
|
import { useTheme } from '../utils/theme';
|
||||||
import { useCheckUpdate } from '../hooks/useCheckUpdate';
|
import { useCheckUpdate } from '../hooks/useCheckUpdate';
|
||||||
|
|
||||||
export default function SettingsScreen() {
|
export default function SettingsScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isLoggedIn, logout } = useAuthStore();
|
const { isLoggedIn, logout } = useAuthStore();
|
||||||
const { coverQuality, setCoverQuality } = useSettingsStore();
|
const { darkMode, setDarkMode, trafficSaving, setTrafficSaving } = useSettingsStore();
|
||||||
|
const theme = useTheme();
|
||||||
const { currentVersion, isChecking, downloadProgress, checkUpdate } = useCheckUpdate();
|
const { currentVersion, isChecking, downloadProgress, checkUpdate } = useCheckUpdate();
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
@@ -19,25 +21,25 @@ export default function SettingsScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe}>
|
<SafeAreaView style={[styles.safe, { backgroundColor: theme.bg }]}>
|
||||||
<View style={styles.topBar}>
|
<View style={[styles.topBar, { backgroundColor: theme.card, borderBottomColor: theme.border }]}>
|
||||||
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
<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>
|
</TouchableOpacity>
|
||||||
<Text style={styles.topTitle}>设置</Text>
|
<Text style={[styles.topTitle, { color: theme.text }]}>设置</Text>
|
||||||
<View style={styles.spacer} />
|
<View style={styles.spacer} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.section}>
|
<View style={[styles.section, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||||
<Text style={styles.sectionLabel}>版本信息</Text>
|
<Text style={[styles.sectionLabel, { color: theme.textSub }]}>版本信息</Text>
|
||||||
<View style={styles.versionRow}>
|
<View style={styles.versionRow}>
|
||||||
<Text style={styles.versionLabel}>当前版本</Text>
|
<Text style={[styles.versionLabel, { color: theme.text }]}>当前版本</Text>
|
||||||
<Text style={styles.versionValue}>v{currentVersion}</Text>
|
<Text style={[styles.versionValue, { color: theme.textSub }]}>v{currentVersion}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.section}>
|
<View style={[styles.section, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||||
<Text style={styles.sectionLabel}>更新</Text>
|
<Text style={[styles.sectionLabel, { color: theme.textSub }]}>更新</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.updateBtn}
|
style={styles.updateBtn}
|
||||||
onPress={checkUpdate}
|
onPress={checkUpdate}
|
||||||
@@ -57,30 +59,51 @@ export default function SettingsScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.section}>
|
<View style={[styles.section, { backgroundColor: theme.card, borderColor: theme.border }]}>
|
||||||
<Text style={styles.sectionLabel}>封面图清晰度</Text>
|
<Text style={[styles.sectionLabel, { color: theme.textSub }]}>外观</Text>
|
||||||
<View style={styles.optionRow}>
|
<View style={styles.optionRow}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.option, coverQuality === 'hd' && styles.optionActive]}
|
style={[styles.option, !darkMode && styles.optionActive]}
|
||||||
onPress={() => setCoverQuality('hd')}
|
onPress={() => setDarkMode(false)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text style={[styles.optionText, coverQuality === 'hd' && styles.optionTextActive]}>
|
<Text style={[styles.optionText, !darkMode && styles.optionTextActive]}>浅色</Text>
|
||||||
高清
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.option, coverQuality === 'normal' && styles.optionActive]}
|
style={[styles.option, darkMode && styles.optionActive]}
|
||||||
onPress={() => setCoverQuality('normal')}
|
onPress={() => setDarkMode(true)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<Text style={[styles.optionText, coverQuality === 'normal' && styles.optionTextActive]}>
|
<Text style={[styles.optionText, darkMode && styles.optionTextActive]}>深色</Text>
|
||||||
普通
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</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 && (
|
{isLoggedIn && (
|
||||||
<TouchableOpacity style={styles.logoutBtn} onPress={handleLogout} activeOpacity={0.8}>
|
<TouchableOpacity style={styles.logoutBtn} onPress={handleLogout} activeOpacity={0.8}>
|
||||||
<Text style={styles.logoutText}>退出登录</Text>
|
<Text style={styles.logoutText}>退出登录</Text>
|
||||||
@@ -91,15 +114,13 @@ export default function SettingsScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
safe: { flex: 1, backgroundColor: '#f4f4f4' },
|
safe: { flex: 1 },
|
||||||
topBar: {
|
topBar: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
backgroundColor: '#fff',
|
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: '#eee',
|
|
||||||
},
|
},
|
||||||
backBtn: { padding: 4, width: 32 },
|
backBtn: { padding: 4, width: 32 },
|
||||||
spacer: { width: 32 },
|
spacer: { width: 32 },
|
||||||
@@ -107,20 +128,16 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#212121',
|
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
section: {
|
section: {
|
||||||
backgroundColor: '#fff',
|
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
borderTopColor: '#eee',
|
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: '#eee',
|
|
||||||
},
|
},
|
||||||
sectionLabel: { fontSize: 13, color: '#999', marginBottom: 10 },
|
sectionLabel: { fontSize: 13, marginBottom: 10 },
|
||||||
optionRow: { flexDirection: 'row', gap: 10 },
|
optionRow: { flexDirection: 'row', gap: 10 },
|
||||||
option: {
|
option: {
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
@@ -128,18 +145,19 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e0e0e0',
|
borderColor: '#e0e0e0',
|
||||||
backgroundColor: '#fff',
|
backgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
optionActive: { borderColor: '#00AEEC', backgroundColor: '#e8f7fd' },
|
optionActive: { borderColor: '#00AEEC', backgroundColor: '#e8f7fd' },
|
||||||
optionText: { fontSize: 14, color: '#666' },
|
optionText: { fontSize: 14, color: '#666' },
|
||||||
optionTextActive: { color: '#00AEEC', fontWeight: '600' },
|
optionTextActive: { color: '#00AEEC', fontWeight: '600' },
|
||||||
|
hint: { fontSize: 12, marginTop: 8 },
|
||||||
versionRow: {
|
versionRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
versionLabel: { fontSize: 14, color: '#212121' },
|
versionLabel: { fontSize: 14 },
|
||||||
versionValue: { fontSize: 14, color: '#999' },
|
versionValue: { fontSize: 14 },
|
||||||
updateBtn: {
|
updateBtn: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -150,7 +168,6 @@ const styles = StyleSheet.create({
|
|||||||
margin: 24,
|
margin: 24,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
backgroundColor: '#fff',
|
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#ff4757',
|
borderColor: '#ff4757',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ import { useRelatedVideos } from "../../hooks/useRelatedVideos";
|
|||||||
import { formatCount, formatDuration } from "../../utils/format";
|
import { formatCount, formatDuration } from "../../utils/format";
|
||||||
import { proxyImageUrl } from "../../utils/imageUrl";
|
import { proxyImageUrl } from "../../utils/imageUrl";
|
||||||
import { DownloadSheet } from "../../components/DownloadSheet";
|
import { DownloadSheet } from "../../components/DownloadSheet";
|
||||||
|
import { useTheme } from "../../utils/theme";
|
||||||
|
|
||||||
type Tab = "intro" | "comments" | "danmaku";
|
type Tab = "intro" | "comments" | "danmaku";
|
||||||
|
|
||||||
export default function VideoDetailScreen() {
|
export default function VideoDetailScreen() {
|
||||||
const { bvid } = useLocalSearchParams<{ bvid: string }>();
|
const { bvid } = useLocalSearchParams<{ bvid: string }>();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const theme = useTheme();
|
||||||
const {
|
const {
|
||||||
video,
|
video,
|
||||||
playData,
|
playData,
|
||||||
@@ -67,20 +69,27 @@ export default function VideoDetailScreen() {
|
|||||||
}, [video?.cid]);
|
}, [video?.cid]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.safe}>
|
<SafeAreaView style={[styles.safe, { backgroundColor: theme.card }]}>
|
||||||
{/* TopBar */}
|
{/* TopBar */}
|
||||||
<View style={styles.topBar}>
|
<View style={[styles.topBar, { borderBottomColor: theme.border }]}>
|
||||||
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
<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>
|
</TouchableOpacity>
|
||||||
<Text style={styles.topTitle} numberOfLines={1}>
|
<Text
|
||||||
|
style={[styles.topTitle, { color: theme.text }]}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
{video?.title ?? "视频详情"}
|
{video?.title ?? "视频详情"}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.miniBtn}
|
style={styles.miniBtn}
|
||||||
onPress={() => setShowDownload(true)}
|
onPress={() => setShowDownload(true)}
|
||||||
>
|
>
|
||||||
<Ionicons name="cloud-download-outline" size={22} color="#212121" />
|
<Ionicons
|
||||||
|
name="cloud-download-outline"
|
||||||
|
size={22}
|
||||||
|
color={theme.text}
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -105,15 +114,19 @@ export default function VideoDetailScreen() {
|
|||||||
qualities={qualities}
|
qualities={qualities}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* TabBar — sits directly below player, always visible once video loads */}
|
{/* TabBar */}
|
||||||
{video && (
|
{video && (
|
||||||
<View style={styles.tabBar}>
|
<View style={[styles.tabBar, { borderBottomColor: theme.border }]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.tabItem}
|
style={styles.tabItem}
|
||||||
onPress={() => setTab("intro")}
|
onPress={() => setTab("intro")}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[styles.tabLabel, tab === "intro" && styles.tabActive]}
|
style={[
|
||||||
|
styles.tabLabel,
|
||||||
|
{ color: theme.textSub },
|
||||||
|
tab === "intro" && styles.tabActive,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
简介
|
简介
|
||||||
</Text>
|
</Text>
|
||||||
@@ -124,7 +137,11 @@ export default function VideoDetailScreen() {
|
|||||||
onPress={() => setTab("comments")}
|
onPress={() => setTab("comments")}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[styles.tabLabel, tab === "comments" && styles.tabActive]}
|
style={[
|
||||||
|
styles.tabLabel,
|
||||||
|
{ color: theme.textSub },
|
||||||
|
tab === "comments" && styles.tabActive,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
评论
|
评论
|
||||||
{video.stat?.reply > 0 ? ` ${formatCount(video.stat.reply)}` : ""}
|
{video.stat?.reply > 0 ? ` ${formatCount(video.stat.reply)}` : ""}
|
||||||
@@ -136,7 +153,11 @@ export default function VideoDetailScreen() {
|
|||||||
onPress={() => setTab("danmaku")}
|
onPress={() => setTab("danmaku")}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[styles.tabLabel, tab === "danmaku" && styles.tabActive]}
|
style={[
|
||||||
|
styles.tabLabel,
|
||||||
|
{ color: theme.textSub },
|
||||||
|
tab === "danmaku" && styles.tabActive,
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
弹幕
|
弹幕
|
||||||
{danmakus.length > 0 ? ` ${formatCount(danmakus.length)}` : ""}
|
{danmakus.length > 0 ? ` ${formatCount(danmakus.length)}` : ""}
|
||||||
@@ -168,13 +189,25 @@ export default function VideoDetailScreen() {
|
|||||||
source={{ uri: proxyImageUrl(video.owner.face) }}
|
source={{ uri: proxyImageUrl(video.owner.face) }}
|
||||||
style={styles.avatar}
|
style={styles.avatar}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.upName}>{video.owner.name}</Text>
|
<Text style={[styles.upName, { color: theme.text }]}>
|
||||||
|
{video.owner.name}
|
||||||
|
</Text>
|
||||||
<TouchableOpacity style={styles.followBtn}>
|
<TouchableOpacity style={styles.followBtn}>
|
||||||
<Text style={styles.followTxt}>+ 关注</Text>
|
<Text style={styles.followTxt}>+ 关注</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.titleSection}>
|
<View
|
||||||
<Text style={styles.title}>{video.title}</Text>
|
style={[
|
||||||
|
styles.titleSection,
|
||||||
|
{ borderBottomColor: theme.border },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.title, { color: theme.text }]}>
|
||||||
|
{video.title}
|
||||||
|
</Text>
|
||||||
|
<Text style={[styles.subTitle, { color: theme.text }]}>
|
||||||
|
{video.desc || "暂无简介"}
|
||||||
|
</Text>
|
||||||
<View style={styles.statsRow}>
|
<View style={styles.statsRow}>
|
||||||
<StatBadge icon="play" count={video.stat.view} />
|
<StatBadge icon="play" count={video.stat.view} />
|
||||||
<StatBadge icon="heart" count={video.stat.like} />
|
<StatBadge icon="heart" count={video.stat.like} />
|
||||||
@@ -191,23 +224,40 @@ export default function VideoDetailScreen() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<View style={styles.descBox}>
|
<View
|
||||||
<Text style={styles.descText}>
|
style={[
|
||||||
{video.desc || "暂无简介"}
|
styles.relatedHeader,
|
||||||
|
{
|
||||||
|
backgroundColor: theme.card,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={[styles.relatedHeaderText, { color: theme.text }]}
|
||||||
|
>
|
||||||
|
推荐视频
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.relatedHeader}>
|
|
||||||
<Text style={styles.relatedHeaderText}>推荐视频</Text>
|
|
||||||
</View>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.relatedCard}
|
style={[
|
||||||
|
styles.relatedCard,
|
||||||
|
{
|
||||||
|
backgroundColor: theme.card,
|
||||||
|
borderBottomColor: theme.border,
|
||||||
|
},
|
||||||
|
]}
|
||||||
onPress={() => router.push(`/video/${item.bvid}` as any)}
|
onPress={() => router.push(`/video/${item.bvid}` as any)}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
<View style={styles.relatedThumbWrap}>
|
<View
|
||||||
|
style={[
|
||||||
|
styles.relatedThumbWrap,
|
||||||
|
{ backgroundColor: theme.card },
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: proxyImageUrl(item.pic) }}
|
source={{ uri: proxyImageUrl(item.pic) }}
|
||||||
style={styles.relatedThumb}
|
style={styles.relatedThumb}
|
||||||
@@ -220,7 +270,10 @@ export default function VideoDetailScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.relatedInfo}>
|
<View style={styles.relatedInfo}>
|
||||||
<Text style={styles.relatedTitle} numberOfLines={2}>
|
<Text
|
||||||
|
style={[styles.relatedTitle, { color: theme.text }]}
|
||||||
|
numberOfLines={2}
|
||||||
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
<View
|
<View
|
||||||
@@ -241,7 +294,7 @@ export default function VideoDetailScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
!relatedLoading ? (
|
relatedLoading ? (
|
||||||
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
@@ -265,7 +318,9 @@ export default function VideoDetailScreen() {
|
|||||||
onEndReachedThreshold={0.3}
|
onEndReachedThreshold={0.3}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
<View style={styles.sortRow}>
|
<View
|
||||||
|
style={[styles.sortRow, { borderBottomColor: theme.border }]}
|
||||||
|
>
|
||||||
<Text style={styles.sortLabel}>排序</Text>
|
<Text style={styles.sortLabel}>排序</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
@@ -352,17 +407,17 @@ function SeasonSection({
|
|||||||
currentBvid: string;
|
currentBvid: string;
|
||||||
onEpisodePress: (bvid: string) => void;
|
onEpisodePress: (bvid: string) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const theme = useTheme();
|
||||||
const episodes = season.sections?.[0]?.episodes ?? [];
|
const episodes = season.sections?.[0]?.episodes ?? [];
|
||||||
const currentIndex = episodes.findIndex((ep) => ep.bvid === currentBvid);
|
const currentIndex = episodes.findIndex((ep) => ep.bvid === currentBvid);
|
||||||
const listRef = useRef<FlatList>(null);
|
const listRef = useRef<FlatList>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentIndex <= 0 || episodes.length === 0) return;
|
if (currentIndex <= 0 || episodes.length === 0) return;
|
||||||
// 等布局完成再滚动
|
|
||||||
const t = setTimeout(() => {
|
const t = setTimeout(() => {
|
||||||
listRef.current?.scrollToIndex({
|
listRef.current?.scrollToIndex({
|
||||||
index: currentIndex,
|
index: currentIndex,
|
||||||
viewPosition: 0.5, // 居中
|
viewPosition: 0.5,
|
||||||
animated: false,
|
animated: false,
|
||||||
});
|
});
|
||||||
}, 200);
|
}, 200);
|
||||||
@@ -370,9 +425,16 @@ function SeasonSection({
|
|||||||
}, [currentIndex, episodes.length]);
|
}, [currentIndex, episodes.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.seasonBox}>
|
<View
|
||||||
|
style={[
|
||||||
|
styles.seasonBox,
|
||||||
|
{ borderTopColor: theme.border, backgroundColor: theme.card },
|
||||||
|
]}
|
||||||
|
>
|
||||||
<View style={styles.seasonHeader}>
|
<View style={styles.seasonHeader}>
|
||||||
<Text style={styles.seasonTitle}>合集 · {season.title}</Text>
|
<Text style={[styles.seasonTitle, { color: theme.text }]}>
|
||||||
|
合集 · {season.title}
|
||||||
|
</Text>
|
||||||
<Text style={styles.seasonCount}>{season.ep_count}个视频</Text>
|
<Text style={styles.seasonCount}>{season.ep_count}个视频</Text>
|
||||||
<Ionicons name="chevron-forward" size={14} color="#999" />
|
<Ionicons name="chevron-forward" size={14} color="#999" />
|
||||||
</View>
|
</View>
|
||||||
@@ -383,10 +445,9 @@ function SeasonSection({
|
|||||||
data={episodes}
|
data={episodes}
|
||||||
keyExtractor={(ep) => ep.bvid}
|
keyExtractor={(ep) => ep.bvid}
|
||||||
contentContainerStyle={{ paddingHorizontal: 12, gap: 10 }}
|
contentContainerStyle={{ paddingHorizontal: 12, gap: 10 }}
|
||||||
// 每个卡片宽 120,gap 10,让 FlatList 直接算任意索引的偏移量
|
|
||||||
getItemLayout={(_data, index) => ({
|
getItemLayout={(_data, index) => ({
|
||||||
length: 130,
|
length: 130,
|
||||||
offset: 12 + index * 130,
|
offset: 12 + index * 140,
|
||||||
index,
|
index,
|
||||||
})}
|
})}
|
||||||
onScrollToIndexFailed={() => {}}
|
onScrollToIndexFailed={() => {}}
|
||||||
@@ -394,20 +455,27 @@ function SeasonSection({
|
|||||||
const isCurrent = ep.bvid === currentBvid;
|
const isCurrent = ep.bvid === currentBvid;
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.epCard, isCurrent && styles.epCardActive]}
|
style={[
|
||||||
|
styles.epCard,
|
||||||
|
{ backgroundColor: theme.card },
|
||||||
|
isCurrent && styles.epCardActive,
|
||||||
|
]}
|
||||||
onPress={() => !isCurrent && onEpisodePress(ep.bvid)}
|
onPress={() => !isCurrent && onEpisodePress(ep.bvid)}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
{ep.arc?.pic && (
|
{ep.arc?.pic && (
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: proxyImageUrl(ep.arc.pic) }}
|
source={{ uri: proxyImageUrl(ep.arc.pic) }}
|
||||||
style={styles.epThumb}
|
style={[styles.epThumb, { backgroundColor: theme.card }]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Text style={[styles.epNum, isCurrent && styles.epNumActive]}>
|
<Text style={[styles.epNum, isCurrent && styles.epNumActive]}>
|
||||||
第{index + 1}集
|
第{index + 1}集
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.epTitle} numberOfLines={2}>
|
<Text
|
||||||
|
style={[styles.epTitle, { color: theme.text }]}
|
||||||
|
numberOfLines={2}
|
||||||
|
>
|
||||||
{ep.title}
|
{ep.title}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -419,14 +487,13 @@ function SeasonSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
safe: { flex: 1, backgroundColor: "#fff" },
|
safe: { flex: 1 },
|
||||||
topBar: {
|
topBar: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: "#eee",
|
|
||||||
},
|
},
|
||||||
backBtn: { padding: 4 },
|
backBtn: { padding: 4 },
|
||||||
topTitle: {
|
topTitle: {
|
||||||
@@ -434,22 +501,23 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
color: "#212121",
|
|
||||||
},
|
},
|
||||||
miniBtn: { padding: 4 },
|
miniBtn: { padding: 4 },
|
||||||
loader: { marginVertical: 30 },
|
loader: { marginVertical: 30 },
|
||||||
titleSection: {
|
titleSection: {
|
||||||
padding: 14,
|
padding: 14,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: "#f0f0f0",
|
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
color: "#212121",
|
|
||||||
lineHeight: 22,
|
lineHeight: 22,
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
|
subTitle: {
|
||||||
|
fontSize: 10,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
statsRow: { flexDirection: "row", gap: 16 },
|
statsRow: { flexDirection: "row", gap: 16 },
|
||||||
stat: { flexDirection: "row", alignItems: "center", gap: 3 },
|
stat: { flexDirection: "row", alignItems: "center", gap: 3 },
|
||||||
statText: { fontSize: 12, color: "#999" },
|
statText: { fontSize: 12, color: "#999" },
|
||||||
@@ -460,8 +528,8 @@ const styles = StyleSheet.create({
|
|||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
paddingTop: 12,
|
paddingTop: 12,
|
||||||
},
|
},
|
||||||
avatar: { width: 48, height: 48, borderRadius:30, marginRight: 10 },
|
avatar: { width: 48, height: 48, borderRadius: 30, marginRight: 10 },
|
||||||
upName: { flex: 1, fontSize: 14, color: "#212121", fontWeight: "500" },
|
upName: { flex: 1, fontSize: 14, fontWeight: "500" },
|
||||||
followBtn: {
|
followBtn: {
|
||||||
backgroundColor: "#00AEEC",
|
backgroundColor: "#00AEEC",
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
@@ -471,7 +539,6 @@ const styles = StyleSheet.create({
|
|||||||
followTxt: { color: "#fff", fontSize: 12, fontWeight: "500" },
|
followTxt: { color: "#fff", fontSize: 12, fontWeight: "500" },
|
||||||
seasonBox: {
|
seasonBox: {
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
borderTopColor: "#f0f0f0",
|
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
},
|
},
|
||||||
seasonHeader: {
|
seasonHeader: {
|
||||||
@@ -481,23 +548,21 @@ const styles = StyleSheet.create({
|
|||||||
paddingBottom: 8,
|
paddingBottom: 8,
|
||||||
gap: 4,
|
gap: 4,
|
||||||
},
|
},
|
||||||
seasonTitle: { flex: 1, fontSize: 13, fontWeight: "600", color: "#212121" },
|
seasonTitle: { flex: 1, fontSize: 13, fontWeight: "600" },
|
||||||
seasonCount: { fontSize: 12, color: "#999" },
|
seasonCount: { fontSize: 12, color: "#999" },
|
||||||
epCard: {
|
epCard: {
|
||||||
width: 120,
|
width: 120,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
backgroundColor: "#f8f8f8",
|
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
borderColor: "transparent",
|
borderColor: "transparent",
|
||||||
},
|
},
|
||||||
epCardActive: { borderColor: "#00AEEC" },
|
epCardActive: { borderColor: "#00AEEC" },
|
||||||
epThumb: { width: 120, height: 68, backgroundColor: "#eee" },
|
epThumb: { width: 120, height: 68 },
|
||||||
epNum: { fontSize: 11, color: "#999", paddingHorizontal: 6, paddingTop: 4 },
|
epNum: { fontSize: 11, color: "#999", paddingHorizontal: 6, paddingTop: 4 },
|
||||||
epNumActive: { color: "#00AEEC", fontWeight: "600" },
|
epNumActive: { color: "#00AEEC", fontWeight: "600" },
|
||||||
epTitle: {
|
epTitle: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: "#333",
|
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 6,
|
||||||
paddingBottom: 6,
|
paddingBottom: 6,
|
||||||
lineHeight: 16,
|
lineHeight: 16,
|
||||||
@@ -505,7 +570,6 @@ const styles = StyleSheet.create({
|
|||||||
tabBar: {
|
tabBar: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: "#eee",
|
|
||||||
},
|
},
|
||||||
tabItem: {
|
tabItem: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -513,7 +577,7 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
},
|
},
|
||||||
tabLabel: { fontSize: 13, color: "#999" },
|
tabLabel: { fontSize: 13 },
|
||||||
tabActive: { color: "#00AEEC" },
|
tabActive: { color: "#00AEEC" },
|
||||||
tabUnderline: {
|
tabUnderline: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -525,28 +589,23 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
tabScroll: { flex: 1 },
|
tabScroll: { flex: 1 },
|
||||||
descBox: { padding: 16 },
|
descBox: { padding: 16 },
|
||||||
descText: { fontSize: 14, color: "#555", lineHeight: 22 },
|
descText: { fontSize: 14, lineHeight: 22 },
|
||||||
danmakuTab: { flex: 1 },
|
danmakuTab: { flex: 1 },
|
||||||
emptyTxt: { textAlign: "center", color: "#bbb", padding: 30 },
|
emptyTxt: { textAlign: "center", color: "#bbb", padding: 30 },
|
||||||
relatedHeader: {
|
relatedHeader: {
|
||||||
paddingHorizontal: 14,
|
paddingLeft: 12,
|
||||||
paddingVertical: 10,
|
paddingBottom: 8,
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
paddingTop: 8,
|
||||||
borderTopColor: "#f0f0f0",
|
|
||||||
backgroundColor: "#f4f4f4",
|
|
||||||
},
|
},
|
||||||
relatedHeaderText: {
|
relatedHeaderText: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: "600" as const,
|
fontWeight: "600" as const,
|
||||||
color: "#212121",
|
|
||||||
},
|
},
|
||||||
relatedCard: {
|
relatedCard: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
backgroundColor: "#fff",
|
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: "#f0f0f0",
|
|
||||||
gap: 10,
|
gap: 10,
|
||||||
},
|
},
|
||||||
relatedThumbWrap: {
|
relatedThumbWrap: {
|
||||||
@@ -555,7 +614,6 @@ const styles = StyleSheet.create({
|
|||||||
height: 68,
|
height: 68,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
backgroundColor: "#eee",
|
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
relatedThumb: { width: 120, height: 68 },
|
relatedThumb: { width: 120, height: 68 },
|
||||||
@@ -574,7 +632,7 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
paddingVertical: 2,
|
paddingVertical: 2,
|
||||||
},
|
},
|
||||||
relatedTitle: { fontSize: 13, color: "#212121", lineHeight: 18 },
|
relatedTitle: { fontSize: 13, lineHeight: 18 },
|
||||||
relatedOwner: { fontSize: 12, color: "#999" },
|
relatedOwner: { fontSize: 12, color: "#999" },
|
||||||
relatedView: { fontSize: 11, color: "#bbb" },
|
relatedView: { fontSize: 11, color: "#bbb" },
|
||||||
sortRow: {
|
sortRow: {
|
||||||
@@ -584,7 +642,6 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
gap: 8,
|
gap: 8,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: "#f0f0f0",
|
|
||||||
},
|
},
|
||||||
sortLabel: { fontSize: 13, color: "#999", marginRight: 4 },
|
sortLabel: { fontSize: 13, color: "#999", marginRight: 4 },
|
||||||
sortBtn: {
|
sortBtn: {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { buildDashMpdUri } from "../utils/dash";
|
|||||||
import { getPlayUrl, getVideoDetail } from "../services/bilibili";
|
import { getPlayUrl, getVideoDetail } from "../services/bilibili";
|
||||||
import { coverImageUrl } from "../utils/imageUrl";
|
import { coverImageUrl } from "../utils/imageUrl";
|
||||||
import { useSettingsStore } from "../store/settingsStore";
|
import { useSettingsStore } from "../store/settingsStore";
|
||||||
|
import { useTheme } from "../utils/theme";
|
||||||
import { formatCount, formatDuration } from "../utils/format";
|
import { formatCount, formatDuration } from "../utils/format";
|
||||||
import type { VideoItem } from "../services/types";
|
import type { VideoItem } from "../services/types";
|
||||||
|
|
||||||
@@ -55,7 +56,8 @@ export const BigVideoCard = React.memo(function BigVideoCard({
|
|||||||
onPress,
|
onPress,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { width: SCREEN_W } = useWindowDimensions();
|
const { width: SCREEN_W } = useWindowDimensions();
|
||||||
const coverQuality = useSettingsStore(s => s.coverQuality);
|
const trafficSaving = useSettingsStore(s => s.trafficSaving);
|
||||||
|
const theme = useTheme();
|
||||||
const THUMB_H = SCREEN_W * 0.5625;
|
const THUMB_H = SCREEN_W * 0.5625;
|
||||||
const mediaDimensions = { width: SCREEN_W - 8, height: THUMB_H };
|
const mediaDimensions = { width: SCREEN_W - 8, height: THUMB_H };
|
||||||
|
|
||||||
@@ -90,7 +92,7 @@ export const BigVideoCard = React.memo(function BigVideoCard({
|
|||||||
|
|
||||||
// Preload: fetch play URL on mount (before card is visible)
|
// Preload: fetch play URL on mount (before card is visible)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (videoUrl) return;
|
if (videoUrl || trafficSaving) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -125,8 +127,8 @@ export const BigVideoCard = React.memo(function BigVideoCard({
|
|||||||
// Pause/resume based on visibility and scroll state
|
// Pause/resume based on visibility and scroll state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!videoUrl) return;
|
if (!videoUrl) return;
|
||||||
if (!isVisible) {
|
if (!isVisible || trafficSaving) {
|
||||||
// Off-screen: pause, mute, show thumbnail
|
// Off-screen or traffic saving: pause, mute, show thumbnail
|
||||||
setPaused(true);
|
setPaused(true);
|
||||||
setMuted(true);
|
setMuted(true);
|
||||||
Animated.timing(thumbOpacity, {
|
Animated.timing(thumbOpacity, {
|
||||||
@@ -146,10 +148,10 @@ export const BigVideoCard = React.memo(function BigVideoCard({
|
|||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
}, [isVisible, isScrolling, videoUrl]);
|
}, [isVisible, isScrolling, videoUrl, trafficSaving]);
|
||||||
|
|
||||||
const handleVideoReady = () => {
|
const handleVideoReady = () => {
|
||||||
if (!isVisible || isScrolling) return;
|
if (!isVisible || isScrolling || trafficSaving) return;
|
||||||
setPaused(false);
|
setPaused(false);
|
||||||
Animated.timing(thumbOpacity, {
|
Animated.timing(thumbOpacity, {
|
||||||
toValue: 0,
|
toValue: 0,
|
||||||
@@ -223,7 +225,7 @@ export const BigVideoCard = React.memo(function BigVideoCard({
|
|||||||
const bufferedRatio = duration > 0 ? clamp(buffered / duration, 0, 1) : 0;
|
const bufferedRatio = duration > 0 ? clamp(buffered / duration, 0, 1) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.card}>
|
<View style={[styles.card, { backgroundColor: theme.card }]}>
|
||||||
{/* Media area */}
|
{/* Media area */}
|
||||||
<View style={[mediaDimensions, { position: "relative" }]}>
|
<View style={[mediaDimensions, { position: "relative" }]}>
|
||||||
{/* Video player — rendered first so it sits behind the thumbnail */}
|
{/* Video player — rendered first so it sits behind the thumbnail */}
|
||||||
@@ -260,7 +262,7 @@ export const BigVideoCard = React.memo(function BigVideoCard({
|
|||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: coverImageUrl(item.pic, coverQuality) }}
|
source={{ uri: coverImageUrl(item.pic, trafficSaving ? 'normal' : 'hd') }}
|
||||||
style={mediaDimensions}
|
style={mediaDimensions}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
/>
|
/>
|
||||||
@@ -338,11 +340,11 @@ export const BigVideoCard = React.memo(function BigVideoCard({
|
|||||||
{/* Info */}
|
{/* Info */}
|
||||||
<TouchableOpacity onPress={onPress} activeOpacity={0.85}>
|
<TouchableOpacity onPress={onPress} activeOpacity={0.85}>
|
||||||
<View style={styles.info}>
|
<View style={styles.info}>
|
||||||
<Text style={styles.title} numberOfLines={2}>
|
<Text style={[styles.title, { color: theme.text }]} numberOfLines={2}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text style={styles.owner} numberOfLines={1}>
|
<Text style={[styles.owner, { color: theme.textSub }]} numberOfLines={1}>
|
||||||
{item.owner?.name ?? ""}
|
{item.owner?.name ?? ""}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -4,16 +4,18 @@ import { Ionicons } from '@expo/vector-icons';
|
|||||||
import type { Comment } from '../services/types';
|
import type { Comment } from '../services/types';
|
||||||
import { formatTime } from '../utils/format';
|
import { formatTime } from '../utils/format';
|
||||||
import { proxyImageUrl } from '../utils/imageUrl';
|
import { proxyImageUrl } from '../utils/imageUrl';
|
||||||
|
import { useTheme } from '../utils/theme';
|
||||||
|
|
||||||
interface Props { item: Comment; }
|
interface Props { item: Comment; }
|
||||||
|
|
||||||
export function CommentItem({ item }: Props) {
|
export function CommentItem({ item }: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<View style={styles.row}>
|
<View style={[styles.row, { borderBottomColor: theme.border }]}>
|
||||||
<Image source={{ uri: proxyImageUrl(item.member.avatar) }} style={styles.avatar} />
|
<Image source={{ uri: proxyImageUrl(item.member.avatar) }} style={styles.avatar} />
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Text style={styles.username}>{item.member.uname}</Text>
|
<Text style={styles.username}>{item.member.uname}</Text>
|
||||||
<Text style={styles.message}>{item.content.message}</Text>
|
<Text style={[styles.message, { color: theme.text }]}>{item.content.message}</Text>
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<Text style={styles.time}>{formatTime(item.ctime)}</Text>
|
<Text style={styles.time}>{formatTime(item.ctime)}</Text>
|
||||||
<View style={styles.likeRow}>
|
<View style={styles.likeRow}>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { DanmakuItem } from "../services/types";
|
import { DanmakuItem } from "../services/types";
|
||||||
import { danmakuColorToCss } from "../utils/danmaku";
|
import { danmakuColorToCss } from "../utils/danmaku";
|
||||||
|
import { useTheme } from "../utils/theme";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
danmakus: DanmakuItem[];
|
danmakus: DanmakuItem[];
|
||||||
@@ -65,6 +66,7 @@ export default function DanmakuList({
|
|||||||
maxItems = 100,
|
maxItems = 100,
|
||||||
giftCounts,
|
giftCounts,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
const flatListRef = useRef<FlatList>(null);
|
const flatListRef = useRef<FlatList>(null);
|
||||||
const [displayedItems, setDisplayedItems] = useState<DisplayedDanmaku[]>([]);
|
const [displayedItems, setDisplayedItems] = useState<DisplayedDanmaku[]>([]);
|
||||||
const [unseenCount, setUnseenCount] = useState(0);
|
const [unseenCount, setUnseenCount] = useState(0);
|
||||||
@@ -213,7 +215,7 @@ export default function DanmakuList({
|
|||||||
const guard = item.guardLevel ? GUARD_LABELS[item.guardLevel] : null;
|
const guard = item.guardLevel ? GUARD_LABELS[item.guardLevel] : null;
|
||||||
const timeStr = formatLiveTime(item.timeline);
|
const timeStr = formatLiveTime(item.timeline);
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[liveStyles.row, { opacity: item._fadeAnim }]}>
|
<Animated.View style={[liveStyles.row, { opacity: item._fadeAnim, borderBottomColor: theme.border }]}>
|
||||||
{timeStr ? (
|
{timeStr ? (
|
||||||
<Text style={liveStyles.time}>{timeStr}</Text>
|
<Text style={liveStyles.time}>{timeStr}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -240,27 +242,27 @@ export default function DanmakuList({
|
|||||||
{item.uname ?? "匿名"}
|
{item.uname ?? "匿名"}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={liveStyles.colon}>:</Text>
|
<Text style={liveStyles.colon}>:</Text>
|
||||||
<Text style={liveStyles.text} numberOfLines={2}>
|
<Text style={[liveStyles.text, { color: theme.text }]} numberOfLines={2}>
|
||||||
{item.text}
|
{item.text}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
}, []);
|
}, [theme]);
|
||||||
|
|
||||||
// ─── Video mode render (original bubble) ───────────────────────────────────
|
// ─── Video mode render (original bubble) ───────────────────────────────────
|
||||||
const renderVideoItem = useCallback(({ item }: { item: DisplayedDanmaku }) => {
|
const renderVideoItem = useCallback(({ item }: { item: DisplayedDanmaku }) => {
|
||||||
const dotColor = danmakuColorToCss(item.color);
|
const dotColor = danmakuColorToCss(item.color);
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[styles.bubble, { opacity: item._fadeAnim }]}>
|
<Animated.View style={[styles.bubble, { opacity: item._fadeAnim, backgroundColor: theme.bg }]}>
|
||||||
<View style={[styles.colorDot, { backgroundColor: dotColor }]} />
|
<View style={[styles.colorDot, { backgroundColor: dotColor }]} />
|
||||||
<Text style={styles.bubbleText} numberOfLines={3}>
|
<Text style={[styles.bubbleText, { color: theme.text }]} numberOfLines={3}>
|
||||||
{item.text}
|
{item.text}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.timestamp}>{formatTimestamp(item.time)}</Text>
|
<Text style={styles.timestamp}>{formatTimestamp(item.time)}</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
}, []);
|
}, [theme]);
|
||||||
|
|
||||||
const keyExtractor = useCallback(
|
const keyExtractor = useCallback(
|
||||||
(item: DisplayedDanmaku) => String(item._key),
|
(item: DisplayedDanmaku) => String(item._key),
|
||||||
@@ -268,7 +270,7 @@ export default function DanmakuList({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, style]}>
|
<View style={[styles.container, { backgroundColor: theme.card, borderTopColor: theme.border }, style]}>
|
||||||
{!hideHeader && (
|
{!hideHeader && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.header}
|
style={styles.header}
|
||||||
@@ -298,7 +300,7 @@ export default function DanmakuList({
|
|||||||
data={displayedItems}
|
data={displayedItems}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
renderItem={isLive ? renderLiveItem : renderVideoItem}
|
renderItem={isLive ? renderLiveItem : renderVideoItem}
|
||||||
style={isLive ? liveStyles.list : styles.list}
|
style={[isLive ? liveStyles.list : styles.list, { backgroundColor: theme.bg }]}
|
||||||
contentContainerStyle={isLive ? liveStyles.listContent : styles.listContent}
|
contentContainerStyle={isLive ? liveStyles.listContent : styles.listContent}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
onScrollBeginDrag={handleScrollBeginDrag}
|
onScrollBeginDrag={handleScrollBeginDrag}
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ import { useAuthStore } from "../store/authStore";
|
|||||||
import { getFollowedLiveRooms } from "../services/bilibili";
|
import { getFollowedLiveRooms } from "../services/bilibili";
|
||||||
import { LivePulse } from "./LivePulse";
|
import { LivePulse } from "./LivePulse";
|
||||||
import { proxyImageUrl } from "../utils/imageUrl";
|
import { proxyImageUrl } from "../utils/imageUrl";
|
||||||
|
import { useTheme } from "../utils/theme";
|
||||||
import type { LiveRoom } from "../services/types";
|
import type { LiveRoom } from "../services/types";
|
||||||
|
|
||||||
export function FollowedLiveStrip() {
|
export function FollowedLiveStrip() {
|
||||||
const { sessdata } = useAuthStore();
|
const { sessdata } = useAuthStore();
|
||||||
const [rooms, setRooms] = useState<LiveRoom[]>([]);
|
const [rooms, setRooms] = useState<LiveRoom[]>([]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sessdata) return;
|
if (!sessdata) return;
|
||||||
@@ -29,7 +31,7 @@ export function FollowedLiveStrip() {
|
|||||||
if (!sessdata || rooms.length === 0) return null;
|
if (!sessdata || rooms.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={[styles.container, { backgroundColor: theme.bg }]}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
@@ -48,9 +50,9 @@ export function FollowedLiveStrip() {
|
|||||||
</View>
|
</View>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: proxyImageUrl(room.face) }}
|
source={{ uri: proxyImageUrl(room.face) }}
|
||||||
style={styles.avatar}
|
style={[styles.avatar, { backgroundColor: theme.card }]}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.name} numberOfLines={1}>
|
<Text style={[styles.name, { color: theme.text }]} numberOfLines={1}>
|
||||||
{room.uname.length > 5 ? room.uname.slice(0, 5) : room.uname}
|
{room.uname.length > 5 ? room.uname.slice(0, 5) : room.uname}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -63,7 +65,7 @@ export function FollowedLiveStrip() {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
backgroundColor: "#f4f4f4",
|
backgroundColor: "#f4f4f4",
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 3,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { LivePulse } from "./LivePulse";
|
|||||||
import type { LiveRoom } from "../services/types";
|
import type { LiveRoom } from "../services/types";
|
||||||
import { formatCount } from "../utils/format";
|
import { formatCount } from "../utils/format";
|
||||||
import { proxyImageUrl } from "../utils/imageUrl";
|
import { proxyImageUrl } from "../utils/imageUrl";
|
||||||
|
import { useTheme } from "../utils/theme";
|
||||||
|
|
||||||
const { width } = Dimensions.get("window");
|
const { width } = Dimensions.get("window");
|
||||||
const CARD_WIDTH = (width - 14) / 2;
|
const CARD_WIDTH = (width - 14) / 2;
|
||||||
@@ -30,9 +31,10 @@ export const LiveCard = React.memo(function LiveCard({
|
|||||||
isLivePulse = false,
|
isLivePulse = false,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const cardWidth = fullWidth ? width - 8 : CARD_WIDTH;
|
const cardWidth = fullWidth ? width - 8 : CARD_WIDTH;
|
||||||
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.card, { width: cardWidth }]}
|
style={[styles.card, { width: cardWidth, backgroundColor: theme.card }]}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
@@ -41,7 +43,7 @@ export const LiveCard = React.memo(function LiveCard({
|
|||||||
source={{ uri: proxyImageUrl(item.cover) }}
|
source={{ uri: proxyImageUrl(item.cover) }}
|
||||||
style={[
|
style={[
|
||||||
styles.thumb,
|
styles.thumb,
|
||||||
{ width: cardWidth, height: cardWidth * 0.5625 },
|
{ width: cardWidth, height: cardWidth * 0.5625, backgroundColor: theme.card },
|
||||||
]}
|
]}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
/>
|
/>
|
||||||
@@ -58,7 +60,7 @@ export const LiveCard = React.memo(function LiveCard({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.info}>
|
<View style={styles.info}>
|
||||||
<Text style={styles.title} numberOfLines={2}>
|
<Text style={[styles.title, { color: theme.text }]} numberOfLines={2}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.ownerRow}>
|
<View style={styles.ownerRow}>
|
||||||
@@ -66,7 +68,7 @@ export const LiveCard = React.memo(function LiveCard({
|
|||||||
source={{ uri: proxyImageUrl(item.face) }}
|
source={{ uri: proxyImageUrl(item.face) }}
|
||||||
style={styles.avatar}
|
style={styles.avatar}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.owner} numberOfLines={1}>
|
<Text style={[styles.owner, { color: theme.textSub }]} numberOfLines={1}>
|
||||||
{item.uname}
|
{item.uname}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Image,
|
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Animated,
|
Animated,
|
||||||
Alert,
|
Alert,
|
||||||
@@ -13,6 +12,7 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import * as FileSystem from "expo-file-system/legacy";
|
import * as FileSystem from "expo-file-system/legacy";
|
||||||
import * as MediaLibrary from "expo-media-library";
|
import * as MediaLibrary from "expo-media-library";
|
||||||
|
import QRCode from "react-native-qrcode-svg";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { generateQRCode, pollQRCode, getUserInfo } from "../services/bilibili";
|
import { generateQRCode, pollQRCode, getUserInfo } from "../services/bilibili";
|
||||||
import { useAuthStore } from "../store/authStore";
|
import { useAuthStore } from "../store/authStore";
|
||||||
@@ -23,10 +23,10 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function LoginModal({ visible, onClose }: Props) {
|
export function LoginModal({ visible, onClose }: Props) {
|
||||||
const [qrUrl, setQrUrl] = useState<string | null>(null);
|
const [qrData, setQrData] = useState<string | null>(null);
|
||||||
const [qrKey, setQrKey] = useState<string | null>(null);
|
const [qrKey, setQrKey] = useState<string | null>(null);
|
||||||
const [qrImageLoaded, setQrImageLoaded] = useState(false);
|
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const qrRef = useRef<any>(null);
|
||||||
const [status, setStatus] = useState<
|
const [status, setStatus] = useState<
|
||||||
"loading" | "waiting" | "scanned" | "done" | "error"
|
"loading" | "waiting" | "scanned" | "done" | "error"
|
||||||
>("loading");
|
>("loading");
|
||||||
@@ -52,14 +52,11 @@ export function LoginModal({ visible, onClose }: Props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
setStatus("loading");
|
setStatus("loading");
|
||||||
setQrUrl(null);
|
setQrData(null);
|
||||||
setQrKey(null);
|
setQrKey(null);
|
||||||
setQrImageLoaded(false);
|
|
||||||
generateQRCode()
|
generateQRCode()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setQrUrl(
|
setQrData(data.url);
|
||||||
`https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(data.url)}&size=400x400`,
|
|
||||||
);
|
|
||||||
setQrKey(data.qrcode_key);
|
setQrKey(data.qrcode_key);
|
||||||
setStatus("waiting");
|
setStatus("waiting");
|
||||||
})
|
})
|
||||||
@@ -98,7 +95,7 @@ export function LoginModal({ visible, onClose }: Props) {
|
|||||||
}, [qrKey, status]);
|
}, [qrKey, status]);
|
||||||
|
|
||||||
async function handleSaveQR() {
|
async function handleSaveQR() {
|
||||||
if (!qrUrl) return;
|
if (!qrRef.current) return;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const { status: perm } = await MediaLibrary.requestPermissionsAsync();
|
const { status: perm } = await MediaLibrary.requestPermissionsAsync();
|
||||||
@@ -106,19 +103,28 @@ export function LoginModal({ visible, onClose }: Props) {
|
|||||||
Alert.alert("提示", "需要相册权限才能保存图片");
|
Alert.alert("提示", "需要相册权限才能保存图片");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dest = `${FileSystem.cacheDirectory}bilibili_qr.png`;
|
qrRef.current.toDataURL(async (base64: string) => {
|
||||||
const { uri } = await FileSystem.downloadAsync(qrUrl, dest);
|
try {
|
||||||
await MediaLibrary.saveToLibraryAsync(uri);
|
const dest = `${FileSystem.cacheDirectory}bilibili_qr.png`;
|
||||||
Alert.alert("已保存", "二维码已存入相册,请用哔哩哔哩扫码登录", [
|
await FileSystem.writeAsStringAsync(dest, base64, {
|
||||||
{ text: "关闭", style: "cancel" },
|
encoding: FileSystem.EncodingType.Base64,
|
||||||
{
|
});
|
||||||
text: "打开哔哩哔哩",
|
await MediaLibrary.saveToLibraryAsync(dest);
|
||||||
onPress: () => Linking.openURL("bilibili://"),
|
Alert.alert("已保存", "二维码已存入相册,请用哔哩哔哩扫码登录", [
|
||||||
},
|
{ text: "关闭", style: "cancel" },
|
||||||
]);
|
{
|
||||||
|
text: "打开哔哩哔哩",
|
||||||
|
onPress: () => Linking.openURL("bilibili://"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} catch {
|
||||||
|
Alert.alert("失败", "保存失败,请重试");
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
Alert.alert("失败", "保存失败,请重试");
|
Alert.alert("失败", "保存失败,请重试");
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,32 +152,25 @@ export function LoginModal({ visible, onClose }: Props) {
|
|||||||
style={styles.loader}
|
style={styles.loader}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(status === "waiting" || status === "scanned") && qrUrl && (
|
{(status === "waiting" || status === "scanned") && qrData && (
|
||||||
<>
|
<>
|
||||||
<View style={styles.qrWrapper}>
|
<View style={styles.qrWrapper}>
|
||||||
<Image
|
<QRCode
|
||||||
source={{ uri: qrUrl }}
|
value={qrData}
|
||||||
style={styles.qr}
|
size={200}
|
||||||
onLoad={() => setQrImageLoaded(true)}
|
getRef={(ref) => { qrRef.current = ref; }}
|
||||||
/>
|
/>
|
||||||
{!qrImageLoaded && (
|
<TouchableOpacity
|
||||||
<View style={styles.qrLoader}>
|
style={styles.saveBtn}
|
||||||
<ActivityIndicator size="large" color="#00AEEC" />
|
onPress={handleSaveQR}
|
||||||
</View>
|
disabled={saving}
|
||||||
)}
|
>
|
||||||
{qrImageLoaded && (
|
{saving ? (
|
||||||
<TouchableOpacity
|
<ActivityIndicator size="small" color="#fff" />
|
||||||
style={styles.saveBtn}
|
) : (
|
||||||
onPress={handleSaveQR}
|
<Ionicons name="download-outline" size={16} color="#fff" />
|
||||||
disabled={saving}
|
)}
|
||||||
>
|
</TouchableOpacity>
|
||||||
{saving ? (
|
|
||||||
<ActivityIndicator size="small" color="#fff" />
|
|
||||||
) : (
|
|
||||||
<Ionicons name="download-outline" size={16} color="#fff" />
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.hint}>
|
<Text style={styles.hint}>
|
||||||
{status === "scanned"
|
{status === "scanned"
|
||||||
@@ -213,13 +212,6 @@ const styles = StyleSheet.create({
|
|||||||
title: { fontSize: 18, fontWeight: "600", marginBottom: 20 },
|
title: { fontSize: 18, fontWeight: "600", marginBottom: 20 },
|
||||||
loader: { marginVertical: 40 },
|
loader: { marginVertical: 40 },
|
||||||
qrWrapper: { width: 200, height: 200, marginBottom: 12 },
|
qrWrapper: { width: 200, height: 200, marginBottom: 12 },
|
||||||
qr: { width: 200, height: 200 },
|
|
||||||
qrLoader: {
|
|
||||||
...StyleSheet.absoluteFillObject,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
backgroundColor: "#f4f4f4",
|
|
||||||
},
|
|
||||||
saveBtn: {
|
saveBtn: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: 6,
|
bottom: 6,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { VideoItem } from "../services/types";
|
|||||||
import { formatCount, formatDuration } from "../utils/format";
|
import { formatCount, formatDuration } from "../utils/format";
|
||||||
import { coverImageUrl } from "../utils/imageUrl";
|
import { coverImageUrl } from "../utils/imageUrl";
|
||||||
import { useSettingsStore } from "../store/settingsStore";
|
import { useSettingsStore } from "../store/settingsStore";
|
||||||
|
import { useTheme } from "../utils/theme";
|
||||||
|
|
||||||
const { width } = Dimensions.get("window");
|
const { width } = Dimensions.get("window");
|
||||||
const CARD_WIDTH = (width - 14) / 2;
|
const CARD_WIDTH = (width - 14) / 2;
|
||||||
@@ -22,17 +23,18 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const VideoCard = React.memo(function VideoCard({ item, onPress }: Props) {
|
export const VideoCard = React.memo(function VideoCard({ item, onPress }: Props) {
|
||||||
const coverQuality = useSettingsStore(s => s.coverQuality);
|
const trafficSaving = useSettingsStore(s => s.trafficSaving);
|
||||||
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.card}
|
style={[styles.card, { backgroundColor: theme.card }]}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
<View style={styles.thumbContainer}>
|
<View style={styles.thumbContainer}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: coverImageUrl(item.pic, coverQuality) }}
|
source={{ uri: coverImageUrl(item.pic, trafficSaving ? 'normal' : 'hd') }}
|
||||||
style={styles.thumb}
|
style={[styles.thumb, { backgroundColor: theme.card }]}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
/>
|
/>
|
||||||
<View style={styles.meta}>
|
<View style={styles.meta}>
|
||||||
@@ -48,10 +50,10 @@ export const VideoCard = React.memo(function VideoCard({ item, onPress }: Props)
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.info}>
|
<View style={styles.info}>
|
||||||
<Text style={styles.title} numberOfLines={2}>
|
<Text style={[styles.title, { color: theme.text }]} numberOfLines={2}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.owner} numberOfLines={1}>
|
<Text style={[styles.owner, { color: theme.textSub }]} numberOfLines={1}>
|
||||||
{item.owner?.name ?? ""}
|
{item.owner?.name ?? ""}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { getVideoDetail, getPlayUrl } from '../services/bilibili';
|
import { getVideoDetail, getPlayUrl } from '../services/bilibili';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '../store/authStore';
|
||||||
|
import { useSettingsStore } from '../store/settingsStore';
|
||||||
import type { VideoItem, PlayUrlResponse } from '../services/types';
|
import type { VideoItem, PlayUrlResponse } from '../services/types';
|
||||||
|
|
||||||
export function useVideoDetail(bvid: string) {
|
export function useVideoDetail(bvid: string) {
|
||||||
@@ -12,6 +13,8 @@ export function useVideoDetail(bvid: string) {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const cidRef = useRef<number>(0);
|
const cidRef = useRef<number>(0);
|
||||||
const isLoggedIn = useAuthStore(s => s.isLoggedIn);
|
const isLoggedIn = useAuthStore(s => s.isLoggedIn);
|
||||||
|
const trafficSaving = useSettingsStore(s => s.trafficSaving);
|
||||||
|
const defaultQn = trafficSaving ? 16 : 126;
|
||||||
|
|
||||||
async function fetchPlayData(cid: number, qn: number, updateList = false) {
|
async function fetchPlayData(cid: number, qn: number, updateList = false) {
|
||||||
const data = await getPlayUrl(bvid, cid, qn);
|
const data = await getPlayUrl(bvid, cid, qn);
|
||||||
@@ -39,7 +42,7 @@ export function useVideoDetail(bvid: string) {
|
|||||||
setVideo(detail);
|
setVideo(detail);
|
||||||
const cid = detail.pages?.[0]?.cid ?? detail.cid as number;
|
const cid = detail.pages?.[0]?.cid ?? detail.cid as number;
|
||||||
cidRef.current = cid;
|
cidRef.current = cid;
|
||||||
await fetchPlayData(cid, 126, true);
|
await fetchPlayData(cid, defaultQn, true);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e.message ?? 'Load failed');
|
setError(e.message ?? 'Load failed');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -52,7 +55,7 @@ export function useVideoDetail(bvid: string) {
|
|||||||
// 登录状态变化时重新拉取清晰度列表(登录后可能获得更高画质)
|
// 登录状态变化时重新拉取清晰度列表(登录后可能获得更高画质)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cidRef.current) {
|
if (cidRef.current) {
|
||||||
fetchPlayData(cidRef.current, 126, true).catch((e) => {
|
fetchPlayData(cidRef.current, defaultQn, true).catch((e) => {
|
||||||
console.warn('Failed to refresh quality list after login change:', e);
|
console.warn('Failed to refresh quality list after login change:', e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
385
package-lock.json
generated
385
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "jkvideo",
|
"name": "jkvideo",
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "jkvideo",
|
"name": "jkvideo",
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dr.pogodin/react-native-static-server": "^0.26.0",
|
"@dr.pogodin/react-native-static-server": "^0.26.0",
|
||||||
"@expo/vector-icons": "^15.0.2",
|
"@expo/vector-icons": "^15.0.2",
|
||||||
@@ -27,14 +27,15 @@
|
|||||||
"expo-screen-orientation": "~55.0.8",
|
"expo-screen-orientation": "~55.0.8",
|
||||||
"expo-status-bar": "~55.0.4",
|
"expo-status-bar": "~55.0.4",
|
||||||
"expo-system-ui": "~55.0.9",
|
"expo-system-ui": "~55.0.9",
|
||||||
"expo-updates": "~55.0.15",
|
|
||||||
"pako": "^2.1.0",
|
"pako": "^2.1.0",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-native": "0.83.2",
|
"react-native": "0.83.2",
|
||||||
"react-native-pager-view": "8.0.0",
|
"react-native-pager-view": "8.0.0",
|
||||||
|
"react-native-qrcode-svg": "^6.3.21",
|
||||||
"react-native-safe-area-context": "~5.6.2",
|
"react-native-safe-area-context": "~5.6.2",
|
||||||
"react-native-screens": "~4.23.0",
|
"react-native-screens": "~4.23.0",
|
||||||
|
"react-native-svg": "15.15.3",
|
||||||
"react-native-video": "^6.19.0",
|
"react-native-video": "^6.19.0",
|
||||||
"react-native-web": "^0.21.0",
|
"react-native-web": "^0.21.0",
|
||||||
"react-native-webview": "13.16.0",
|
"react-native-webview": "13.16.0",
|
||||||
@@ -3897,6 +3898,12 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/boolbase": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/bplist-creator": {
|
"node_modules/bplist-creator": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
||||||
@@ -4436,6 +4443,56 @@
|
|||||||
"hyphenate-style-name": "^1.0.3"
|
"hyphenate-style-name": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-select": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0",
|
||||||
|
"css-what": "^6.1.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"nth-check": "^2.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-tree": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mdn-data": "2.0.14",
|
||||||
|
"source-map": "^0.6.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-tree/node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-what": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -4460,6 +4517,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/decode-uri-component": {
|
"node_modules/decode-uri-component": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||||
@@ -4542,12 +4608,73 @@
|
|||||||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dijkstrajs": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dnssd-advertise": {
|
"node_modules/dnssd-advertise": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.3.tgz",
|
||||||
"integrity": "sha512-XENsHi3MBzWOCAXif3yZvU1Ah0l+nhJj1sjWL6TnOAYKvGiFhbTx32xHN7+wLMLUOCj7Nr0evADWG4R8JtqCDA==",
|
"integrity": "sha512-XENsHi3MBzWOCAXif3yZvU1Ah0l+nhJj1sjWL6TnOAYKvGiFhbTx32xHN7+wLMLUOCj7Nr0evADWG4R8JtqCDA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -4589,6 +4716,18 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/error-stack-parser": {
|
"node_modules/error-stack-parser": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
|
||||||
@@ -4848,12 +4987,6 @@
|
|||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-eas-client": {
|
|
||||||
"version": "55.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-55.0.2.tgz",
|
|
||||||
"integrity": "sha512-fjOgSXaZFBK2Xmzn/uw0DTF3BsYv97JEa4PYXXqVCEvNJPwJB1cV1eX6Xyq6iKGIhMPH9k62sOc+oUdt094WCw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/expo-file-system": {
|
"node_modules/expo-file-system": {
|
||||||
"version": "55.0.10",
|
"version": "55.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.10.tgz",
|
||||||
@@ -5184,12 +5317,6 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-structured-headers": {
|
|
||||||
"version": "55.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/expo-structured-headers/-/expo-structured-headers-55.0.0.tgz",
|
|
||||||
"integrity": "sha512-udaNvuWb45/Sryq9FLC/blwgOChhznuqlTrUzVjC0T83pMdcmscKJX23lnNDW6hCec8p81Y3z1DIFwIyk0g/PQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/expo-symbols": {
|
"node_modules/expo-symbols": {
|
||||||
"version": "55.0.5",
|
"version": "55.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-55.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/expo-symbols/-/expo-symbols-55.0.5.tgz",
|
||||||
@@ -5226,36 +5353,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-updates": {
|
|
||||||
"version": "55.0.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/expo-updates/-/expo-updates-55.0.15.tgz",
|
|
||||||
"integrity": "sha512-UE9Ik56trq//kNeJ/BlC5vOTYdNTvsHwhfWFYMazP1UOQK4lnX59/t0qz8Ut+3aPXZZT7+B6mnbWtic0QqN1wA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@expo/code-signing-certificates": "^0.0.6",
|
|
||||||
"@expo/plist": "^0.5.2",
|
|
||||||
"@expo/spawn-async": "^1.7.2",
|
|
||||||
"arg": "^4.1.0",
|
|
||||||
"chalk": "^4.1.2",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"expo-eas-client": "~55.0.2",
|
|
||||||
"expo-manifests": "~55.0.11",
|
|
||||||
"expo-structured-headers": "~55.0.0",
|
|
||||||
"expo-updates-interface": "~55.1.3",
|
|
||||||
"getenv": "^2.0.0",
|
|
||||||
"glob": "^13.0.0",
|
|
||||||
"ignore": "^5.3.1",
|
|
||||||
"resolve-from": "^5.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"expo-updates": "bin/cli.js"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"expo": "*",
|
|
||||||
"react": "*",
|
|
||||||
"react-native": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/expo-updates-interface": {
|
"node_modules/expo-updates-interface": {
|
||||||
"version": "55.1.3",
|
"version": "55.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-55.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-55.1.3.tgz",
|
||||||
@@ -5265,12 +5362,6 @@
|
|||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-updates/node_modules/arg": {
|
|
||||||
"version": "4.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
|
||||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/expo/node_modules/@expo/cli": {
|
"node_modules/expo/node_modules/@expo/cli": {
|
||||||
"version": "55.0.15",
|
"version": "55.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-55.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-55.0.15.tgz",
|
||||||
@@ -7106,6 +7197,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mdn-data": {
|
||||||
|
"version": "2.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||||
|
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
||||||
|
"license": "CC0-1.0"
|
||||||
|
},
|
||||||
"node_modules/media-typer": {
|
"node_modules/media-typer": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
@@ -7624,6 +7721,18 @@
|
|||||||
"node": "^16.14.0 || >=18.0.0"
|
"node": "^16.14.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nth-check": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nullthrows": {
|
"node_modules/nullthrows": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
||||||
@@ -8109,6 +8218,23 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prop-types": {
|
||||||
|
"version": "15.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prop-types/node_modules/react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@@ -8129,6 +8255,107 @@
|
|||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
|
"pngjs": "^5.0.0",
|
||||||
|
"yargs": "^15.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"qrcode": "bin/qrcode"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/camelcase": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/cliui": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wrap-ansi": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/pngjs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/y18n": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/yargs": {
|
||||||
|
"version": "15.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^6.0.0",
|
||||||
|
"decamelize": "^1.2.0",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-caller-file": "^2.0.1",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"require-main-filename": "^2.0.0",
|
||||||
|
"set-blocking": "^2.0.0",
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"which-module": "^2.0.0",
|
||||||
|
"y18n": "^4.0.0",
|
||||||
|
"yargs-parser": "^18.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/yargs-parser": {
|
||||||
|
"version": "18.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||||
|
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^5.0.0",
|
||||||
|
"decamelize": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.14.2",
|
"version": "6.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||||
@@ -8330,6 +8557,22 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-qrcode-svg": {
|
||||||
|
"version": "6.3.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-qrcode-svg/-/react-native-qrcode-svg-6.3.21.tgz",
|
||||||
|
"integrity": "sha512-6vcj4rcdpWedvphDR+NSJcudJykNuLgNGFwm2p4xYjR8RdyTzlrELKI5LkO4ANS9cQUbqsfkpippPv64Q2tUtA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.8.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
|
"text-encoding": "^0.7.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": ">=0.63.4",
|
||||||
|
"react-native-svg": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-safe-area-context": {
|
"node_modules/react-native-safe-area-context": {
|
||||||
"version": "5.6.2",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
||||||
@@ -8354,6 +8597,21 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-svg": {
|
||||||
|
"version": "15.15.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.3.tgz",
|
||||||
|
"integrity": "sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-select": "^5.1.0",
|
||||||
|
"css-tree": "^1.1.3",
|
||||||
|
"warn-once": "0.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-video": {
|
"node_modules/react-native-video": {
|
||||||
"version": "6.19.0",
|
"version": "6.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-6.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-6.19.0.tgz",
|
||||||
@@ -8637,6 +8895,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-main-filename": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
@@ -8912,6 +9176,12 @@
|
|||||||
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/setimmediate": {
|
"node_modules/setimmediate": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
@@ -9394,6 +9664,13 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/text-encoding": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==",
|
||||||
|
"deprecated": "no longer maintained",
|
||||||
|
"license": "(Unlicense OR Apache-2.0)"
|
||||||
|
},
|
||||||
"node_modules/throat": {
|
"node_modules/throat": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz",
|
||||||
@@ -9785,6 +10062,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which-module": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
|||||||
@@ -34,8 +34,10 @@
|
|||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-native": "0.83.2",
|
"react-native": "0.83.2",
|
||||||
"react-native-pager-view": "8.0.0",
|
"react-native-pager-view": "8.0.0",
|
||||||
|
"react-native-qrcode-svg": "^6.3.21",
|
||||||
"react-native-safe-area-context": "~5.6.2",
|
"react-native-safe-area-context": "~5.6.2",
|
||||||
"react-native-screens": "~4.23.0",
|
"react-native-screens": "~4.23.0",
|
||||||
|
"react-native-svg": "15.15.3",
|
||||||
"react-native-video": "^6.19.0",
|
"react-native-video": "^6.19.0",
|
||||||
"react-native-web": "^0.21.0",
|
"react-native-web": "^0.21.0",
|
||||||
"react-native-webview": "13.16.0",
|
"react-native-webview": "13.16.0",
|
||||||
|
|||||||
@@ -2,21 +2,35 @@ import { create } from 'zustand';
|
|||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
coverQuality: 'hd' | 'normal';
|
darkMode: boolean;
|
||||||
setCoverQuality: (q: 'hd' | 'normal') => Promise<void>;
|
trafficSaving: boolean;
|
||||||
|
setDarkMode: (v: boolean) => Promise<void>;
|
||||||
|
setTrafficSaving: (v: boolean) => Promise<void>;
|
||||||
restore: () => Promise<void>;
|
restore: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = create<SettingsState>((set) => ({
|
export const useSettingsStore = create<SettingsState>((set) => ({
|
||||||
coverQuality: 'hd',
|
darkMode: false,
|
||||||
|
trafficSaving: false,
|
||||||
|
|
||||||
setCoverQuality: async (q) => {
|
setDarkMode: async (v) => {
|
||||||
await AsyncStorage.setItem('COVER_QUALITY', q);
|
await AsyncStorage.setItem('DARK_MODE', v ? '1' : '0');
|
||||||
set({ coverQuality: q });
|
set({ darkMode: v });
|
||||||
|
},
|
||||||
|
|
||||||
|
setTrafficSaving: async (v) => {
|
||||||
|
await AsyncStorage.setItem('TRAFFIC_SAVING', v ? '1' : '0');
|
||||||
|
set({ trafficSaving: v });
|
||||||
},
|
},
|
||||||
|
|
||||||
restore: async () => {
|
restore: async () => {
|
||||||
const q = await AsyncStorage.getItem('COVER_QUALITY');
|
const [dm, ts] = await Promise.all([
|
||||||
if (q === 'hd' || q === 'normal') set({ coverQuality: q });
|
AsyncStorage.getItem('DARK_MODE'),
|
||||||
|
AsyncStorage.getItem('TRAFFIC_SAVING'),
|
||||||
|
]);
|
||||||
|
set({
|
||||||
|
darkMode: dm === '1',
|
||||||
|
trafficSaving: ts === '1',
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
33
utils/theme.ts
Normal file
33
utils/theme.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { useSettingsStore } from '../store/settingsStore';
|
||||||
|
|
||||||
|
export type ThemeColors = {
|
||||||
|
bg: string; // 页面/列表背景
|
||||||
|
card: string; // 卡片/section 背景
|
||||||
|
text: string; // 主文字
|
||||||
|
textSub: string; // 次要文字
|
||||||
|
border: string; // 分割线
|
||||||
|
inputBg: string; // 输入框背景
|
||||||
|
};
|
||||||
|
|
||||||
|
const light: ThemeColors = {
|
||||||
|
bg: '#f4f4f4',
|
||||||
|
card: '#fff',
|
||||||
|
text: '#212121',
|
||||||
|
textSub: '#999',
|
||||||
|
border: '#eee',
|
||||||
|
inputBg: '#f0f0f0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const dark: ThemeColors = {
|
||||||
|
bg: '#0f0f0f',
|
||||||
|
card: '#1a1a1a',
|
||||||
|
text: '#e0e0e0',
|
||||||
|
textSub: '#666',
|
||||||
|
border: '#2a2a2a',
|
||||||
|
inputBg: '#2a2a2a',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useTheme(): ThemeColors {
|
||||||
|
const darkMode = useSettingsStore(s => s.darkMode);
|
||||||
|
return darkMode ? dark : light;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user