mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
feat: add resolution probing support to VideoCard, VideoGrid, and VideoGroupCard components
This commit is contained in:
@@ -10,7 +10,8 @@ import { LatencyBadge } from '@/components/ui/LatencyBadge';
|
||||
import { FavoriteButton } from '@/components/favorites/FavoriteButton';
|
||||
|
||||
import { Video } from '@/lib/types';
|
||||
import { parseVideoTitle, extractQualityLabel } from '@/lib/utils/video';
|
||||
import { parseVideoTitle } from '@/lib/utils/video';
|
||||
import type { ResolutionInfo } from '@/lib/hooks/useResolutionProbe';
|
||||
|
||||
interface VideoCardProps {
|
||||
video: Video;
|
||||
@@ -20,6 +21,8 @@ interface VideoCardProps {
|
||||
onCardClick: (e: React.MouseEvent, cardId: string, videoUrl: string) => void;
|
||||
isPremium?: boolean;
|
||||
latencies?: Record<string, number>;
|
||||
resolution?: ResolutionInfo | null;
|
||||
isProbing?: boolean;
|
||||
}
|
||||
|
||||
export const VideoCard = memo<VideoCardProps>(({
|
||||
@@ -29,7 +32,9 @@ export const VideoCard = memo<VideoCardProps>(({
|
||||
isActive,
|
||||
onCardClick,
|
||||
isPremium = false,
|
||||
latencies = {}
|
||||
latencies = {},
|
||||
resolution,
|
||||
isProbing = false,
|
||||
}) => {
|
||||
const displayLatency = latencies[video.source] ?? video.latency;
|
||||
return (
|
||||
@@ -157,10 +162,7 @@ export const VideoCard = memo<VideoCardProps>(({
|
||||
{/* Info */}
|
||||
<div className="p-3 flex-1 flex flex-col">
|
||||
{(() => {
|
||||
const { cleanTitle, quality } = parseVideoTitle(video.vod_name);
|
||||
// Visual priority: Quality from title tag, then vod_remarks
|
||||
const displayQuality = quality || video.vod_remarks;
|
||||
const qualityBadge = extractQualityLabel(video.vod_remarks, quality);
|
||||
const { cleanTitle } = parseVideoTitle(video.vod_name);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -168,23 +170,16 @@ export const VideoCard = memo<VideoCardProps>(({
|
||||
{cleanTitle}
|
||||
</h4>
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{qualityBadge && (
|
||||
<span className={`inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold text-white ${qualityBadge.color}`}>
|
||||
{qualityBadge.label}
|
||||
{resolution ? (
|
||||
<span className={`inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold text-white ${resolution.color}`}>
|
||||
{resolution.label}
|
||||
</span>
|
||||
)}
|
||||
{displayQuality && (
|
||||
<p className="text-xs text-[var(--text-color-secondary)] font-medium truncate">
|
||||
{displayQuality}
|
||||
</p>
|
||||
)}
|
||||
) : isProbing ? (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold text-white/50 bg-gray-500/50 animate-pulse">
|
||||
...
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{/* Hide remarks if it was used as quality to avoid duplication */}
|
||||
{video.vod_remarks && video.vod_remarks !== displayQuality && (
|
||||
<p className="text-xs text-[var(--text-color-secondary)] mt-1 line-clamp-1">
|
||||
{video.vod_remarks}
|
||||
</p>
|
||||
)}
|
||||
{video.vod_lang && (
|
||||
<p className="text-xs text-[var(--text-color-secondary)] mt-1">
|
||||
{video.vod_lang}
|
||||
|
||||
@@ -74,6 +74,15 @@ export const VideoGrid = memo(function VideoGrid({
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build stable list of videos to probe for resolution
|
||||
const videosToProbe = useMemo(() => {
|
||||
if (displayMode === 'grouped') {
|
||||
// For grouped mode, will probe after grouping
|
||||
return [];
|
||||
}
|
||||
return videos.map(v => ({ id: String(v.vod_id), source: v.source }));
|
||||
}, [videos, displayMode]);
|
||||
|
||||
// Group videos by name when in grouped mode
|
||||
const groupedVideos = useMemo<GroupedVideo[]>(() => {
|
||||
if (displayMode !== 'grouped') return [];
|
||||
@@ -166,6 +175,16 @@ export const VideoGrid = memo(function VideoGrid({
|
||||
}));
|
||||
}, [groupedVideos, displayMode]);
|
||||
|
||||
// Build probe list for grouped mode (probe representative of each group)
|
||||
const groupedProbeList = useMemo(() => {
|
||||
if (displayMode !== 'grouped') return [];
|
||||
return groupedVideos.map(g => ({ id: String(g.representative.vod_id), source: g.representative.source }));
|
||||
}, [groupedVideos, displayMode]);
|
||||
|
||||
// Probe resolutions
|
||||
const probeList = displayMode === 'grouped' ? groupedProbeList : videosToProbe;
|
||||
const { resolutions, isProbing } = useResolutionProbe(probeList);
|
||||
|
||||
const totalItems = displayMode === 'grouped' ? groupItems.length : videoItems.length;
|
||||
|
||||
return (
|
||||
@@ -189,6 +208,8 @@ export const VideoGrid = memo(function VideoGrid({
|
||||
onCardClick={handleCardClick}
|
||||
isPremium={isPremium}
|
||||
latencies={latencies}
|
||||
resolution={resolutions[`${group.representative.source}:${group.representative.vod_id}`]}
|
||||
isProbing={isProbing && !resolutions[`${group.representative.source}:${group.representative.vod_id}`]}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@@ -206,6 +227,8 @@ export const VideoGrid = memo(function VideoGrid({
|
||||
onCardClick={handleCardClick}
|
||||
isPremium={isPremium}
|
||||
latencies={latencies}
|
||||
resolution={resolutions[`${video.source}:${video.vod_id}`]}
|
||||
isProbing={isProbing && !resolutions[`${video.source}:${video.vod_id}`]}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ import { FavoriteButton } from '@/components/favorites/FavoriteButton';
|
||||
import { Video } from '@/lib/types';
|
||||
import { parseVideoTitle } from '@/lib/utils/video';
|
||||
import { storeGroupedSources } from '@/lib/utils/grouped-sources-cache';
|
||||
import type { ResolutionInfo } from '@/lib/hooks/useResolutionProbe';
|
||||
|
||||
export interface GroupedVideo {
|
||||
/** Representative video (lowest latency) */
|
||||
@@ -33,6 +34,8 @@ interface VideoGroupCardProps {
|
||||
onCardClick: (e: React.MouseEvent, cardId: string, videoUrl: string) => void;
|
||||
isPremium?: boolean;
|
||||
latencies?: Record<string, number>;
|
||||
resolution?: ResolutionInfo | null;
|
||||
isProbing?: boolean;
|
||||
}
|
||||
|
||||
export const VideoGroupCard = memo<VideoGroupCardProps>(({
|
||||
@@ -41,7 +44,9 @@ export const VideoGroupCard = memo<VideoGroupCardProps>(({
|
||||
isActive,
|
||||
onCardClick,
|
||||
isPremium = false,
|
||||
latencies = {}
|
||||
latencies = {},
|
||||
resolution,
|
||||
isProbing = false,
|
||||
}) => {
|
||||
const { representative, videos, name } = group;
|
||||
|
||||
@@ -201,19 +206,24 @@ export const VideoGroupCard = memo<VideoGroupCardProps>(({
|
||||
{/* Info */}
|
||||
<div className="p-3 flex-1 flex flex-col">
|
||||
{(() => {
|
||||
const { cleanTitle, quality } = parseVideoTitle(name);
|
||||
const displayQuality = quality || representative.vod_remarks;
|
||||
const { cleanTitle } = parseVideoTitle(name);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4 className="font-semibold text-sm text-[var(--text-color)] line-clamp-2 min-h-[2.5rem] mb-1">
|
||||
{cleanTitle}
|
||||
</h4>
|
||||
{displayQuality && (
|
||||
<p className="text-xs text-[var(--text-color-secondary)] font-medium">
|
||||
{displayQuality}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{resolution ? (
|
||||
<span className={`inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold text-white ${resolution.color}`}>
|
||||
{resolution.label}
|
||||
</span>
|
||||
) : isProbing ? (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold text-white/50 bg-gray-500/50 animate-pulse">
|
||||
...
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{representative.vod_lang && (
|
||||
<p className="text-xs text-[var(--text-color-secondary)] mt-1">
|
||||
{representative.vod_lang}
|
||||
|
||||
Reference in New Issue
Block a user