From f1c040de68493a1a9d0d633983dae725a17ec54c Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 19 Mar 2026 17:00:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E4=BA=8C=E7=BB=B4=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=92=8C=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/LoginModal.tsx | 111 ++++++++++++++++++++++++++++--- components/NativeVideoPlayer.tsx | 17 +++-- package-lock.json | 97 +++------------------------ package.json | 1 + utils/format.ts | 1 + 5 files changed, 127 insertions(+), 100 deletions(-) diff --git a/components/LoginModal.tsx b/components/LoginModal.tsx index c1e7e9b..2520e73 100644 --- a/components/LoginModal.tsx +++ b/components/LoginModal.tsx @@ -7,7 +7,12 @@ import { TouchableOpacity, Image, ActivityIndicator, + Animated, + Alert, } from "react-native"; +import * as FileSystem from "expo-file-system/legacy"; +import * as MediaLibrary from "expo-media-library"; +import { Ionicons } from "@expo/vector-icons"; import { generateQRCode, pollQRCode, getUserInfo } from "../services/bilibili"; import { useAuthStore } from "../store/authStore"; @@ -19,6 +24,8 @@ interface Props { export function LoginModal({ visible, onClose }: Props) { const [qrUrl, setQrUrl] = useState(null); const [qrKey, setQrKey] = useState(null); + const [qrImageLoaded, setQrImageLoaded] = useState(false); + const [saving, setSaving] = useState(false); const [status, setStatus] = useState< "loading" | "waiting" | "scanned" | "done" | "error" >("loading"); @@ -26,15 +33,31 @@ export function LoginModal({ visible, onClose }: Props) { const login = useAuthStore((s) => s.login); const setProfile = useAuthStore((s) => s.setProfile); + // sheet 滑入动画 + const slideY = useRef(new Animated.Value(300)).current; + + useEffect(() => { + if (visible) { + Animated.spring(slideY, { + toValue: 0, + useNativeDriver: true, + bounciness: 4, + }).start(); + } else { + slideY.setValue(300); + } + }, [visible]); + useEffect(() => { if (!visible) return; setStatus("loading"); setQrUrl(null); setQrKey(null); + setQrImageLoaded(false); generateQRCode() .then((data) => { setQrUrl( - `https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(data.url)}&size=200x200`, + `https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(data.url)}&size=400x400`, ); setQrKey(data.qrcode_key); setStatus("waiting"); @@ -60,7 +83,6 @@ export function LoginModal({ visible, onClose }: Props) { try { await login(result.cookie, "", ""); setStatus("done"); - // 登录后异步拉取用户头像和昵称 const info = await getUserInfo(); setProfile(info.face, info.uname, String(info.mid)); } catch { @@ -74,6 +96,26 @@ export function LoginModal({ visible, onClose }: Props) { }; }, [qrKey, status]); + async function handleSaveQR() { + if (!qrUrl) return; + setSaving(true); + try { + const { status: perm } = await MediaLibrary.requestPermissionsAsync(); + if (perm !== "granted") { + Alert.alert("提示", "需要相册权限才能保存图片"); + return; + } + const dest = `${FileSystem.cacheDirectory}bilibili_qr.png`; + const { uri } = await FileSystem.downloadAsync(qrUrl, dest); + await MediaLibrary.saveToLibraryAsync(uri); + Alert.alert("已保存", "二维码已存入相册"); + } catch { + Alert.alert("失败", "保存失败,请重试"); + } finally { + setSaving(false); + } + } + return ( - + {/* 遮罩固定不动 */} + + + {/* sheet 独立滑入 */} + 扫码登录 {status === "loading" && ( @@ -93,7 +141,31 @@ export function LoginModal({ visible, onClose }: Props) { )} {(status === "waiting" || status === "scanned") && qrUrl && ( <> - + + setQrImageLoaded(true)} + /> + {!qrImageLoaded && ( + + + + )} + {qrImageLoaded && ( + + {saving ? ( + + ) : ( + + )} + + )} + {status === "scanned" ? "扫描成功,请在手机确认" @@ -108,16 +180,21 @@ export function LoginModal({ visible, onClose }: Props) { 关闭 - + ); } const styles = StyleSheet.create({ overlay: { - flex: 1, + ...StyleSheet.absoluteFillObject, backgroundColor: "rgba(0,0,0,0.5)", - justifyContent: "flex-end", + }, + sheetWrapper: { + position: "absolute", + bottom: 0, + left: 0, + right: 0, }, sheet: { backgroundColor: "#fff", @@ -128,7 +205,25 @@ const styles = StyleSheet.create({ }, title: { fontSize: 18, fontWeight: "600", marginBottom: 20 }, loader: { marginVertical: 40 }, - qr: { 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: { + position: "absolute", + bottom: 6, + right: 6, + backgroundColor: "rgba(0,0,0,0.45)", + borderRadius: 14, + width: 28, + height: 28, + alignItems: "center", + justifyContent: "center", + }, hint: { fontSize: 13, color: "#666", marginBottom: 20 }, closeBtn: { padding: 12 }, closeTxt: { fontSize: 14, color: "#00AEEC" }, diff --git a/components/NativeVideoPlayer.tsx b/components/NativeVideoPlayer.tsx index d9890c1..a1448b1 100644 --- a/components/NativeVideoPlayer.tsx +++ b/components/NativeVideoPlayer.tsx @@ -413,7 +413,7 @@ export const NativeVideoPlayer = forwardRef( onQualityChange(80); return; } - console.warn('Video playback error:', e); + console.warn("Video playback error:", e); }} /> ) : ( @@ -436,13 +436,12 @@ export const NativeVideoPlayer = forwardRef( {showControls && ( <> - {/* 小窗口 */} + {/* 小窗口 */} - + > ( q.qn === currentQn && styles.qualityItemActive, ]} > - {q.desc}{q.qn === 126 ? ' DV' : ''} + {q.desc} + {q.qn === 126 ? " DV" : ""} {q.qn === currentQn && ( @@ -684,7 +684,12 @@ const styles = StyleSheet.create({ marginTop: 4, }, ctrlBtn: { paddingHorizontal: 8, paddingVertical: 4 }, - timeText: { color: "#fff", fontSize: 11, marginHorizontal: 2 }, + timeText: { + color: "#fff", + fontSize: 11, + marginHorizontal: 2, + fontWeight: "600", + }, qualityText: { color: "#fff", fontSize: 11, fontWeight: "600" }, modalOverlay: { flex: 1, diff --git a/package-lock.json b/package-lock.json index 9b4cbc3..325138c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,11 +16,11 @@ "expo-dev-client": "~55.0.11", "expo-file-system": "~55.0.10", "expo-linear-gradient": "~55.0.8", + "expo-media-library": "~55.0.10", "expo-router": "~55.0.4", "expo-screen-orientation": "~55.0.8", "expo-status-bar": "~55.0.4", "expo-system-ui": "~55.0.9", - "fast-xml-parser": "^5.5.1", "pako": "^2.1.0", "react": "19.2.0", "react-dom": "19.2.0", @@ -31,7 +31,6 @@ "react-native-video": "^6.19.0", "react-native-web": "^0.21.0", "react-native-webview": "13.16.0", - "xml2js": "^0.6.2", "zustand": "^5.0.11" }, "devDependencies": { @@ -4525,6 +4524,16 @@ "expo": "*" } }, + "node_modules/expo-media-library": { + "version": "55.0.10", + "resolved": "https://registry.npmjs.org/expo-media-library/-/expo-media-library-55.0.10.tgz", + "integrity": "sha512-xXmz8Do9BJSt1LrkC6r8l2HAXNdaAr2TdzCuiVo9u47Vuo54twlOgzLpdx4rIzODy/cclqbYb2tae4nCKHJLbw==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "55.0.8", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-55.0.8.tgz", @@ -5219,41 +5228,6 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, - "node_modules/fast-xml-builder": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.0.tgz", - "integrity": "sha512-7mtITW/we2/wTUZqMyBOR2F8xP4CRxMiSEcQxPIqdRWdO2L/HZSOlzoNyghmyDwNB8BDxePooV1ZTJpkOUhdRg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.1.2" - } - }, - "node_modules/fast-xml-parser": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.1.tgz", - "integrity": "sha512-JTpMz8P5mDoNYzXTmTT/xzWjFiCWi0U+UQTJtrFH9muXsr2RqtXZPbnCW5h2mKsOd4u3XcPWCvDSrnaBPlUcMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "fast-xml-builder": "^1.1.0", - "path-expression-matcher": "^1.1.2", - "strnum": "^2.1.2" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/fb-dotslash": { "version": "0.5.8", "resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz", @@ -7410,21 +7384,6 @@ "node": ">=8" } }, - "node_modules/path-expression-matcher": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.2.tgz", - "integrity": "sha512-LXWqJmcpp2BKOEmgt4CyuESFmBfPuhJlAHKJsFzuJU6CxErWk75BrO+Ni77M9OxHN6dCYKM4vj+21Z6cOL96YQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -8764,18 +8723,6 @@ "node": ">=8" } }, - "node_modules/strnum": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", - "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/structured-headers": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz", @@ -9389,28 +9336,6 @@ "node": ">=10.0.0" } }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xml2js/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/package.json b/package.json index 157b5d9..7473c00 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "expo-dev-client": "~55.0.11", "expo-file-system": "~55.0.10", "expo-linear-gradient": "~55.0.8", + "expo-media-library": "~55.0.10", "expo-router": "~55.0.4", "expo-screen-orientation": "~55.0.8", "expo-status-bar": "~55.0.4", diff --git a/utils/format.ts b/utils/format.ts index b6642be..2536ff5 100644 --- a/utils/format.ts +++ b/utils/format.ts @@ -1,6 +1,7 @@ export function formatCount(n: number): string { if (n >= 100_000_000) return (n / 100_000_000).toFixed(1) + '亿'; if (n >= 10_000) return (n / 10_000).toFixed(1) + '万'; + return String(n); }