From f2fc4693a66ff3d82b4a24947ec265564ac249fd Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 24 Mar 2026 21:04:10 +0800 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E6=9A=97=E9=BB=91=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=20+=20=E8=8A=82=E6=B5=81=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - settingsStore: 移除 coverQuality,新增 darkMode / trafficSaving 布尔字段(AsyncStorage 持久化) - utils/theme.ts: 新建 useTheme() hook,返回浅色/深色颜色表 - settings.tsx: 新增外观(浅色/深色)和流量(标准/节流)选项,移除封面图清晰度 - BigVideoCard: 节流模式跳过预加载、禁止自动播放;封面低画质;支持暗黑配色 - useVideoDetail: 节流模式下默认 qn=16(360p) - VideoCard / LiveCard: 移除 coverQuality 依赖,改用 trafficSaving;支持暗黑配色 - index.tsx: 导航栏/搜索栏/列表背景跟随主题 - search.tsx: 页面背景/输入框/文字跟随主题 - downloads.tsx: 顶栏/背景/文字跟随主题 - _layout.tsx: StatusBar 随暗黑模式切换 light/dark --- .gitignore | 1 + app/_layout.tsx | 4 +- app/downloads.tsx | 10 +++-- app/index.tsx | 15 ++++--- app/search.tsx | 14 +++--- app/settings.tsx | 87 ++++++++++++++++++++++--------------- components/BigVideoCard.tsx | 22 +++++----- components/LiveCard.tsx | 8 ++-- components/VideoCard.tsx | 12 ++--- hooks/useVideoDetail.ts | 7 ++- store/settingsStore.ts | 30 +++++++++---- utils/theme.ts | 33 ++++++++++++++ 12 files changed, 164 insertions(+), 79 deletions(-) create mode 100644 utils/theme.ts diff --git a/.gitignore b/.gitignore index 1e88ecf..dc9b4d1 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,5 @@ docs/ feature.md livePlan.md Promotion.md +wordsFilter.md .env.local diff --git a/app/_layout.tsx b/app/_layout.tsx index b89e078..3058fb9 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -6,6 +6,7 @@ import { useEffect } from 'react'; import { useAuthStore } from '../store/authStore'; import { useDownloadStore } from '../store/downloadStore'; import { useSettingsStore } from '../store/settingsStore'; +import { useTheme } from '../utils/theme'; import { MiniPlayer } from '../components/MiniPlayer'; import * as Sentry from '@sentry/react-native'; import { ErrorBoundary } from '@sentry/react-native'; @@ -21,6 +22,7 @@ function RootLayout() { const restore = useAuthStore(s => s.restore); const loadDownloads = useDownloadStore(s => s.loadFromStorage); const restoreSettings = useSettingsStore(s => s.restore); + const darkMode = useSettingsStore(s => s.darkMode); useEffect(() => { restore(); @@ -31,7 +33,7 @@ function RootLayout() { return ( - + 发生错误,请重启 App}> diff --git a/app/downloads.tsx b/app/downloads.tsx index 72c5e4d..0f19eaf 100644 --- a/app/downloads.tsx +++ b/app/downloads.tsx @@ -27,9 +27,11 @@ function formatFileSize(bytes?: number): string { return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; } import { proxyImageUrl } from '../utils/imageUrl'; +import { useTheme } from '../utils/theme'; export default function DownloadsScreen() { const router = useRouter(); + const theme = useTheme(); const { tasks, loadFromStorage, removeTask } = useDownloadStore(); const [playingUri, setPlayingUri] = useState(null); const [playingTitle, setPlayingTitle] = useState(''); @@ -61,12 +63,12 @@ export default function DownloadsScreen() { if (done.length > 0) sections.push({ title: '已下载', data: done }); return ( - - + + router.back()} style={styles.backBtn}> - + - 我的下载 + 我的下载 diff --git a/app/index.tsx b/app/index.tsx index 2521408..0350335 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -40,6 +40,7 @@ import { } from "../utils/videoRows"; import { BigVideoCard } from "../components/BigVideoCard"; import { FollowedLiveStrip } from "../components/FollowedLiveStrip"; +import { useTheme } from "../utils/theme"; import type { LiveRoom } from "../services/types"; const HEADER_H = 44; @@ -83,6 +84,7 @@ export default function HomeScreen() { const [activeTab, setActiveTab] = useState("hot"); const [liveAreaId, setLiveAreaId] = useState(0); + const theme = useTheme(); const [visibleBigKey, setVisibleBigKey] = useState(null); const rows = useMemo(() => toListRows(pages, liveRooms), [pages, liveRooms]); const pagerRef = useRef(null); @@ -286,7 +288,7 @@ export default function HomeScreen() { activeTab === "hot" ? headerOpacity : liveHeaderOpacity; return ( - + {/* 滑动切换容器 */} @@ -439,19 +443,19 @@ export default function HomeScreen() { router.push("/search" as any)} activeOpacity={0.7} > - - 搜索视频、UP主... + + 搜索视频、UP主... router.push("/downloads" as any)} /> - + {TABS.map((tab) => ( diff --git a/app/search.tsx b/app/search.tsx index 12324ee..ff20aa2 100644 --- a/app/search.tsx +++ b/app/search.tsx @@ -13,11 +13,13 @@ import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { VideoCard } from '../components/VideoCard'; import { useSearch } from '../hooks/useSearch'; +import { useTheme } from '../utils/theme'; import type { VideoItem } from '../services/types'; export default function SearchScreen() { const router = useRouter(); const { keyword, setKeyword, results, loading, hasMore, search, loadMore } = useSearch(); + const theme = useTheme(); const inputRef = useRef(null); const handleSearch = useCallback(() => { @@ -64,7 +66,7 @@ export default function SearchScreen() { return ( - + {results.length === 0 && keyword.trim() ? '没有找到相关视频' : '输入关键词搜索'} @@ -74,16 +76,16 @@ export default function SearchScreen() { }; return ( - + {/* Search header */} - + router.back()} style={styles.backBtn}> - + - + { @@ -19,25 +21,25 @@ export default function SettingsScreen() { }; return ( - - + + router.back()} style={styles.backBtn}> - + - 设置 + 设置 - - 版本信息 + + 版本信息 - 当前版本 - v{currentVersion} + 当前版本 + v{currentVersion} - - 更新 + + 更新 - - 封面图清晰度 + + 外观 setCoverQuality('hd')} + style={[styles.option, !darkMode && styles.optionActive]} + onPress={() => setDarkMode(false)} activeOpacity={0.7} > - - 高清 - + 浅色 setCoverQuality('normal')} + style={[styles.option, darkMode && styles.optionActive]} + onPress={() => setDarkMode(true)} activeOpacity={0.7} > - - 普通 - + 深色 + + 流量 + + setTrafficSaving(false)} + activeOpacity={0.7} + > + 标准 + + setTrafficSaving(true)} + activeOpacity={0.7} + > + 节流 + + + {trafficSaving && ( + + 封面低画质 · 首页视频不自动播放 · 视频默认 360p + + )} + + {isLoggedIn && ( 退出登录 @@ -91,15 +114,13 @@ export default function SettingsScreen() { } const styles = StyleSheet.create({ - safe: { flex: 1, backgroundColor: '#f4f4f4' }, + safe: { flex: 1 }, topBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, paddingVertical: 8, - backgroundColor: '#fff', borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '#eee', }, backBtn: { padding: 4, width: 32 }, spacer: { width: 32 }, @@ -107,20 +128,16 @@ const styles = StyleSheet.create({ flex: 1, fontSize: 16, fontWeight: '600', - color: '#212121', textAlign: 'center', }, section: { - backgroundColor: '#fff', marginTop: 16, paddingHorizontal: 16, paddingVertical: 14, borderTopWidth: StyleSheet.hairlineWidth, - borderTopColor: '#eee', borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: '#eee', }, - sectionLabel: { fontSize: 13, color: '#999', marginBottom: 10 }, + sectionLabel: { fontSize: 13, marginBottom: 10 }, optionRow: { flexDirection: 'row', gap: 10 }, option: { paddingHorizontal: 20, @@ -128,18 +145,19 @@ const styles = StyleSheet.create({ borderRadius: 20, borderWidth: 1, borderColor: '#e0e0e0', - backgroundColor: '#fff', + backgroundColor: 'transparent', }, optionActive: { borderColor: '#00AEEC', backgroundColor: '#e8f7fd' }, optionText: { fontSize: 14, color: '#666' }, optionTextActive: { color: '#00AEEC', fontWeight: '600' }, + hint: { fontSize: 12, marginTop: 8 }, versionRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, - versionLabel: { fontSize: 14, color: '#212121' }, - versionValue: { fontSize: 14, color: '#999' }, + versionLabel: { fontSize: 14 }, + versionValue: { fontSize: 14 }, updateBtn: { flexDirection: 'row', alignItems: 'center', @@ -150,7 +168,6 @@ const styles = StyleSheet.create({ margin: 24, paddingVertical: 12, borderRadius: 8, - backgroundColor: '#fff', borderWidth: 1, borderColor: '#ff4757', alignItems: 'center', diff --git a/components/BigVideoCard.tsx b/components/BigVideoCard.tsx index d6956ff..4199f06 100644 --- a/components/BigVideoCard.tsx +++ b/components/BigVideoCard.tsx @@ -22,6 +22,7 @@ import { buildDashMpdUri } from "../utils/dash"; import { getPlayUrl, getVideoDetail } from "../services/bilibili"; import { coverImageUrl } from "../utils/imageUrl"; import { useSettingsStore } from "../store/settingsStore"; +import { useTheme } from "../utils/theme"; import { formatCount, formatDuration } from "../utils/format"; import type { VideoItem } from "../services/types"; @@ -55,7 +56,8 @@ export const BigVideoCard = React.memo(function BigVideoCard({ onPress, }: Props) { 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 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) useEffect(() => { - if (videoUrl) return; + if (videoUrl || trafficSaving) return; let cancelled = false; (async () => { try { @@ -125,8 +127,8 @@ export const BigVideoCard = React.memo(function BigVideoCard({ // Pause/resume based on visibility and scroll state useEffect(() => { if (!videoUrl) return; - if (!isVisible) { - // Off-screen: pause, mute, show thumbnail + if (!isVisible || trafficSaving) { + // Off-screen or traffic saving: pause, mute, show thumbnail setPaused(true); setMuted(true); Animated.timing(thumbOpacity, { @@ -146,10 +148,10 @@ export const BigVideoCard = React.memo(function BigVideoCard({ useNativeDriver: true, }).start(); } - }, [isVisible, isScrolling, videoUrl]); + }, [isVisible, isScrolling, videoUrl, trafficSaving]); const handleVideoReady = () => { - if (!isVisible || isScrolling) return; + if (!isVisible || isScrolling || trafficSaving) return; setPaused(false); Animated.timing(thumbOpacity, { toValue: 0, @@ -223,7 +225,7 @@ export const BigVideoCard = React.memo(function BigVideoCard({ const bufferedRatio = duration > 0 ? clamp(buffered / duration, 0, 1) : 0; return ( - + {/* Media area */} {/* Video player — rendered first so it sits behind the thumbnail */} @@ -260,7 +262,7 @@ export const BigVideoCard = React.memo(function BigVideoCard({ pointerEvents="none" > @@ -338,11 +340,11 @@ export const BigVideoCard = React.memo(function BigVideoCard({ {/* Info */} - + {item.title} - + {item.owner?.name ?? ""} diff --git a/components/LiveCard.tsx b/components/LiveCard.tsx index ba335d7..6e24058 100644 --- a/components/LiveCard.tsx +++ b/components/LiveCard.tsx @@ -12,6 +12,7 @@ import { LivePulse } from "./LivePulse"; import type { LiveRoom } from "../services/types"; import { formatCount } from "../utils/format"; import { proxyImageUrl } from "../utils/imageUrl"; +import { useTheme } from "../utils/theme"; const { width } = Dimensions.get("window"); const CARD_WIDTH = (width - 14) / 2; @@ -30,9 +31,10 @@ export const LiveCard = React.memo(function LiveCard({ isLivePulse = false, }: Props) { const cardWidth = fullWidth ? width - 8 : CARD_WIDTH; + const theme = useTheme(); return ( @@ -58,7 +60,7 @@ export const LiveCard = React.memo(function LiveCard({ - + {item.title} @@ -66,7 +68,7 @@ export const LiveCard = React.memo(function LiveCard({ source={{ uri: proxyImageUrl(item.face) }} style={styles.avatar} /> - + {item.uname} diff --git a/components/VideoCard.tsx b/components/VideoCard.tsx index 7e7b999..153fd7d 100644 --- a/components/VideoCard.tsx +++ b/components/VideoCard.tsx @@ -12,6 +12,7 @@ import type { VideoItem } from "../services/types"; import { formatCount, formatDuration } from "../utils/format"; import { coverImageUrl } from "../utils/imageUrl"; import { useSettingsStore } from "../store/settingsStore"; +import { useTheme } from "../utils/theme"; const { width } = Dimensions.get("window"); const CARD_WIDTH = (width - 14) / 2; @@ -22,16 +23,17 @@ interface 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 ( @@ -48,10 +50,10 @@ export const VideoCard = React.memo(function VideoCard({ item, onPress }: Props) - + {item.title} - + {item.owner?.name ?? ""} diff --git a/hooks/useVideoDetail.ts b/hooks/useVideoDetail.ts index 7bf0386..e0075b9 100644 --- a/hooks/useVideoDetail.ts +++ b/hooks/useVideoDetail.ts @@ -1,6 +1,7 @@ import { useState, useEffect, useRef } from 'react'; import { getVideoDetail, getPlayUrl } from '../services/bilibili'; import { useAuthStore } from '../store/authStore'; +import { useSettingsStore } from '../store/settingsStore'; import type { VideoItem, PlayUrlResponse } from '../services/types'; export function useVideoDetail(bvid: string) { @@ -12,6 +13,8 @@ export function useVideoDetail(bvid: string) { const [error, setError] = useState(null); const cidRef = useRef(0); 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) { const data = await getPlayUrl(bvid, cid, qn); @@ -39,7 +42,7 @@ export function useVideoDetail(bvid: string) { setVideo(detail); const cid = detail.pages?.[0]?.cid ?? detail.cid as number; cidRef.current = cid; - await fetchPlayData(cid, 126, true); + await fetchPlayData(cid, defaultQn, true); } catch (e: any) { setError(e.message ?? 'Load failed'); } finally { @@ -52,7 +55,7 @@ export function useVideoDetail(bvid: string) { // 登录状态变化时重新拉取清晰度列表(登录后可能获得更高画质) useEffect(() => { 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); }); } diff --git a/store/settingsStore.ts b/store/settingsStore.ts index b8c4f64..39ab39a 100644 --- a/store/settingsStore.ts +++ b/store/settingsStore.ts @@ -2,21 +2,35 @@ import { create } from 'zustand'; import AsyncStorage from '@react-native-async-storage/async-storage'; interface SettingsState { - coverQuality: 'hd' | 'normal'; - setCoverQuality: (q: 'hd' | 'normal') => Promise; + darkMode: boolean; + trafficSaving: boolean; + setDarkMode: (v: boolean) => Promise; + setTrafficSaving: (v: boolean) => Promise; restore: () => Promise; } export const useSettingsStore = create((set) => ({ - coverQuality: 'hd', + darkMode: false, + trafficSaving: false, - setCoverQuality: async (q) => { - await AsyncStorage.setItem('COVER_QUALITY', q); - set({ coverQuality: q }); + setDarkMode: async (v) => { + await AsyncStorage.setItem('DARK_MODE', v ? '1' : '0'); + set({ darkMode: v }); + }, + + setTrafficSaving: async (v) => { + await AsyncStorage.setItem('TRAFFIC_SAVING', v ? '1' : '0'); + set({ trafficSaving: v }); }, restore: async () => { - const q = await AsyncStorage.getItem('COVER_QUALITY'); - if (q === 'hd' || q === 'normal') set({ coverQuality: q }); + const [dm, ts] = await Promise.all([ + AsyncStorage.getItem('DARK_MODE'), + AsyncStorage.getItem('TRAFFIC_SAVING'), + ]); + set({ + darkMode: dm === '1', + trafficSaving: ts === '1', + }); }, })); diff --git a/utils/theme.ts b/utils/theme.ts new file mode 100644 index 0000000..7265ef5 --- /dev/null +++ b/utils/theme.ts @@ -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; +} From a61aa5caacd16f396db6c1aed4f9926793b519a7 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 24 Mar 2026 21:23:22 +0800 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=E4=BD=BF=E7=94=A8=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E5=BA=93=20react-native-qrcode-svg=20=E7=94=9F?= =?UTF-8?q?=E6=88=90=E4=BA=8C=E7=BB=B4=E7=A0=81=EF=BC=8C=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E7=AC=AC=E4=B8=89=E6=96=B9=E8=BF=9C=E7=A8=8B=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/LoginModal.tsx | 92 +++++---- package-lock.json | 385 +++++++++++++++++++++++++++++++++----- package.json | 4 +- 3 files changed, 379 insertions(+), 102 deletions(-) diff --git a/components/LoginModal.tsx b/components/LoginModal.tsx index e6b2b48..8f5f418 100644 --- a/components/LoginModal.tsx +++ b/components/LoginModal.tsx @@ -5,7 +5,6 @@ import { Text, StyleSheet, TouchableOpacity, - Image, ActivityIndicator, Animated, Alert, @@ -13,6 +12,7 @@ import { } from "react-native"; import * as FileSystem from "expo-file-system/legacy"; import * as MediaLibrary from "expo-media-library"; +import QRCode from "react-native-qrcode-svg"; import { Ionicons } from "@expo/vector-icons"; import { generateQRCode, pollQRCode, getUserInfo } from "../services/bilibili"; import { useAuthStore } from "../store/authStore"; @@ -23,10 +23,10 @@ interface Props { } export function LoginModal({ visible, onClose }: Props) { - const [qrUrl, setQrUrl] = useState(null); + const [qrData, setQrData] = useState(null); const [qrKey, setQrKey] = useState(null); - const [qrImageLoaded, setQrImageLoaded] = useState(false); const [saving, setSaving] = useState(false); + const qrRef = useRef(null); const [status, setStatus] = useState< "loading" | "waiting" | "scanned" | "done" | "error" >("loading"); @@ -52,14 +52,11 @@ export function LoginModal({ visible, onClose }: Props) { useEffect(() => { if (!visible) return; setStatus("loading"); - setQrUrl(null); + setQrData(null); setQrKey(null); - setQrImageLoaded(false); generateQRCode() .then((data) => { - setQrUrl( - `https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(data.url)}&size=400x400`, - ); + setQrData(data.url); setQrKey(data.qrcode_key); setStatus("waiting"); }) @@ -98,7 +95,7 @@ export function LoginModal({ visible, onClose }: Props) { }, [qrKey, status]); async function handleSaveQR() { - if (!qrUrl) return; + if (!qrRef.current) return; setSaving(true); try { const { status: perm } = await MediaLibrary.requestPermissionsAsync(); @@ -106,19 +103,28 @@ export function LoginModal({ visible, onClose }: Props) { Alert.alert("提示", "需要相册权限才能保存图片"); return; } - const dest = `${FileSystem.cacheDirectory}bilibili_qr.png`; - const { uri } = await FileSystem.downloadAsync(qrUrl, dest); - await MediaLibrary.saveToLibraryAsync(uri); - Alert.alert("已保存", "二维码已存入相册,请用哔哩哔哩扫码登录", [ - { text: "关闭", style: "cancel" }, - { - text: "打开哔哩哔哩", - onPress: () => Linking.openURL("bilibili://"), - }, - ]); + qrRef.current.toDataURL(async (base64: string) => { + try { + const dest = `${FileSystem.cacheDirectory}bilibili_qr.png`; + await FileSystem.writeAsStringAsync(dest, base64, { + encoding: FileSystem.EncodingType.Base64, + }); + await MediaLibrary.saveToLibraryAsync(dest); + Alert.alert("已保存", "二维码已存入相册,请用哔哩哔哩扫码登录", [ + { text: "关闭", style: "cancel" }, + { + text: "打开哔哩哔哩", + onPress: () => Linking.openURL("bilibili://"), + }, + ]); + } catch { + Alert.alert("失败", "保存失败,请重试"); + } finally { + setSaving(false); + } + }); } catch { Alert.alert("失败", "保存失败,请重试"); - } finally { setSaving(false); } } @@ -146,32 +152,25 @@ export function LoginModal({ visible, onClose }: Props) { style={styles.loader} /> )} - {(status === "waiting" || status === "scanned") && qrUrl && ( + {(status === "waiting" || status === "scanned") && qrData && ( <> - setQrImageLoaded(true)} + { qrRef.current = ref; }} /> - {!qrImageLoaded && ( - - - - )} - {qrImageLoaded && ( - - {saving ? ( - - ) : ( - - )} - - )} + + {saving ? ( + + ) : ( + + )} + {status === "scanned" @@ -213,13 +212,6 @@ const styles = StyleSheet.create({ title: { fontSize: 18, fontWeight: "600", marginBottom: 20 }, loader: { marginVertical: 40 }, qrWrapper: { width: 200, height: 200, marginBottom: 12 }, - qr: { width: 200, height: 200 }, - qrLoader: { - ...StyleSheet.absoluteFillObject, - alignItems: "center", - justifyContent: "center", - backgroundColor: "#f4f4f4", - }, saveBtn: { position: "absolute", bottom: 6, diff --git a/package-lock.json b/package-lock.json index dccf46f..b3afaee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jkvideo", - "version": "1.0.5", + "version": "1.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jkvideo", - "version": "1.0.5", + "version": "1.0.6", "dependencies": { "@dr.pogodin/react-native-static-server": "^0.26.0", "@expo/vector-icons": "^15.0.2", @@ -27,14 +27,15 @@ "expo-screen-orientation": "~55.0.8", "expo-status-bar": "~55.0.4", "expo-system-ui": "~55.0.9", - "expo-updates": "~55.0.15", "pako": "^2.1.0", "react": "19.2.0", "react-dom": "19.2.0", "react-native": "0.83.2", "react-native-pager-view": "8.0.0", + "react-native-qrcode-svg": "^6.3.21", "react-native-safe-area-context": "~5.6.2", "react-native-screens": "~4.23.0", + "react-native-svg": "15.15.3", "react-native-video": "^6.19.0", "react-native-web": "^0.21.0", "react-native-webview": "13.16.0", @@ -3897,6 +3898,12 @@ "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": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", @@ -4436,6 +4443,56 @@ "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": { "version": "3.2.3", "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": { "version": "0.2.2", "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==", "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": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dnssd-advertise/-/dnssd-advertise-1.1.3.tgz", "integrity": "sha512-XENsHi3MBzWOCAXif3yZvU1Ah0l+nhJj1sjWL6TnOAYKvGiFhbTx32xHN7+wLMLUOCj7Nr0evADWG4R8JtqCDA==", "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4589,6 +4716,18 @@ "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": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", @@ -4848,12 +4987,6 @@ "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": { "version": "55.0.10", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.10.tgz", @@ -5184,12 +5317,6 @@ "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": { "version": "55.0.5", "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": { "version": "55.1.3", "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-55.1.3.tgz", @@ -5265,12 +5362,6 @@ "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": { "version": "55.0.15", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-55.0.15.tgz", @@ -7106,6 +7197,12 @@ "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": { "version": "0.3.0", "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_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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -8109,6 +8218,23 @@ "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": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -8129,6 +8255,107 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "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": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", @@ -8330,6 +8557,22 @@ "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": { "version": "5.6.2", "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": "*" } }, + "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": { "version": "6.19.0", "resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-6.19.0.tgz", @@ -8637,6 +8895,12 @@ "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": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -8912,6 +9176,12 @@ "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", "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": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -9394,6 +9664,13 @@ "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": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", @@ -9785,6 +10062,12 @@ "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": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 6edb1b1..2f06ccf 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,15 @@ "expo-screen-orientation": "~55.0.8", "expo-status-bar": "~55.0.4", "expo-system-ui": "~55.0.9", -"pako": "^2.1.0", + "pako": "^2.1.0", "react": "19.2.0", "react-dom": "19.2.0", "react-native": "0.83.2", "react-native-pager-view": "8.0.0", + "react-native-qrcode-svg": "^6.3.21", "react-native-safe-area-context": "~5.6.2", "react-native-screens": "~4.23.0", + "react-native-svg": "15.15.3", "react-native-video": "^6.19.0", "react-native-web": "^0.21.0", "react-native-webview": "13.16.0", From 5a368dc794d6e045411efef988ff25669450a715 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 24 Mar 2026 21:43:52 +0800 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20=E6=9A=97=E9=BB=91=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=B8=8B=E5=B0=81=E9=9D=A2=E5=9B=BE=E5=8D=A0=E4=BD=8D=E8=83=8C?= =?UTF-8?q?=E6=99=AF=E8=89=B2=E8=B7=9F=E9=9A=8F=E5=8D=A1=E7=89=87=E9=A2=9C?= =?UTF-8?q?=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/LiveCard.tsx | 2 +- components/VideoCard.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/LiveCard.tsx b/components/LiveCard.tsx index 6e24058..0fec613 100644 --- a/components/LiveCard.tsx +++ b/components/LiveCard.tsx @@ -43,7 +43,7 @@ export const LiveCard = React.memo(function LiveCard({ source={{ uri: proxyImageUrl(item.cover) }} style={[ styles.thumb, - { width: cardWidth, height: cardWidth * 0.5625 }, + { width: cardWidth, height: cardWidth * 0.5625, backgroundColor: theme.card }, ]} resizeMode="cover" /> diff --git a/components/VideoCard.tsx b/components/VideoCard.tsx index 153fd7d..ac9c480 100644 --- a/components/VideoCard.tsx +++ b/components/VideoCard.tsx @@ -34,7 +34,7 @@ export const VideoCard = React.memo(function VideoCard({ item, onPress }: Props) From 03aee783da0b18587afb7a53bda57da6f655c190 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 24 Mar 2026 21:49:51 +0800 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20FollowedLiveStrip=20=E8=83=8C?= =?UTF-8?q?=E6=99=AF=E8=89=B2/=E5=A4=B4=E5=83=8F=E5=8D=A0=E4=BD=8D/?= =?UTF-8?q?=E4=B8=BB=E6=92=AD=E5=90=8D=E8=B7=9F=E9=9A=8F=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/FollowedLiveStrip.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/components/FollowedLiveStrip.tsx b/components/FollowedLiveStrip.tsx index a2a465d..150bcb1 100644 --- a/components/FollowedLiveStrip.tsx +++ b/components/FollowedLiveStrip.tsx @@ -12,12 +12,14 @@ import { useAuthStore } from "../store/authStore"; import { getFollowedLiveRooms } from "../services/bilibili"; import { LivePulse } from "./LivePulse"; import { proxyImageUrl } from "../utils/imageUrl"; +import { useTheme } from "../utils/theme"; import type { LiveRoom } from "../services/types"; export function FollowedLiveStrip() { const { sessdata } = useAuthStore(); const [rooms, setRooms] = useState([]); const router = useRouter(); + const theme = useTheme(); useEffect(() => { if (!sessdata) return; @@ -29,7 +31,7 @@ export function FollowedLiveStrip() { if (!sessdata || rooms.length === 0) return null; return ( - + - + {room.uname.length > 5 ? room.uname.slice(0, 5) : room.uname} From f6ac2d3cd47a71b16e4f710d5f72a54ca3a1272e Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 24 Mar 2026 21:58:00 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=E8=A7=86=E9=A2=91=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E5=92=8C=E7=9B=B4=E6=92=AD=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E9=A1=B5=E6=94=AF=E6=8C=81=E6=9A=97=E9=BB=91=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/live/[roomId].tsx | 42 ++++++++++----------- app/video/[bvid].tsx | 88 ++++++++++++++++++------------------------- 2 files changed, 56 insertions(+), 74 deletions(-) diff --git a/app/live/[roomId].tsx b/app/live/[roomId].tsx index 135d947..9881f34 100644 --- a/app/live/[roomId].tsx +++ b/app/live/[roomId].tsx @@ -17,12 +17,14 @@ import { LivePlayer } from "../../components/LivePlayer"; import DanmakuList from "../../components/DanmakuList"; import { formatCount } from "../../utils/format"; import { proxyImageUrl } from "../../utils/imageUrl"; +import { useTheme } from "../../utils/theme"; type Tab = "intro" | "danmaku"; export default function LiveDetailScreen() { const { roomId } = useLocalSearchParams<{ roomId: string }>(); const router = useRouter(); + const theme = useTheme(); const id = parseInt(roomId ?? "0", 10); const { room, anchor, stream, loading, error, changeQuality } = useLiveDetail(id); @@ -34,17 +36,17 @@ export default function LiveDetailScreen() { const qualities = stream?.qualities ?? []; 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 { danmakus, giftCounts } = useLiveDanmaku(isLive ? actualRoomId : 0); + return ( - + {/* TopBar */} - + router.back()} style={styles.backBtn}> - + - + {room?.title ?? "直播间"} @@ -60,12 +62,12 @@ export default function LiveDetailScreen() { /> {/* TabBar */} - + setTab("intro")} > - + 简介 {tab === "intro" && } @@ -75,7 +77,7 @@ export default function LiveDetailScreen() { onPress={() => setTab("danmaku")} > 弹幕{danmakus.length > 0 ? ` ${danmakus.length}` : ""} @@ -83,7 +85,7 @@ export default function LiveDetailScreen() { - {/* Content — 两个面板始终挂载,通过 display 切换,保留弹幕列表状态 */} + {/* Content */} {loading ? ( ) : error ? ( @@ -95,7 +97,7 @@ export default function LiveDetailScreen() { showsVerticalScrollIndicator={false} > - {room?.title} + {room?.title} {isLive ? ( @@ -126,7 +128,7 @@ export default function LiveDetailScreen() { - + {anchor && ( @@ -134,7 +136,7 @@ export default function LiveDetailScreen() { source={{ uri: proxyImageUrl(anchor.face) }} style={styles.avatar} /> - {anchor.uname} + {anchor.uname} + 关注 @@ -143,7 +145,7 @@ export default function LiveDetailScreen() { {!!room?.description && ( - {room.description} + {room.description} )} @@ -166,14 +168,13 @@ export default function LiveDetailScreen() { } const styles = StyleSheet.create({ - safe: { flex: 1, backgroundColor: "#fff" }, + safe: { flex: 1 }, topBar: { flexDirection: "row", alignItems: "center", paddingHorizontal: 8, paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: "#eee", }, backBtn: { padding: 4 }, topTitle: { @@ -181,15 +182,12 @@ const styles = StyleSheet.create({ fontSize: 15, fontWeight: "600", marginLeft: 4, - color: "#212121", }, loader: { marginVertical: 30 }, errorText: { textAlign: "center", color: "#f00", padding: 20 }, tabBar: { flexDirection: "row", borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: "#eee", - backgroundColor: "#fff", }, tabItem: { paddingHorizontal: 20, @@ -197,7 +195,7 @@ const styles = StyleSheet.create({ alignItems: "center", position: "relative", }, - tabLabel: { fontSize: 14, color: "#999", fontWeight: "500" }, + tabLabel: { fontSize: 14, fontWeight: "500" }, tabActive: { color: "#00AEEC", fontWeight: "700" }, tabUnderline: { position: "absolute", @@ -212,7 +210,6 @@ const styles = StyleSheet.create({ title: { fontSize: 16, fontWeight: "600", - color: "#212121", lineHeight: 22, marginBottom: 8, }, @@ -248,7 +245,6 @@ const styles = StyleSheet.create({ }, divider: { height: StyleSheet.hairlineWidth, - backgroundColor: "#f0f0f0", marginHorizontal: 14, }, anchorRow: { @@ -258,7 +254,7 @@ const styles = StyleSheet.create({ paddingVertical: 12, }, 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: { backgroundColor: "#00AEEC", paddingHorizontal: 14, @@ -267,7 +263,7 @@ const styles = StyleSheet.create({ }, followTxt: { color: "#fff", fontSize: 12, fontWeight: "600" }, descBox: { padding: 14, paddingTop: 4 }, - descText: { fontSize: 14, color: "#555", lineHeight: 22 }, + descText: { fontSize: 14, lineHeight: 22 }, danmakuFull: { flex: 1 }, hidden: { display: "none" }, }); diff --git a/app/video/[bvid].tsx b/app/video/[bvid].tsx index 574bf78..f1fef41 100644 --- a/app/video/[bvid].tsx +++ b/app/video/[bvid].tsx @@ -22,12 +22,14 @@ import { useRelatedVideos } from "../../hooks/useRelatedVideos"; import { formatCount, formatDuration } from "../../utils/format"; import { proxyImageUrl } from "../../utils/imageUrl"; import { DownloadSheet } from "../../components/DownloadSheet"; +import { useTheme } from "../../utils/theme"; type Tab = "intro" | "comments" | "danmaku"; export default function VideoDetailScreen() { const { bvid } = useLocalSearchParams<{ bvid: string }>(); const router = useRouter(); + const theme = useTheme(); const { video, playData, @@ -67,20 +69,20 @@ export default function VideoDetailScreen() { }, [video?.cid]); return ( - + {/* TopBar */} - + router.back()} style={styles.backBtn}> - + - + {video?.title ?? "视频详情"} setShowDownload(true)} > - + @@ -105,15 +107,15 @@ export default function VideoDetailScreen() { qualities={qualities} /> - {/* TabBar — sits directly below player, always visible once video loads */} + {/* TabBar */} {video && ( - + setTab("intro")} > 简介 @@ -124,7 +126,7 @@ export default function VideoDetailScreen() { onPress={() => setTab("comments")} > 评论 {video.stat?.reply > 0 ? ` ${formatCount(video.stat.reply)}` : ""} @@ -136,7 +138,7 @@ export default function VideoDetailScreen() { onPress={() => setTab("danmaku")} > 弹幕 {danmakus.length > 0 ? ` ${formatCount(danmakus.length)}` : ""} @@ -168,13 +170,13 @@ export default function VideoDetailScreen() { source={{ uri: proxyImageUrl(video.owner.face) }} style={styles.avatar} /> - {video.owner.name} + {video.owner.name} + 关注 - - {video.title} + + {video.title} @@ -192,22 +194,22 @@ export default function VideoDetailScreen() { /> )} - + {video.desc || "暂无简介"} - - 推荐视频 + + 推荐视频 } renderItem={({ item }) => ( router.push(`/video/${item.bvid}` as any)} activeOpacity={0.85} > - + - + {item.title} + 排序 void; }) { + const theme = useTheme(); const episodes = season.sections?.[0]?.episodes ?? []; const currentIndex = episodes.findIndex((ep) => ep.bvid === currentBvid); const listRef = useRef(null); useEffect(() => { if (currentIndex <= 0 || episodes.length === 0) return; - // 等布局完成再滚动 const t = setTimeout(() => { listRef.current?.scrollToIndex({ index: currentIndex, - viewPosition: 0.5, // 居中 + viewPosition: 0.5, animated: false, }); }, 200); @@ -370,9 +372,9 @@ function SeasonSection({ }, [currentIndex, episodes.length]); return ( - + - 合集 · {season.title} + 合集 · {season.title} {season.ep_count}个视频 @@ -383,7 +385,6 @@ function SeasonSection({ data={episodes} keyExtractor={(ep) => ep.bvid} contentContainerStyle={{ paddingHorizontal: 12, gap: 10 }} - // 每个卡片宽 120,gap 10,让 FlatList 直接算任意索引的偏移量 getItemLayout={(_data, index) => ({ length: 130, offset: 12 + index * 130, @@ -394,20 +395,20 @@ function SeasonSection({ const isCurrent = ep.bvid === currentBvid; return ( !isCurrent && onEpisodePress(ep.bvid)} activeOpacity={0.8} > {ep.arc?.pic && ( )} 第{index + 1}集 - + {ep.title} @@ -419,14 +420,13 @@ function SeasonSection({ } const styles = StyleSheet.create({ - safe: { flex: 1, backgroundColor: "#fff" }, + safe: { flex: 1 }, topBar: { flexDirection: "row", alignItems: "center", paddingHorizontal: 8, paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: "#eee", }, backBtn: { padding: 4 }, topTitle: { @@ -434,19 +434,16 @@ const styles = StyleSheet.create({ fontSize: 15, fontWeight: "600", marginLeft: 4, - color: "#212121", }, miniBtn: { padding: 4 }, loader: { marginVertical: 30 }, titleSection: { padding: 14, borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: "#f0f0f0", }, title: { fontSize: 13, fontWeight: "600", - color: "#212121", lineHeight: 22, marginBottom: 8, }, @@ -460,8 +457,8 @@ const styles = StyleSheet.create({ paddingBottom: 0, paddingTop: 12, }, - avatar: { width: 48, height: 48, borderRadius:30, marginRight: 10 }, - upName: { flex: 1, fontSize: 14, color: "#212121", fontWeight: "500" }, + avatar: { width: 48, height: 48, borderRadius: 30, marginRight: 10 }, + upName: { flex: 1, fontSize: 14, fontWeight: "500" }, followBtn: { backgroundColor: "#00AEEC", paddingHorizontal: 10, @@ -471,7 +468,6 @@ const styles = StyleSheet.create({ followTxt: { color: "#fff", fontSize: 12, fontWeight: "500" }, seasonBox: { borderTopWidth: StyleSheet.hairlineWidth, - borderTopColor: "#f0f0f0", paddingVertical: 10, }, seasonHeader: { @@ -481,23 +477,21 @@ const styles = StyleSheet.create({ paddingBottom: 8, gap: 4, }, - seasonTitle: { flex: 1, fontSize: 13, fontWeight: "600", color: "#212121" }, + seasonTitle: { flex: 1, fontSize: 13, fontWeight: "600" }, seasonCount: { fontSize: 12, color: "#999" }, epCard: { width: 120, borderRadius: 6, overflow: "hidden", - backgroundColor: "#f8f8f8", borderWidth: 1.5, borderColor: "transparent", }, epCardActive: { borderColor: "#00AEEC" }, - epThumb: { width: 120, height: 68, backgroundColor: "#eee" }, + epThumb: { width: 120, height: 68 }, epNum: { fontSize: 11, color: "#999", paddingHorizontal: 6, paddingTop: 4 }, epNumActive: { color: "#00AEEC", fontWeight: "600" }, epTitle: { fontSize: 12, - color: "#333", paddingHorizontal: 6, paddingBottom: 6, lineHeight: 16, @@ -505,7 +499,6 @@ const styles = StyleSheet.create({ tabBar: { flexDirection: "row", borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: "#eee", }, tabItem: { flex: 1, @@ -513,7 +506,7 @@ const styles = StyleSheet.create({ paddingVertical: 12, position: "relative", }, - tabLabel: { fontSize: 13, color: "#999" }, + tabLabel: { fontSize: 13 }, tabActive: { color: "#00AEEC" }, tabUnderline: { position: "absolute", @@ -525,28 +518,23 @@ const styles = StyleSheet.create({ }, tabScroll: { flex: 1 }, descBox: { padding: 16 }, - descText: { fontSize: 14, color: "#555", lineHeight: 22 }, + descText: { fontSize: 14, lineHeight: 22 }, danmakuTab: { flex: 1 }, emptyTxt: { textAlign: "center", color: "#bbb", padding: 30 }, relatedHeader: { paddingHorizontal: 14, paddingVertical: 10, borderTopWidth: StyleSheet.hairlineWidth, - borderTopColor: "#f0f0f0", - backgroundColor: "#f4f4f4", }, relatedHeaderText: { fontSize: 13, fontWeight: "600" as const, - color: "#212121", }, relatedCard: { flexDirection: "row", paddingHorizontal: 12, paddingVertical: 8, - backgroundColor: "#fff", borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: "#f0f0f0", gap: 10, }, relatedThumbWrap: { @@ -555,7 +543,6 @@ const styles = StyleSheet.create({ height: 68, borderRadius: 4, overflow: "hidden", - backgroundColor: "#eee", flexShrink: 0, }, relatedThumb: { width: 120, height: 68 }, @@ -574,7 +561,7 @@ const styles = StyleSheet.create({ justifyContent: "space-between", paddingVertical: 2, }, - relatedTitle: { fontSize: 13, color: "#212121", lineHeight: 18 }, + relatedTitle: { fontSize: 13, lineHeight: 18 }, relatedOwner: { fontSize: 12, color: "#999" }, relatedView: { fontSize: 11, color: "#bbb" }, sortRow: { @@ -584,7 +571,6 @@ const styles = StyleSheet.create({ paddingVertical: 10, gap: 8, borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: "#f0f0f0", }, sortLabel: { fontSize: 13, color: "#999", marginRight: 4 }, sortBtn: { From 12e770a0bf4885adc553ac6bb26314ea70586853 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 24 Mar 2026 23:08:17 +0800 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20=E6=8E=A8=E8=8D=90=E5=9B=BE=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D=E8=89=B2=E3=80=81=E8=AF=84=E8=AE=BA=E6=96=87=E5=AD=97?= =?UTF-8?q?=E3=80=81=E5=BC=B9=E5=B9=95=E8=83=8C=E6=99=AF=E8=89=B2=E8=B7=9F?= =?UTF-8?q?=E9=9A=8F=E6=9A=97=E9=BB=91=E4=B8=BB=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/video/[bvid].tsx | 2 +- components/CommentItem.tsx | 6 ++++-- components/DanmakuList.tsx | 18 ++++++++++-------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/app/video/[bvid].tsx b/app/video/[bvid].tsx index f1fef41..62604de 100644 --- a/app/video/[bvid].tsx +++ b/app/video/[bvid].tsx @@ -209,7 +209,7 @@ export default function VideoDetailScreen() { onPress={() => router.push(`/video/${item.bvid}` as any)} activeOpacity={0.85} > - + + {item.member.uname} - {item.content.message} + {item.content.message} {formatTime(item.ctime)} diff --git a/components/DanmakuList.tsx b/components/DanmakuList.tsx index e93416e..f7c990f 100644 --- a/components/DanmakuList.tsx +++ b/components/DanmakuList.tsx @@ -12,6 +12,7 @@ import { import { Ionicons } from "@expo/vector-icons"; import { DanmakuItem } from "../services/types"; import { danmakuColorToCss } from "../utils/danmaku"; +import { useTheme } from "../utils/theme"; interface Props { danmakus: DanmakuItem[]; @@ -65,6 +66,7 @@ export default function DanmakuList({ maxItems = 100, giftCounts, }: Props) { + const theme = useTheme(); const flatListRef = useRef(null); const [displayedItems, setDisplayedItems] = useState([]); const [unseenCount, setUnseenCount] = useState(0); @@ -213,7 +215,7 @@ export default function DanmakuList({ const guard = item.guardLevel ? GUARD_LABELS[item.guardLevel] : null; const timeStr = formatLiveTime(item.timeline); return ( - + {timeStr ? ( {timeStr} ) : null} @@ -240,27 +242,27 @@ export default function DanmakuList({ {item.uname ?? "匿名"} - + {item.text} ); - }, []); + }, [theme]); // ─── Video mode render (original bubble) ─────────────────────────────────── const renderVideoItem = useCallback(({ item }: { item: DisplayedDanmaku }) => { const dotColor = danmakuColorToCss(item.color); return ( - + - + {item.text} {formatTimestamp(item.time)} ); - }, []); + }, [theme]); const keyExtractor = useCallback( (item: DisplayedDanmaku) => String(item._key), @@ -268,7 +270,7 @@ export default function DanmakuList({ ); return ( - + {!hideHeader && ( Date: Tue, 24 Mar 2026 23:48:04 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat:=E6=9A=97=E9=BB=91=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/video/[bvid].tsx | 123 ++++++++++++++++++++++++------- components/FollowedLiveStrip.tsx | 2 +- 2 files changed, 99 insertions(+), 26 deletions(-) diff --git a/app/video/[bvid].tsx b/app/video/[bvid].tsx index 62604de..b8a6146 100644 --- a/app/video/[bvid].tsx +++ b/app/video/[bvid].tsx @@ -75,14 +75,21 @@ export default function VideoDetailScreen() { router.back()} style={styles.backBtn}> - + {video?.title ?? "视频详情"} setShowDownload(true)} > - + @@ -115,7 +122,11 @@ export default function VideoDetailScreen() { onPress={() => setTab("intro")} > 简介 @@ -126,7 +137,11 @@ export default function VideoDetailScreen() { onPress={() => setTab("comments")} > 评论 {video.stat?.reply > 0 ? ` ${formatCount(video.stat.reply)}` : ""} @@ -138,7 +153,11 @@ export default function VideoDetailScreen() { onPress={() => setTab("danmaku")} > 弹幕 {danmakus.length > 0 ? ` ${formatCount(danmakus.length)}` : ""} @@ -170,13 +189,25 @@ export default function VideoDetailScreen() { source={{ uri: proxyImageUrl(video.owner.face) }} style={styles.avatar} /> - {video.owner.name} + + {video.owner.name} + + 关注 - - {video.title} + + + {video.title} + + + {video.desc || "暂无简介"} + @@ -193,23 +224,42 @@ export default function VideoDetailScreen() { } /> )} - - - {video.desc || "暂无简介"} + + + 推荐视频 - - 推荐视频 - } renderItem={({ item }) => ( router.push(`/video/${item.bvid}` as any)} activeOpacity={0.85} > - + - + {item.title} + 排序 + - 合集 · {season.title} + + 合集 · {season.title} + {season.ep_count}个视频 @@ -395,7 +457,11 @@ function SeasonSection({ const isCurrent = ep.bvid === currentBvid; return ( !isCurrent && onEpisodePress(ep.bvid)} activeOpacity={0.8} > @@ -408,7 +474,10 @@ function SeasonSection({ 第{index + 1}集 - + {ep.title} @@ -447,6 +516,10 @@ const styles = StyleSheet.create({ lineHeight: 22, marginBottom: 8, }, + subTitle: { + fontSize: 10, + marginBottom: 8, + }, statsRow: { flexDirection: "row", gap: 16 }, stat: { flexDirection: "row", alignItems: "center", gap: 3 }, statText: { fontSize: 12, color: "#999" }, @@ -522,9 +595,9 @@ const styles = StyleSheet.create({ danmakuTab: { flex: 1 }, emptyTxt: { textAlign: "center", color: "#bbb", padding: 30 }, relatedHeader: { - paddingHorizontal: 14, - paddingVertical: 10, - borderTopWidth: StyleSheet.hairlineWidth, + paddingLeft: 12, + paddingBottom: 8, + paddingTop: 8, }, relatedHeaderText: { fontSize: 13, diff --git a/components/FollowedLiveStrip.tsx b/components/FollowedLiveStrip.tsx index 150bcb1..5af6c7c 100644 --- a/components/FollowedLiveStrip.tsx +++ b/components/FollowedLiveStrip.tsx @@ -65,7 +65,7 @@ export function FollowedLiveStrip() { const styles = StyleSheet.create({ container: { backgroundColor: "#f4f4f4", - paddingHorizontal: 12, + paddingHorizontal: 3, paddingVertical: 8, }, scrollContent: { From d84e2de5a1c467db9a96cc98464adc0069d97296 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 24 Mar 2026 23:57:03 +0800 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=8E=A8=E8=8D=90?= =?UTF-8?q?=E5=88=97=E8=A1=A8=20ListEmptyComponent=20=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E3=80=81epCard=20offset=20=E5=81=8F=E7=A7=BB=E5=92=8C=E5=8F=8C?= =?UTF-8?q?=E8=BE=B9=E6=A1=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/video/[bvid].tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/video/[bvid].tsx b/app/video/[bvid].tsx index b8a6146..a8f4f13 100644 --- a/app/video/[bvid].tsx +++ b/app/video/[bvid].tsx @@ -245,8 +245,6 @@ export default function VideoDetailScreen() { style={[ styles.relatedCard, { - borderTopWidth: StyleSheet.hairlineWidth, - borderTopColor: theme.border, backgroundColor: theme.card, borderBottomColor: theme.border, }, @@ -296,7 +294,7 @@ export default function VideoDetailScreen() { )} ListEmptyComponent={ - !relatedLoading ? ( + relatedLoading ? ( ) : null } @@ -449,7 +447,7 @@ function SeasonSection({ contentContainerStyle={{ paddingHorizontal: 12, gap: 10 }} getItemLayout={(_data, index) => ({ length: 130, - offset: 12 + index * 130, + offset: 12 + index * 140, index, })} onScrollToIndexFailed={() => {}} From 7870c83b12093bd6c6195bac06f943869a63de30 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 24 Mar 2026 23:59:21 +0800 Subject: [PATCH 9/9] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20CHANGELOG?= =?UTF-8?q?=EF=BC=8C=E8=AE=B0=E5=BD=95=20v1.0.8=20=E6=96=B0=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=92=8C=E4=BF=AE=E5=A4=8D=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca6a62e..ab34285 100644 --- a/CHANGELOG.md +++ b/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 ### 首个正式版本