From e9b00d026fe7205caf1e0bec8381bd2ed2d7a1c4 Mon Sep 17 00:00:00 2001 From: Developer Date: Sat, 14 Mar 2026 11:55:45 +0800 Subject: [PATCH] 1 --- app/index.tsx | 40 +++++++++++++++++++-------- components/BigVideoCard.tsx | 53 ++++++++--------------------------- components/LiveCard.tsx | 2 +- hooks/useVideoList.ts | 20 +++++++++++--- services/bilibili.ts | 40 +++++---------------------- utils/videoRows.ts | 55 ++++++++++++++++++++++++++----------- 6 files changed, 102 insertions(+), 108 deletions(-) diff --git a/app/index.tsx b/app/index.tsx index 4fda111..ae3ac6f 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -31,7 +31,7 @@ import { LoginModal } from "../components/LoginModal"; import { useVideoList } from "../hooks/useVideoList"; import { useLiveList } from "../hooks/useLiveList"; import { useAuthStore } from "../store/authStore"; -import { toListRows, type ListRow, type BigRow } from "../utils/videoRows"; +import { toListRows, type ListRow, type BigRow, type LiveRow } from "../utils/videoRows"; import { BigVideoCard } from "../components/BigVideoCard"; import type { LiveRoom } from "../services/types"; @@ -62,7 +62,7 @@ const LIVE_AREAS = [ export default function HomeScreen() { const router = useRouter(); - const { pages, loading, refreshing, load, refresh } = useVideoList(); + const { pages, liveRooms, loading, refreshing, load, refresh } = useVideoList(); const { rooms, loading: liveLoading, @@ -77,7 +77,7 @@ export default function HomeScreen() { const [liveAreaId, setLiveAreaId] = useState(0); const [visibleBigKey, setVisibleBigKey] = useState(null); - const rows = useMemo(() => toListRows(pages), [pages]); + const rows = useMemo(() => toListRows(pages, liveRooms), [pages, liveRooms]); const hotListRef = useRef(null); const liveListRef = useRef(null); @@ -186,16 +186,30 @@ export default function HomeScreen() { { - if (row.item.goto === 'live' && row.item.roomid) { - Linking.openURL(`https://live.bilibili.com/${row.item.roomid}`); - } else { - router.push(`/video/${row.item.bvid}` as any); - } - }} + onPress={() => router.push(`/video/${row.item.bvid}` as any)} /> ); } + if (row.type === "live") { + return ( + + + Linking.openURL(`https://live.bilibili.com/${row.left.roomid}`)} + /> + + {row.right && ( + + Linking.openURL(`https://live.bilibili.com/${row.right!.roomid}`)} + /> + + )} + + ); + } const right = row.right; return ( @@ -260,6 +274,8 @@ export default function HomeScreen() { keyExtractor={(row: any) => row.type === "big" ? `big-${row.item.bvid}` + : row.type === "live" + ? `live-${row.left.roomid}-${row.right?.roomid ?? "empty"}` : `pair-${row.left.bvid}-${row.right?.bvid ?? "empty"}` } contentContainerStyle={{ @@ -387,9 +403,9 @@ export default function HomeScreen() { /> )} - + {/* - + */} 哔哩哔哩 diff --git a/components/BigVideoCard.tsx b/components/BigVideoCard.tsx index 6925646..bf9b6cc 100644 --- a/components/BigVideoCard.tsx +++ b/components/BigVideoCard.tsx @@ -16,7 +16,6 @@ import { buildDashMpdUri } from "../utils/dash"; import { getPlayUrl, getVideoDetail } from "../services/bilibili"; import { proxyImageUrl } from "../utils/imageUrl"; import { formatCount, formatDuration } from "../utils/format"; -import { LivePulse } from "./LivePulse"; import type { VideoItem } from "../services/types"; const HEADERS = { @@ -75,11 +74,8 @@ export function BigVideoCard({ item, isVisible, onPress }: Props) { thumbOpacity.setValue(1); }, [item.bvid]); - const isLive = item.goto === 'live'; - // Fetch play URL when visible for the first time useEffect(() => { - if (isLive) return; if (!isVisible || videoUrl) return; let cancelled = false; (async () => { @@ -220,10 +216,8 @@ export function BigVideoCard({ item, isVisible, onPress }: Props) { /> - {/* Swipe gesture layer (video only) */} - {!isLive && ( - - )} + {/* Swipe gesture layer */} + {/* Seek time label */} {seekLabel && ( @@ -232,29 +226,18 @@ export function BigVideoCard({ item, isVisible, onPress }: Props) { )} - {/* Live badge */} - {isLive && ( - - - 直播中 - - )} - - + - {formatCount(isLive ? (item.online ?? 0) : (item.stat?.view ?? 0))} + {formatCount(item.stat?.view ?? 0)} - {/* Duration badge on thumbnail (video only) */} - {!isLive && ( - - - {formatDuration(item.duration)} - - - )} + + + {formatDuration(item.duration)} + + {/* Mute toggle — visible only when video is playing */} {videoUrl && !paused && ( @@ -272,8 +255,8 @@ export function BigVideoCard({ item, isVisible, onPress }: Props) { )} - {/* Progress bar between video and info (video only) */} - {!isLive && videoUrl && duration > 0 && ( + {/* Progress bar between video and info */} + {videoUrl && duration > 0 && ( ([]); + const [liveRooms, setLiveRooms] = useState([]); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); @@ -17,8 +18,19 @@ export function useVideoList() { const idx = reset ? 0 : freshIdxRef.current; setLoading(true); try { - const data = await getRecommendFeed(idx); + const promises: [Promise, Promise] = [ + getRecommendFeed(idx), + reset || idx === 0 + ? getLiveList(1, 0).catch(() => [] as LiveRoom[]) + : Promise.resolve([] as LiveRoom[]), + ]; + const [data, live] = await Promise.all(promises); setPages(prev => reset ? [data] : [...prev, data]); + if (reset || idx === 0) { + // Take top 2 by online count + const sorted = [...live].sort((a, b) => b.online - a.online).slice(0, 2); + setLiveRooms(sorted); + } freshIdxRef.current = idx + 1; } catch (e) { console.error('Failed to load videos', e); @@ -36,5 +48,5 @@ export function useVideoList() { const videos = useMemo(() => pages.flat(), [pages]); - return { videos, pages, loading, refreshing, load, refresh }; + return { videos, pages, liveRooms, loading, refreshing, load, refresh }; } diff --git a/services/bilibili.ts b/services/bilibili.ts index d7faaef..e3738c3 100644 --- a/services/bilibili.ts +++ b/services/bilibili.ts @@ -83,39 +83,13 @@ export async function getRecommendFeed(freshIdx = 0): Promise { const res = await api.get('/x/web-interface/wbi/index/top/feed/rcmd', { params: signed }); const items: any[] = res.data.data?.item ?? []; return items - .filter(item => - (item.goto === 'av' && item.bvid && item.title) || - (item.goto === 'live' && item.title), - ) - .map(item => { - if (item.goto === 'live') { - const roomid = item.roomid ?? item.room_id ?? 0; - return { - bvid: `live-${roomid}`, - aid: 0, - title: item.title, - pic: item.pic ?? item.cover ?? '', - owner: item.owner ?? { - mid: item.owner_info?.mid ?? item.uid ?? 0, - name: item.owner_info?.name ?? item.uname ?? '', - face: item.owner_info?.face ?? item.face ?? '', - }, - duration: 0, - desc: '', - stat: null, - goto: 'live' as const, - roomid, - online: item.watched_show?.num ?? item.online ?? 0, - area_name: item.area_name ?? '', - } as VideoItem; - } - return { - ...item, - aid: item.id ?? item.aid, - pic: item.pic ?? item.cover, - owner: item.owner ?? { mid: 0, name: item.owner_info?.name ?? '', face: item.owner_info?.face ?? '' }, - } as VideoItem; - }); + .filter(item => item.goto === 'av' && item.bvid && item.title) + .map(item => ({ + ...item, + aid: item.id ?? item.aid, + pic: item.pic ?? item.cover, + owner: item.owner ?? { mid: 0, name: item.owner_info?.name ?? '', face: item.owner_info?.face ?? '' }, + } as VideoItem)); } export async function getPopularVideos(pn = 1): Promise { diff --git a/utils/videoRows.ts b/utils/videoRows.ts index 0f9104d..962b4c3 100644 --- a/utils/videoRows.ts +++ b/utils/videoRows.ts @@ -1,4 +1,4 @@ -import type { VideoItem } from '../services/types'; +import type { VideoItem, LiveRoom } from '../services/types'; export interface NormalRow { type: 'pair'; @@ -11,29 +11,52 @@ export interface BigRow { item: VideoItem; } -export type ListRow = NormalRow | BigRow; +export interface LiveRow { + type: 'live'; + left: LiveRoom; + right?: LiveRoom; +} -export function toListRows(pages: VideoItem[][]): ListRow[] { +export type ListRow = NormalRow | BigRow | LiveRow; + +export function toListRows(pages: VideoItem[][], liveRooms?: LiveRoom[]): ListRow[] { const rows: ListRow[] = []; + let liveInjected = false; + for (const chunk of pages) { if (chunk.length === 0) continue; - // Prioritize: first live item becomes BigRow - let bigIdx = chunk.findIndex(item => item.goto === 'live'); - if (bigIdx === -1) { - // Fallback: highest view count - bigIdx = 0; - let maxView = chunk[0].stat?.view ?? 0; - for (let i = 1; i < chunk.length; i++) { - const v = chunk[i].stat?.view ?? 0; - if (v > maxView) { maxView = v; bigIdx = i; } - } + + // Highest view count becomes BigRow + let bigIdx = 0; + let maxView = chunk[0].stat?.view ?? 0; + for (let i = 1; i < chunk.length; i++) { + const v = chunk[i].stat?.view ?? 0; + if (v > maxView) { maxView = v; bigIdx = i; } } + const bigItem = chunk[bigIdx]; const rest = chunk.filter((_, i) => i !== bigIdx); - for (let i = 0; i < rest.length; i += 2) { - rows.push({ type: 'pair', left: rest[i], right: rest[i + 1] ?? null }); - } + rows.push({ type: 'big', item: bigItem }); + + const pairs: NormalRow[] = []; + for (let i = 0; i < rest.length; i += 2) { + pairs.push({ type: 'pair', left: rest[i], right: rest[i + 1] ?? null }); + } + + // Inject LiveRow once, after ~3 pairs in the first chunk + if (!liveInjected && liveRooms && liveRooms.length > 0) { + const insertAt = Math.min(3, pairs.length); + const liveRow: LiveRow = { + type: 'live', + left: liveRooms[0], + right: liveRooms[1], + }; + pairs.splice(insertAt, 0, liveRow as any); + liveInjected = true; + } + + rows.push(...pairs); } return rows; }