From e1fa8cc347d10c8928ef1e9cab1cdd80cab4c823 Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 25 Mar 2026 12:03:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=86=E9=A2=91=E6=8E=A8=E8=8D=90?= =?UTF-8?q?=E5=85=B3=E8=81=94=E5=BD=93=E5=89=8D=E8=A7=86=E9=A2=91=E3=80=81?= =?UTF-8?q?UP=E4=B8=BB=E4=BF=A1=E6=81=AF=E5=B1=95=E7=A4=BA=E3=80=81?= =?UTF-8?q?=E7=9B=B4=E6=92=AD=E5=85=A8=E5=B1=8F=E4=BF=AE=E5=A4=8D=E5=8F=8A?= =?UTF-8?q?UI=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 推荐列表改用 getVideoRelated 基于当前 bvid 推荐,与首页 feed 解耦 - 视频详情页 UP主名称下方展示粉丝数和视频数 - 推荐视频点击改为 router.replace 避免页面堆叠 - 直播全屏改用 position:absolute,解决退出全屏视频重建暂停问题 - 退出全屏时直播自动暂停 - 直播画质选中状态修复,过滤杜比/4K 画质选项 - 直播画质面板改为居中 Modal 弹出框 - 视频详情 Tab 按钮左对齐,评论排序及设置页按钮统一实心背景风格 --- CHANGELOG.md | 20 ++- app/settings.tsx | 16 +-- app/video/[bvid].tsx | 59 ++++---- components/DanmakuList.tsx | 148 +++++++++++-------- components/LivePlayer.tsx | 284 ++++++++++++++++++++++--------------- hooks/useLiveDetail.ts | 2 +- hooks/useRelatedVideos.ts | 19 ++- services/bilibili.ts | 19 ++- 8 files changed, 340 insertions(+), 227 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab34285..425ff5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,25 @@ --- -## [1.0.8] - 2026-03-24 +## [1.0.12] - 2026-03-25 + +### 新增 +- **UP主信息**:视频详情页博主名称下方展示粉丝数和视频数(`getUploaderStat` → `/x/web-interface/card`) +- **视频相关推荐**:详情页推荐列表改为基于当前视频(`getVideoRelated` → `/x/web-interface/archive/related`),不再与首页 feed 共用 + +### 修复 +- **直播全屏退出暂停**:全屏改用 `position:absolute` 覆盖,Video 组件始终在同一棵 React 树中,不再因 Modal 切换导致重建暂停;退出全屏时直播自动暂停 +- **直播画质选中**:`changeQuality` 强制用请求的 `qn` 覆盖服务端协商值,画质面板高亮与用户选择一致 +- **直播画质过滤**:过滤 `qn > 10000` 的选项(杜比/4K),最高仅展示原画 +- **推荐视频导航**:点击推荐列表改用 `router.replace`,避免详情页无限堆叠 + +### 优化 +- **直播画质面板**:改为居中 Modal 弹出框 +- **视频详情 Tab**:按钮向左靠齐,移除均分宽度 +- **评论排序按钮**:统一为实心背景风格(`#f0f0f0` → `#00AEEC`),与直播分区 Tab 一致 +- **设置页按钮**:外观/流量选项按钮统一为实心背景风格 + +## [1.0.11] - 2026-03-24 ### 新增 - **暗黑模式**:全局主题系统(`utils/theme.ts`),支持亮色 / 暗色一键切换,覆盖所有页面和组件 diff --git a/app/settings.tsx b/app/settings.tsx index 7377caf..33a0463 100644 --- a/app/settings.tsx +++ b/app/settings.tsx @@ -140,16 +140,14 @@ const styles = StyleSheet.create({ sectionLabel: { fontSize: 13, marginBottom: 10 }, optionRow: { flexDirection: 'row', gap: 10 }, option: { - paddingHorizontal: 20, - paddingVertical: 6, - borderRadius: 20, - borderWidth: 1, - borderColor: '#e0e0e0', - backgroundColor: 'transparent', + paddingHorizontal: 10, + paddingVertical: 2, + borderRadius: 16, + backgroundColor: '#f0f0f0', }, - optionActive: { borderColor: '#00AEEC', backgroundColor: '#e8f7fd' }, - optionText: { fontSize: 14, color: '#666' }, - optionTextActive: { color: '#00AEEC', fontWeight: '600' }, + optionActive: { backgroundColor: '#00AEEC' }, + optionText: { fontSize: 13, color: '#333', fontWeight: '500' }, + optionTextActive: { color: '#fff', fontWeight: '600' }, hint: { fontSize: 12, marginTop: 8 }, versionRow: { flexDirection: 'row', diff --git a/app/video/[bvid].tsx b/app/video/[bvid].tsx index a8f4f13..7e5ecdf 100644 --- a/app/video/[bvid].tsx +++ b/app/video/[bvid].tsx @@ -13,7 +13,7 @@ import { useLocalSearchParams, useRouter } from "expo-router"; import { Ionicons } from "@expo/vector-icons"; import { VideoPlayer } from "../../components/VideoPlayer"; import { CommentItem } from "../../components/CommentItem"; -import { getDanmaku } from "../../services/bilibili"; +import { getDanmaku, getUploaderStat } from "../../services/bilibili"; import { DanmakuItem } from "../../services/types"; import DanmakuList from "../../components/DanmakuList"; import { useVideoDetail } from "../../hooks/useVideoDetail"; @@ -49,11 +49,12 @@ export default function VideoDetailScreen() { const [danmakus, setDanmakus] = useState([]); const [currentTime, setCurrentTime] = useState(0); const [showDownload, setShowDownload] = useState(false); + const [uploaderStat, setUploaderStat] = useState<{ follower: number; archiveCount: number } | null>(null); const { videos: relatedVideos, loading: relatedLoading, load: loadRelated, - } = useRelatedVideos(); + } = useRelatedVideos(bvid as string); useEffect(() => { loadRelated(); @@ -68,6 +69,11 @@ export default function VideoDetailScreen() { getDanmaku(video.cid).then(setDanmakus); }, [video?.cid]); + useEffect(() => { + if (!video?.owner?.mid) return; + getUploaderStat(video.owner.mid).then(setUploaderStat).catch(() => {}); + }, [video?.owner?.mid]); + return ( {/* TopBar */} @@ -178,10 +184,6 @@ export default function VideoDetailScreen() { data={relatedVideos} keyExtractor={(item) => item.bvid} showsVerticalScrollIndicator={false} - onEndReached={() => { - if (!relatedLoading) loadRelated(); - }} - onEndReachedThreshold={0.5} ListHeaderComponent={ <> @@ -189,9 +191,16 @@ export default function VideoDetailScreen() { source={{ uri: proxyImageUrl(video.owner.face) }} style={styles.avatar} /> - - {video.owner.name} - + + + {video.owner.name} + + {uploaderStat && ( + + {formatCount(uploaderStat.follower)}粉丝 · {formatCount(uploaderStat.archiveCount)}视频 + + )} + + 关注 @@ -249,7 +258,7 @@ export default function VideoDetailScreen() { borderBottomColor: theme.border, }, ]} - onPress={() => router.push(`/video/${item.bvid}` as any)} + onPress={() => router.replace(`/video/${item.bvid}` as any)} activeOpacity={0.85} > - 排序 ({ length: 130, - offset: 12 + index * 140, + offset: 12 + index * 130, index, })} onScrollToIndexFailed={() => {}} @@ -528,8 +536,10 @@ const styles = StyleSheet.create({ paddingBottom: 0, paddingTop: 12, }, - avatar: { width: 48, height: 48, borderRadius: 30, marginRight: 10 }, - upName: { flex: 1, fontSize: 14, fontWeight: "500" }, + avatar: { width: 40, height: 40, borderRadius: 30, marginRight: 10 }, + upInfo: { flex: 1, justifyContent: "center" }, + upName: { fontSize: 14, fontWeight: "500" }, + upStat: { fontSize: 11, color: "#999", marginTop: 2 }, followBtn: { backgroundColor: "#00AEEC", paddingHorizontal: 10, @@ -570,11 +580,12 @@ const styles = StyleSheet.create({ tabBar: { flexDirection: "row", borderBottomWidth: StyleSheet.hairlineWidth, + paddingLeft: 3, }, tabItem: { - flex: 1, alignItems: "center", paddingVertical: 12, + paddingHorizontal: 12, position: "relative", }, tabLabel: { fontSize: 13 }, @@ -593,7 +604,7 @@ const styles = StyleSheet.create({ danmakuTab: { flex: 1 }, emptyTxt: { textAlign: "center", color: "#bbb", padding: 30 }, relatedHeader: { - paddingLeft: 12, + paddingLeft: 13, paddingBottom: 8, paddingTop: 8, }, @@ -643,15 +654,13 @@ const styles = StyleSheet.create({ gap: 8, borderBottomWidth: StyleSheet.hairlineWidth, }, - sortLabel: { fontSize: 13, color: "#999", marginRight: 4 }, sortBtn: { - paddingHorizontal: 14, - paddingVertical: 3, - borderRadius: 20, - borderWidth: 1, - borderColor: "#e0e0e0", + paddingHorizontal: 10, + paddingVertical: 2, + borderRadius: 16, + backgroundColor: "#f0f0f0", }, - sortBtnActive: { borderColor: "#00AEEC", backgroundColor: "#e8f7fd" }, - sortBtnTxt: { fontSize: 12, color: "#666" }, - sortBtnTxtActive: { color: "#00AEEC", fontWeight: "600" as const }, + sortBtnActive: { backgroundColor: "#00AEEC" }, + sortBtnTxt: { fontSize: 13, color: "#333", fontWeight: "500" }, + sortBtnTxtActive: { color: "#fff", fontWeight: "600" as const }, }); diff --git a/components/DanmakuList.tsx b/components/DanmakuList.tsx index f7c990f..e7677a7 100644 --- a/components/DanmakuList.tsx +++ b/components/DanmakuList.tsx @@ -211,58 +211,81 @@ export default function DanmakuList({ }, []); // ─── Live mode render (B站-style chat) ───────────────────────────────────── - const renderLiveItem = useCallback(({ item }: { item: DisplayedDanmaku }) => { - const guard = item.guardLevel ? GUARD_LABELS[item.guardLevel] : null; - const timeStr = formatLiveTime(item.timeline); - return ( - - {timeStr ? ( - {timeStr} - ) : null} - - {guard && ( - - {guard.text} - - )} - {item.isAdmin && ( - - 房管 - - )} - {item.medalLevel != null && item.medalName && ( - - {item.medalName} - - {item.medalLevel} + const renderLiveItem = useCallback( + ({ item }: { item: DisplayedDanmaku }) => { + const guard = item.guardLevel ? GUARD_LABELS[item.guardLevel] : null; + const timeStr = formatLiveTime(item.timeline); + return ( + + {timeStr ? {timeStr} : null} + + {guard && ( + + {guard.text} - - )} - - {item.uname ?? "匿名"} - - - - {item.text} - - - - ); - }, [theme]); + )} + {item.isAdmin && ( + + 房管 + + )} + {item.medalLevel != null && item.medalName && ( + + {item.medalName} + + {item.medalLevel} + + + )} + + {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 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), @@ -270,7 +293,13 @@ export default function DanmakuList({ ); return ( - + {!hideHeader && ( )} - ); } @@ -370,13 +403,6 @@ const styles = StyleSheet.create({ marginVertical: 2, gap: 8, }, - colorDot: { - width: 6, - height: 6, - borderRadius: 3, - marginTop: 6, - flexShrink: 0, - }, bubbleText: { flex: 1, fontSize: 13, @@ -425,8 +451,6 @@ const liveStyles = StyleSheet.create({ flexDirection: "row", alignItems: "flex-start", paddingVertical: 5, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: "#f0f0f2", }, time: { fontSize: 10, diff --git a/components/LivePlayer.tsx b/components/LivePlayer.tsx index 2657b0e..7f6d355 100644 --- a/components/LivePlayer.tsx +++ b/components/LivePlayer.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useCallback, useEffect } from 'react'; +import React, { useState, useRef, useCallback, useEffect } from "react"; import { View, Text, @@ -8,8 +8,8 @@ import { Modal, Platform, useWindowDimensions, -} from 'react-native'; -import { Ionicons } from '@expo/vector-icons'; +} from "react-native"; +import { Ionicons } from "@expo/vector-icons"; interface Props { hlsUrl: string; @@ -23,16 +23,23 @@ interface Props { const HIDE_DELAY = 3000; const HEADERS = { - Referer: 'https://live.bilibili.com', - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + Referer: "https://live.bilibili.com", + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", }; -export function LivePlayer({ hlsUrl, flvUrl, isLive, qualities = [], currentQn = 0, onQualityChange }: Props) { +export function LivePlayer({ + hlsUrl, + flvUrl, + isLive, + qualities = [], + currentQn = 0, + onQualityChange, +}: Props) { const { width: SCREEN_W, height: SCREEN_H } = useWindowDimensions(); const VIDEO_H = SCREEN_W * 0.5625; - if (Platform.OS === 'web') { + if (Platform.OS === "web") { return ( 请在手机端观看直播 @@ -79,7 +86,7 @@ function NativeLivePlayer({ currentQn: number; onQualityChange?: (qn: number) => void; }) { - const Video = require('react-native-video').default; + const Video = require("react-native-video").default; const [showControls, setShowControls] = useState(true); const [paused, setPaused] = useState(false); @@ -97,14 +104,16 @@ function NativeLivePlayer({ useEffect(() => { resetHideTimer(); - return () => { if (hideTimer.current) clearTimeout(hideTimer.current); }; + return () => { + if (hideTimer.current) clearTimeout(hideTimer.current); + }; }, []); // Lock/unlock orientation on fullscreen toggle useEffect(() => { (async () => { try { - const ScreenOrientation = require('expo-screen-orientation'); + const ScreenOrientation = require("expo-screen-orientation"); if (isFullscreen) { await ScreenOrientation.lockAsync( ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT, @@ -114,7 +123,9 @@ function NativeLivePlayer({ ScreenOrientation.OrientationLock.PORTRAIT_UP, ); } - } catch { /* graceful degradation in Expo Go */ } + } catch { + /* graceful degradation in Expo Go */ + } })(); }, [isFullscreen]); @@ -123,18 +134,23 @@ function NativeLivePlayer({ return () => { (async () => { try { - const ScreenOrientation = require('expo-screen-orientation'); + const ScreenOrientation = require("expo-screen-orientation"); await ScreenOrientation.lockAsync( ScreenOrientation.OrientationLock.PORTRAIT_UP, ); - } catch { /* ignore */ } + } catch { + /* ignore */ + } })(); }; }, []); const handleTap = useCallback(() => { - setShowControls(prev => { - if (!prev) { resetHideTimer(); return true; } + setShowControls((prev) => { + if (!prev) { + resetHideTimer(); + return true; + } if (hideTimer.current) clearTimeout(hideTimer.current); return false; }); @@ -143,23 +159,27 @@ function NativeLivePlayer({ const fsW = Math.max(screenW, screenH); const fsH = Math.min(screenW, screenH); const containerStyle = isFullscreen - ? { width: fsW, height: fsH } + ? { position: 'absolute' as const, top: 0, left: 0, width: fsW, height: fsH, zIndex: 999, elevation: 999 } : { width: screenW, height: videoH }; - const currentQnDesc = qualities.find(q => q.qn === currentQn)?.desc ?? ''; + const currentQnDesc = qualities.find((q) => q.qn === currentQn)?.desc ?? ""; const videoContent = ( ); - if (isFullscreen) { - return ( - setIsFullscreen(false)}> - {videoContent} - - ); - } - return videoContent; } const styles = StyleSheet.create({ container: { - backgroundColor: '#000', - alignItems: 'center', - justifyContent: 'center', + backgroundColor: "#000", + alignItems: "center", + justifyContent: "center", }, - webHint: { color: '#fff', fontSize: 15 }, - offlineText: { color: '#999', fontSize: 14, marginTop: 10 }, + webHint: { color: "#fff", fontSize: 15 }, + offlineText: { color: "#999", fontSize: 14, marginTop: 10 }, bufferingOverlay: { ...StyleSheet.absoluteFillObject, - alignItems: 'center', - justifyContent: 'center', + alignItems: "center", + justifyContent: "center", }, - bufferingText: { color: '#fff', fontSize: 13, opacity: 0.8 }, + bufferingText: { color: "#fff", fontSize: 13, opacity: 0.8 }, liveBadge: { - position: 'absolute', + position: "absolute", top: 10, left: 12, - flexDirection: 'row', - alignItems: 'center', - backgroundColor: 'rgba(0,0,0,0.5)', + flexDirection: "row", + alignItems: "center", + backgroundColor: "rgba(0,0,0,0.5)", paddingHorizontal: 8, paddingVertical: 3, borderRadius: 4, @@ -289,79 +340,78 @@ const styles = StyleSheet.create({ width: 6, height: 6, borderRadius: 3, - backgroundColor: '#f00', + backgroundColor: "#f00", marginRight: 5, }, - liveText: { color: '#fff', fontSize: 11, fontWeight: '700', letterSpacing: 1 }, + liveText: { + color: "#fff", + fontSize: 11, + fontWeight: "700", + letterSpacing: 1, + }, centerBtn: { - position: 'absolute', - top: '50%', - left: '50%', + position: "absolute", + top: "50%", + left: "50%", transform: [{ translateX: -28 }, { translateY: -28 }], }, centerBtnBg: { width: 56, height: 56, borderRadius: 28, - backgroundColor: 'rgba(0,0,0,0.45)', - alignItems: 'center', - justifyContent: 'center', + backgroundColor: "rgba(0,0,0,0.45)", + alignItems: "center", + justifyContent: "center", }, bottomBar: { - position: 'absolute', + position: "absolute", bottom: 0, left: 0, right: 0, - flexDirection: 'row', - alignItems: 'center', + flexDirection: "row", + alignItems: "center", paddingHorizontal: 8, paddingBottom: 8, paddingTop: 24, - backgroundColor: 'rgba(0,0,0,0)', + backgroundColor: "rgba(0,0,0,0)", }, ctrlBtn: { paddingHorizontal: 8, paddingVertical: 4 }, qualityBtn: { paddingHorizontal: 8, paddingVertical: 4, - backgroundColor: 'rgba(0,0,0,0.4)', + backgroundColor: "rgba(0,0,0,0.4)", borderRadius: 4, marginRight: 4, }, - qualityText: { color: '#fff', fontSize: 11, fontWeight: '600' }, + qualityText: { color: "#fff", fontSize: 11, fontWeight: "600" }, qualityOverlay: { - ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(0,0,0,0.5)', - justifyContent: 'flex-end', + flex: 1, + backgroundColor: "rgba(0,0,0,0.5)", + justifyContent: "center", + alignItems: "center", }, qualityPanel: { - backgroundColor: 'rgba(20,20,20,0.95)', - borderTopLeftRadius: 12, - borderTopRightRadius: 12, - paddingBottom: 16, + backgroundColor: "#fff", + borderRadius: 12, + paddingVertical: 8, + paddingHorizontal: 16, + minWidth: 180, }, qualityPanelTitle: { - color: '#fff', - fontSize: 13, - fontWeight: '600', - textAlign: 'center', - paddingVertical: 12, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: 'rgba(255,255,255,0.1)', + fontSize: 15, + fontWeight: "700", + color: "#212121", + paddingVertical: 10, + textAlign: "center", }, qualityItem: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 20, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", paddingVertical: 12, + borderTopWidth: StyleSheet.hairlineWidth, + borderTopColor: "#eee", }, - qualityItemActive: { backgroundColor: 'rgba(0,174,236,0.15)' }, - qualityItemText: { color: '#ccc', fontSize: 14 }, - qualityItemTextActive: { color: '#00AEEC', fontWeight: '600' }, - fsModal: { - flex: 1, - backgroundColor: '#000', - alignItems: 'center', - justifyContent: 'center', - }, + qualityItemText: { fontSize: 14, color: "#333" }, + qualityItemTextActive: { color: "#00AEEC", fontWeight: "700" }, }); diff --git a/hooks/useLiveDetail.ts b/hooks/useLiveDetail.ts index 1dbab95..ae3b249 100644 --- a/hooks/useLiveDetail.ts +++ b/hooks/useLiveDetail.ts @@ -53,7 +53,7 @@ export function useLiveDetail(roomId: number) { const changeQuality = useCallback(async (qn: number) => { try { const stream = await getLiveStreamUrl(roomId, qn); - setState(prev => ({ ...prev, stream })); + setState(prev => ({ ...prev, stream: { ...stream, qn } })); } catch { /* ignore */ } }, [roomId]); diff --git a/hooks/useRelatedVideos.ts b/hooks/useRelatedVideos.ts index 70b2638..d5406e2 100644 --- a/hooks/useRelatedVideos.ts +++ b/hooks/useRelatedVideos.ts @@ -1,29 +1,26 @@ -import { useState, useCallback, useRef, useMemo } from 'react'; -import { getRecommendFeed } from '../services/bilibili'; +import { useState, useCallback, useRef } from 'react'; +import { getVideoRelated } from '../services/bilibili'; import type { VideoItem } from '../services/types'; -export function useRelatedVideos() { - const [pages, setPages] = useState([]); +export function useRelatedVideos(bvid: string) { + const [videos, setVideos] = useState([]); const [loading, setLoading] = useState(false); const loadingRef = useRef(false); - const freshIdxRef = useRef(0); const load = useCallback(async () => { if (loadingRef.current) return; loadingRef.current = true; setLoading(true); try { - const data = await getRecommendFeed(freshIdxRef.current); - setPages(prev => [...prev, data]); - freshIdxRef.current += 1; + const data = await getVideoRelated(bvid); + setVideos(data); } catch (e) { console.warn('useRelatedVideos: failed', e); } finally { loadingRef.current = false; setLoading(false); } - }, []); + }, [bvid]); - const videos = useMemo(() => pages.flat(), [pages]); - return { videos, loading, load }; + return { videos, loading, load, hasMore: false }; } diff --git a/services/bilibili.ts b/services/bilibili.ts index 7c94c9a..f6824fc 100644 --- a/services/bilibili.ts +++ b/services/bilibili.ts @@ -114,6 +114,12 @@ export async function getVideoDetail(bvid: string): Promise { return res.data.data as VideoItem; } +export async function getVideoRelated(bvid: string): Promise { + const res = await api.get('/x/web-interface/archive/related', { params: { bvid } }); + const items: any[] = res.data.data ?? []; + return items as VideoItem[]; +} + export async function getPlayUrl(bvid: string, cid: number, qn = 64): Promise { const isAndroid = Platform.OS === 'android'; // 1488 = 16(DASH)|64(HDR)|128(4K)|256(杜比全景声)|1024(杜比视界) @@ -143,6 +149,15 @@ export async function getPlayUrlForDownload( return url; } +export async function getUploaderStat(mid: number): Promise<{ follower: number; archiveCount: number }> { + const res = await api.get('/x/web-interface/card', { params: { mid } }); + const data = res.data.data ?? {}; + return { + follower: data.follower ?? 0, + archiveCount: data.archive_count ?? 0, + }; +} + export async function getUserInfo(): Promise<{ face: string; uname: string; mid: number }> { const res = await api.get('/x/web-interface/nav'); const { face, uname, mid } = res.data.data; @@ -286,7 +301,9 @@ export async function getLiveStreamUrl(roomId: number, qn = 10000): Promise ({ qn: q.qn as number, desc: q.desc as string })); + const qualities = gQnDesc + .map((q: any) => ({ qn: q.qn as number, desc: q.desc as string })) + .filter(q => q.qn <= 10000); let hlsUrl = ''; let flvUrl = '';