mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-08 07:28:37 +08:00
评论
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Image,
|
||||
@@ -22,7 +23,7 @@ import { useVideoStore } from "../../store/videoStore";
|
||||
import { formatCount } from "../../utils/format";
|
||||
import { proxyImageUrl } from "../../utils/imageUrl";
|
||||
|
||||
type Tab = "intro" | "comments";
|
||||
type Tab = "intro" | "comments" | "danmaku";
|
||||
|
||||
export default function VideoDetailScreen() {
|
||||
const { bvid } = useLocalSearchParams<{ bvid: string }>();
|
||||
@@ -35,15 +36,17 @@ export default function VideoDetailScreen() {
|
||||
currentQn,
|
||||
changeQuality,
|
||||
} = useVideoDetail(bvid as string);
|
||||
const [commentSort, setCommentSort] = useState<0 | 2>(2);
|
||||
const flatListHeightRef = useRef(0);
|
||||
const {
|
||||
comments,
|
||||
loading: cmtLoading,
|
||||
hasMore: cmtHasMore,
|
||||
load: loadComments,
|
||||
} = useComments(video?.aid ?? 0);
|
||||
const [tab, setTab] = useState<Tab>("comments");
|
||||
} = useComments(video?.aid ?? 0, commentSort);
|
||||
const [tab, setTab] = useState<Tab>("intro");
|
||||
const [danmakus, setDanmakus] = useState<DanmakuItem[]>([]);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [showDanmakuList, setShowDanmakuList] = useState(true);
|
||||
const { setVideo, clearVideo } = useVideoStore();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,7 +55,7 @@ export default function VideoDetailScreen() {
|
||||
|
||||
useEffect(() => {
|
||||
if (video?.aid) loadComments();
|
||||
}, [video?.aid]);
|
||||
}, [video?.aid, commentSort]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!video?.cid) return;
|
||||
@@ -68,6 +71,7 @@ export default function VideoDetailScreen() {
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safe}>
|
||||
{/* TopBar */}
|
||||
<View style={styles.topBar}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
||||
<Ionicons name="chevron-back" size={24} color="#212121" />
|
||||
@@ -80,6 +84,7 @@ export default function VideoDetailScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Video player — fixed 16:9 */}
|
||||
<VideoPlayer
|
||||
playData={playData}
|
||||
qualities={qualities}
|
||||
@@ -91,18 +96,38 @@ export default function VideoDetailScreen() {
|
||||
danmakus={danmakus}
|
||||
onTimeUpdate={setCurrentTime}
|
||||
/>
|
||||
<DanmakuList
|
||||
danmakus={danmakus}
|
||||
currentTime={currentTime}
|
||||
visible={showDanmakuList}
|
||||
onToggle={() => setShowDanmakuList((v) => !v)}
|
||||
/>
|
||||
|
||||
<ScrollView style={styles.scroll} showsVerticalScrollIndicator={false}>
|
||||
{/* TabBar — sits directly below player, always visible once video loads */}
|
||||
{video && (
|
||||
<View style={styles.tabBar}>
|
||||
<TouchableOpacity style={styles.tabItem} onPress={() => setTab("intro")}>
|
||||
<Text style={[styles.tabLabel, tab === "intro" && styles.tabActive]}>
|
||||
简介
|
||||
</Text>
|
||||
{tab === "intro" && <View style={styles.tabUnderline} />}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.tabItem} onPress={() => setTab("comments")}>
|
||||
<Text style={[styles.tabLabel, tab === "comments" && styles.tabActive]}>
|
||||
评论{video.stat?.reply > 0 ? ` ${formatCount(video.stat.reply)}` : ""}
|
||||
</Text>
|
||||
{tab === "comments" && <View style={styles.tabUnderline} />}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.tabItem} onPress={() => setTab("danmaku")}>
|
||||
<Text style={[styles.tabLabel, tab === "danmaku" && styles.tabActive]}>
|
||||
弹幕{danmakus.length > 0 ? ` ${formatCount(danmakus.length)}` : ""}
|
||||
</Text>
|
||||
{tab === "danmaku" && <View style={styles.tabUnderline} />}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Tab content */}
|
||||
{videoLoading ? (
|
||||
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
||||
) : video ? (
|
||||
<>
|
||||
tab === "intro" ? (
|
||||
// 简介:视频信息 + 合集 + 简介文本
|
||||
<ScrollView style={styles.tabScroll} showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.titleSection}>
|
||||
<Text style={styles.title}>{video.title}</Text>
|
||||
<View style={styles.statsRow}>
|
||||
@@ -112,7 +137,6 @@ export default function VideoDetailScreen() {
|
||||
<StatBadge icon="chatbubble" count={video.stat.reply} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.upRow}>
|
||||
<Image
|
||||
source={{ uri: proxyImageUrl(video.owner.face) }}
|
||||
@@ -123,64 +147,70 @@ export default function VideoDetailScreen() {
|
||||
<Text style={styles.followTxt}>+ 关注</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.tabBar}>
|
||||
<TouchableOpacity
|
||||
style={styles.tabItem}
|
||||
onPress={() => setTab("intro")}
|
||||
>
|
||||
<Text
|
||||
style={[styles.tabLabel, tab === "intro" && styles.tabActive]}
|
||||
>
|
||||
简介
|
||||
</Text>
|
||||
{tab === "intro" && <View style={styles.tabUnderline} />}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.tabItem}
|
||||
onPress={() => setTab("comments")}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.tabLabel,
|
||||
tab === "comments" && styles.tabActive,
|
||||
]}
|
||||
>
|
||||
评论{" "}
|
||||
{video.stat.reply > 0 ? formatCount(video.stat.reply) : ""}
|
||||
</Text>
|
||||
{tab === "comments" && <View style={styles.tabUnderline} />}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{tab === "intro" ? (
|
||||
{video.ugc_season && (
|
||||
<SeasonSection
|
||||
season={video.ugc_season}
|
||||
currentBvid={bvid as string}
|
||||
onEpisodePress={(epBvid) => router.replace(`/video/${epBvid}`)}
|
||||
/>
|
||||
)}
|
||||
<View style={styles.descBox}>
|
||||
<Text style={styles.descText}>{video.desc || "暂无简介"}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{comments.map((c) => (
|
||||
<CommentItem key={c.rpid} item={c} />
|
||||
))}
|
||||
{cmtLoading && (
|
||||
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
||||
)}
|
||||
{!cmtLoading && comments.length > 0 && (
|
||||
<TouchableOpacity
|
||||
style={styles.loadMore}
|
||||
onPress={loadComments}
|
||||
>
|
||||
<Text style={styles.loadMoreTxt}>加载更多评论</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{!cmtLoading && comments.length === 0 && !videoLoading && (
|
||||
<Text style={styles.emptyTxt}>暂无评论</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
) : tab === "danmaku" ? (
|
||||
<DanmakuList
|
||||
danmakus={danmakus}
|
||||
currentTime={currentTime}
|
||||
visible={true}
|
||||
onToggle={() => {}}
|
||||
style={styles.danmakuTab}
|
||||
/>
|
||||
) : (
|
||||
<FlatList
|
||||
style={styles.tabScroll}
|
||||
data={comments}
|
||||
keyExtractor={(c) => String(c.rpid)}
|
||||
renderItem={({ item }) => <CommentItem item={item} />}
|
||||
onEndReached={() => { if (cmtHasMore && !cmtLoading) loadComments(); }}
|
||||
onEndReachedThreshold={0.3}
|
||||
onLayout={({ nativeEvent }) => { flatListHeightRef.current = nativeEvent.layout.height; }}
|
||||
onContentSizeChange={(_, contentHeight) => {
|
||||
if (contentHeight < flatListHeightRef.current && cmtHasMore && !cmtLoading) {
|
||||
loadComments();
|
||||
}
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListHeaderComponent={
|
||||
<View style={styles.sortRow}>
|
||||
<Text style={styles.sortLabel}>排序</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.sortBtn, commentSort === 2 && styles.sortBtnActive]}
|
||||
onPress={() => setCommentSort(2)}
|
||||
>
|
||||
<Text style={[styles.sortBtnTxt, commentSort === 2 && styles.sortBtnTxtActive]}>热门</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.sortBtn, commentSort === 0 && styles.sortBtnActive]}
|
||||
onPress={() => setCommentSort(0)}
|
||||
>
|
||||
<Text style={[styles.sortBtnTxt, commentSort === 0 && styles.sortBtnTxtActive]}>最新</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={
|
||||
cmtLoading ? (
|
||||
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
||||
) : !cmtHasMore && comments.length > 0 ? (
|
||||
<Text style={styles.emptyTxt}>已加载全部评论</Text>
|
||||
) : null
|
||||
}
|
||||
ListEmptyComponent={
|
||||
!cmtLoading ? <Text style={styles.emptyTxt}>暂无评论</Text> : null
|
||||
}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -194,6 +224,78 @@ function StatBadge({ icon, count }: { icon: string; count: number }) {
|
||||
);
|
||||
}
|
||||
|
||||
function SeasonSection({
|
||||
season,
|
||||
currentBvid,
|
||||
onEpisodePress,
|
||||
}: {
|
||||
season: NonNullable<import("../../services/types").VideoItem["ugc_season"]>;
|
||||
currentBvid: string;
|
||||
onEpisodePress: (bvid: string) => void;
|
||||
}) {
|
||||
const episodes = season.sections?.[0]?.episodes ?? [];
|
||||
const currentIndex = episodes.findIndex((ep) => ep.bvid === currentBvid);
|
||||
const listRef = useRef<FlatList>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentIndex <= 0 || episodes.length === 0) return;
|
||||
// 等布局完成再滚动
|
||||
listRef.current?.scrollToIndex({
|
||||
index: currentIndex,
|
||||
viewPosition: 0.5, // 居中
|
||||
animated: false,
|
||||
});
|
||||
}, [currentIndex, episodes.length]);
|
||||
|
||||
return (
|
||||
<View style={styles.seasonBox}>
|
||||
<View style={styles.seasonHeader}>
|
||||
<Text style={styles.seasonTitle}>合集 · {season.title}</Text>
|
||||
<Text style={styles.seasonCount}>{season.ep_count}个视频</Text>
|
||||
<Ionicons name="chevron-forward" size={14} color="#999" />
|
||||
</View>
|
||||
<FlatList
|
||||
ref={listRef}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
data={episodes}
|
||||
keyExtractor={(ep) => ep.bvid}
|
||||
contentContainerStyle={{ paddingHorizontal: 12, gap: 10 }}
|
||||
// 每个卡片宽 120,gap 10,让 FlatList 直接算任意索引的偏移量
|
||||
getItemLayout={(_data, index) => ({
|
||||
length: 130,
|
||||
offset: 12 + index * 130,
|
||||
index,
|
||||
})}
|
||||
onScrollToIndexFailed={() => {}}
|
||||
renderItem={({ item: ep, index }) => {
|
||||
const isCurrent = ep.bvid === currentBvid;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.epCard, isCurrent && styles.epCardActive]}
|
||||
onPress={() => !isCurrent && onEpisodePress(ep.bvid)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{ep.arc?.pic && (
|
||||
<Image
|
||||
source={{ uri: proxyImageUrl(ep.arc.pic) }}
|
||||
style={styles.epThumb}
|
||||
/>
|
||||
)}
|
||||
<Text style={[styles.epNum, isCurrent && styles.epNumActive]}>
|
||||
第{index + 1}集
|
||||
</Text>
|
||||
<Text style={styles.epTitle} numberOfLines={2}>
|
||||
{ep.title}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1, backgroundColor: "#fff" },
|
||||
topBar: {
|
||||
@@ -213,7 +315,6 @@ const styles = StyleSheet.create({
|
||||
color: "#212121",
|
||||
},
|
||||
miniBtn: { padding: 4 },
|
||||
scroll: { flex: 1 },
|
||||
loader: { marginVertical: 30 },
|
||||
titleSection: { padding: 14 },
|
||||
title: {
|
||||
@@ -243,6 +344,39 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 14,
|
||||
},
|
||||
followTxt: { color: "#fff", fontSize: 12, fontWeight: "600" },
|
||||
seasonBox: {
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: "#f0f0f0",
|
||||
paddingVertical: 10,
|
||||
},
|
||||
seasonHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 14,
|
||||
paddingBottom: 8,
|
||||
gap: 4,
|
||||
},
|
||||
seasonTitle: { flex: 1, fontSize: 13, fontWeight: "600", color: "#212121" },
|
||||
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" },
|
||||
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,
|
||||
},
|
||||
tabBar: {
|
||||
flexDirection: "row",
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
@@ -264,9 +398,29 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: "#00AEEC",
|
||||
borderRadius: 1,
|
||||
},
|
||||
tabScroll: { flex: 1 },
|
||||
descBox: { padding: 16 },
|
||||
descText: { fontSize: 14, color: "#555", lineHeight: 22 },
|
||||
loadMore: { alignItems: "center", padding: 16 },
|
||||
loadMoreTxt: { color: "#00AEEC", fontSize: 13 },
|
||||
danmakuTab: { flex: 1 },
|
||||
emptyTxt: { textAlign: "center", color: "#bbb", padding: 30 },
|
||||
sortRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
gap: 8,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: "#f0f0f0",
|
||||
},
|
||||
sortLabel: { fontSize: 13, color: "#999", marginRight: 4 },
|
||||
sortBtn: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: "#e0e0e0",
|
||||
},
|
||||
sortBtnActive: { borderColor: "#00AEEC", backgroundColor: "#e8f7fd" },
|
||||
sortBtnTxt: { fontSize: 12, color: "#666" },
|
||||
sortBtnTxtActive: { color: "#00AEEC", fontWeight: "600" as const },
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ interface Props {
|
||||
currentTime: number;
|
||||
visible: boolean;
|
||||
onToggle: () => void;
|
||||
style?: object;
|
||||
}
|
||||
|
||||
interface DisplayedDanmaku extends DanmakuItem {
|
||||
@@ -37,7 +38,7 @@ function formatTimestamp(seconds: number): string {
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export default function DanmakuList({ danmakus, currentTime, visible, onToggle }: Props) {
|
||||
export default function DanmakuList({ danmakus, currentTime, visible, onToggle, style }: Props) {
|
||||
const flatListRef = useRef<FlatList>(null);
|
||||
const [displayedItems, setDisplayedItems] = useState<DisplayedDanmaku[]>([]);
|
||||
const [unseenCount, setUnseenCount] = useState(0);
|
||||
@@ -179,7 +180,7 @@ export default function DanmakuList({ danmakus, currentTime, visible, onToggle }
|
||||
const keyExtractor = useCallback((item: DisplayedDanmaku) => String(item._key), []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[styles.container, style]}>
|
||||
<TouchableOpacity style={styles.header} onPress={onToggle} activeOpacity={0.7}>
|
||||
<Ionicons
|
||||
name={visible ? 'chatbubbles' : 'chatbubbles-outline'}
|
||||
@@ -250,7 +251,7 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '500',
|
||||
},
|
||||
listWrapper: {
|
||||
height: 200,
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
},
|
||||
list: {
|
||||
|
||||
@@ -40,6 +40,17 @@ function clamp(v: number, lo: number, hi: number) {
|
||||
return Math.max(lo, Math.min(hi, v));
|
||||
}
|
||||
|
||||
// index[i] = timestamp (seconds) for frame i. Returns frame number (position i), not index value.
|
||||
function findFrameByTime(index: number[], seekTime: number): number {
|
||||
let lo = 0, hi = index.length - 1;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi + 1) >> 1;
|
||||
if (index[mid] <= seekTime) lo = mid;
|
||||
else hi = mid - 1;
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
|
||||
|
||||
interface Props {
|
||||
playData: PlayUrlResponse | null;
|
||||
@@ -129,7 +140,7 @@ export function NativeVideoPlayer({
|
||||
if (cancelled) return;
|
||||
if (shotData?.image?.length) {
|
||||
setShots(shotData);
|
||||
console.log(shotData.index,'缩略图长度')
|
||||
console.log(shotData.index,shotData.image,'缩略图长度')
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
@@ -263,14 +274,17 @@ export function NativeVideoPlayer({
|
||||
const framesPerSheet = img_x_len * img_y_len;
|
||||
const totalFrames = framesPerSheet * image.length;
|
||||
const seekTime = touchRatio * duration;
|
||||
// index[i] = timestamp of frame i (seconds). Binary search returns position i (= frame number).
|
||||
// Cap to index.length-1 to avoid black padding frames beyond valid content.
|
||||
const frameIdx = index?.length && duration > 0
|
||||
? clamp(index[clamp(Math.floor(seekTime / duration * index.length), 0, index.length - 1)], 0, totalFrames - 1)
|
||||
? clamp(findFrameByTime(index, seekTime), 0, index.length - 1)
|
||||
: clamp(Math.floor(touchRatio * (totalFrames - 1)), 0, totalFrames - 1);
|
||||
|
||||
const sheetIdx = Math.floor(frameIdx / framesPerSheet);
|
||||
const local = frameIdx % framesPerSheet;
|
||||
const col = local % img_x_len;
|
||||
const row = Math.floor(local / img_x_len);
|
||||
|
||||
console.log('[thumb]', { seekTime, duration, indexLen: index?.length, frameIdx, totalFrames, sheetIdx, col, row });
|
||||
|
||||
// Scale sprite frame to display size
|
||||
|
||||
@@ -1,30 +1,49 @@
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { getComments } from '../services/bilibili';
|
||||
import type { Comment } from '../services/types';
|
||||
|
||||
export function useComments(aid: number) {
|
||||
export function useComments(aid: number, sort: number) {
|
||||
const [comments, setComments] = useState<Comment[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const pageRef = useRef(1);
|
||||
|
||||
const loadingRef = useRef(false);
|
||||
const hasMoreRef = useRef(true);
|
||||
const sortRef = useRef(sort);
|
||||
const aidRef = useRef(aid);
|
||||
const cursorRef = useRef(''); // empty = first page
|
||||
|
||||
aidRef.current = aid;
|
||||
|
||||
useEffect(() => {
|
||||
if (sortRef.current === sort) return;
|
||||
sortRef.current = sort;
|
||||
cursorRef.current = '';
|
||||
hasMoreRef.current = true;
|
||||
setComments([]);
|
||||
setHasMore(true);
|
||||
}, [sort]);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (loadingRef.current || !hasMore || !aid) return;
|
||||
if (loadingRef.current || !hasMoreRef.current || !aidRef.current) return;
|
||||
loadingRef.current = true;
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await getComments(aid, pageRef.current);
|
||||
if (data.length === 0) { setHasMore(false); return; }
|
||||
setComments(prev => [...prev, ...data]);
|
||||
pageRef.current += 1;
|
||||
const isFirstPage = cursorRef.current === '';
|
||||
const { replies, nextCursor, isEnd } = await getComments(aidRef.current, cursorRef.current, sortRef.current);
|
||||
cursorRef.current = nextCursor;
|
||||
setComments(prev => isFirstPage ? replies : [...prev, ...replies]);
|
||||
if (isEnd || replies.length === 0) {
|
||||
hasMoreRef.current = false;
|
||||
setHasMore(false);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load comments', e);
|
||||
} finally {
|
||||
loadingRef.current = false;
|
||||
setLoading(false);
|
||||
}
|
||||
}, [aid, hasMore]);
|
||||
}, []);
|
||||
|
||||
return { comments, loading, hasMore, load };
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ export async function getRecommendFeed(freshIdx = 0): Promise<VideoItem[]> {
|
||||
);
|
||||
const res = await api.get('/x/web-interface/wbi/index/top/feed/rcmd', { params: signed });
|
||||
const items: any[] = res.data.data?.item ?? [];
|
||||
console.log(items,'items')
|
||||
return items
|
||||
.filter(item => item.goto === 'av' && item.bvid && item.title)
|
||||
.map(item => ({
|
||||
@@ -129,11 +130,26 @@ export async function getUserInfo(): Promise<{ face: string; uname: string; mid:
|
||||
return { face: face ?? '', uname: uname ?? '', mid: mid ?? 0 };
|
||||
}
|
||||
|
||||
export async function getComments(aid: number, pn = 1): Promise<Comment[]> {
|
||||
const res = await api.get('/x/v2/reply', {
|
||||
params: { oid: aid, type: 1, pn, ps: 20, sort: 2 },
|
||||
export async function getComments(
|
||||
aid: number,
|
||||
cursor = '',
|
||||
sort = 2,
|
||||
): Promise<{ replies: Comment[]; nextCursor: string; isEnd: boolean }> {
|
||||
const mode = sort === 2 ? 3 : 2; // 3=hot, 2=time
|
||||
const res = await api.get('/x/v2/reply/main', {
|
||||
params: {
|
||||
oid: aid,
|
||||
type: 1,
|
||||
mode,
|
||||
plat: 1,
|
||||
pagination_str: JSON.stringify({ offset: cursor }),
|
||||
},
|
||||
});
|
||||
return (res.data.data?.replies ?? []) as Comment[];
|
||||
const data = res.data.data;
|
||||
const replies = (data?.replies ?? []) as Comment[];
|
||||
const nextCursor: string = data?.cursor?.next ?? '';
|
||||
const isEnd: boolean = data?.cursor?.is_end ?? true;
|
||||
return { replies, nextCursor, isEnd };
|
||||
}
|
||||
|
||||
export async function getVideoShot(bvid: string, cid: number): Promise<VideoShotData | null> {
|
||||
|
||||
@@ -24,6 +24,21 @@ export interface VideoItem {
|
||||
roomid?: number;
|
||||
online?: number;
|
||||
area_name?: string;
|
||||
ugc_season?: {
|
||||
id: number;
|
||||
title: string;
|
||||
cover: string;
|
||||
ep_count: number;
|
||||
sections: Array<{
|
||||
episodes: Array<{
|
||||
aid: number;
|
||||
bvid: string;
|
||||
cid: number;
|
||||
title: string;
|
||||
arc?: { pic: string; stat?: { view: number } };
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
|
||||
@@ -53,8 +53,8 @@ export function toListRows(pages: VideoItem[][], liveRooms?: LiveRoom[]): ListRo
|
||||
roomIdx++;
|
||||
}
|
||||
|
||||
rows.push(...pairs);
|
||||
rows.push({ type: 'big', item: bigItem });
|
||||
rows.push(...pairs);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user