feat: add resolution probing support to VideoCard, VideoGrid, and VideoGroupCard components

This commit is contained in:
kuekhaoyang
2026-03-12 23:36:30 +08:00
parent b41ead2564
commit b424047c5b
3 changed files with 57 additions and 29 deletions
+16 -21
View File
@@ -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}
+23
View File
@@ -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}`]}
/>
);
})
+18 -8
View File
@@ -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}