mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-18 18:23:43 +08:00
feat: Implement language filtering and display language information across video components.
This commit is contained in:
@@ -2,9 +2,11 @@
|
||||
import { ResultsHeader } from '@/components/search/ResultsHeader';
|
||||
import { SourceBadges } from '@/components/search/SourceBadges';
|
||||
import { TypeBadges } from '@/components/search/TypeBadges';
|
||||
import { LanguageBadges } from '@/components/search/LanguageBadges';
|
||||
import { VideoGrid } from '@/components/search/VideoGrid';
|
||||
import { useSourceBadges } from '@/lib/hooks/useSourceBadges';
|
||||
import { useTypeBadges } from '@/lib/hooks/useTypeBadges';
|
||||
import { useLanguageBadges } from '@/lib/hooks/useLanguageBadges';
|
||||
import { Video, SourceBadge } from '@/lib/types';
|
||||
|
||||
interface SearchResultsProps {
|
||||
@@ -34,10 +36,19 @@ export function SearchResults({
|
||||
const {
|
||||
typeBadges,
|
||||
selectedTypes,
|
||||
filteredVideos: finalFilteredVideos,
|
||||
filteredVideos: typeFilteredVideos,
|
||||
toggleType,
|
||||
} = useTypeBadges(sourceFilteredVideos);
|
||||
|
||||
// Language badges hook - auto-collects and filters by vod_lang
|
||||
// Apply on type-filtered results for combined filtering
|
||||
const {
|
||||
languageBadges,
|
||||
selectedLangs,
|
||||
filteredVideos: finalFilteredVideos,
|
||||
toggleLang,
|
||||
} = useLanguageBadges(typeFilteredVideos);
|
||||
|
||||
if (results.length === 0 && !loading) return null;
|
||||
|
||||
return (
|
||||
@@ -68,7 +79,17 @@ export function SearchResults({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Display filtered videos (both source and type filters applied) */}
|
||||
{/* Language Badges - Auto-collected from search results */}
|
||||
{languageBadges.length > 0 && (
|
||||
<LanguageBadges
|
||||
badges={languageBadges}
|
||||
selectedLangs={selectedLangs}
|
||||
onToggleLang={toggleLang}
|
||||
className="mb-6"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Display filtered videos (source, type, and language filters applied) */}
|
||||
<VideoGrid
|
||||
videos={finalFilteredVideos}
|
||||
isPremium={isPremium}
|
||||
|
||||
@@ -48,6 +48,12 @@ export function VideoMetadata({ videoData, source, title }: VideoMetadataProps)
|
||||
{videoData.vod_area}
|
||||
</Badge>
|
||||
)}
|
||||
{videoData?.vod_lang && (
|
||||
<Badge variant="secondary">
|
||||
<Icons.Languages size={14} className="mr-1" />
|
||||
{videoData.vod_lang}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{videoData?.vod_content && (
|
||||
<p className="text-sm sm:text-base text-[var(--text-secondary)]">
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* LanguageBadges - Language filter badge component
|
||||
* Mirrors TypeBadges pattern, reuses TypeBadgeItem for individual badges
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Icons } from '@/components/ui/Icon';
|
||||
import { TypeBadgeItem } from './TypeBadgeItem';
|
||||
import type { LanguageBadge } from '@/lib/types';
|
||||
|
||||
interface LanguageBadgesProps {
|
||||
badges: LanguageBadge[];
|
||||
selectedLangs: Set<string>;
|
||||
onToggleLang: (lang: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const LanguageBadges = memo(function LanguageBadges({
|
||||
badges,
|
||||
selectedLangs,
|
||||
onToggleLang,
|
||||
className = ''
|
||||
}: LanguageBadgesProps) {
|
||||
if (badges.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleClearAll = () => {
|
||||
selectedLangs.forEach(lang => onToggleLang(lang));
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
hover={false}
|
||||
className={`p-4 animate-fade-in bg-[var(--bg-color)]/50 backdrop-blur-none saturate-100 shadow-sm border-[var(--glass-border)] ${className}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-center gap-2 shrink-0 pt-1">
|
||||
<Icons.Languages size={16} className="text-[var(--accent-color)]" />
|
||||
<span className="text-sm font-semibold text-[var(--text-color)]">
|
||||
语言标签 ({badges.length}):
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Desktop: flex wrap, Mobile: horizontal scroll */}
|
||||
<div className="hidden md:flex md:flex-1 -mx-1 px-1">
|
||||
<div className="flex items-center gap-2 flex-wrap p-1">
|
||||
{badges.map((badge) => (
|
||||
<TypeBadgeItem
|
||||
key={badge.lang}
|
||||
type={badge.lang}
|
||||
count={badge.count}
|
||||
isSelected={selectedLangs.has(badge.lang)}
|
||||
onToggle={() => onToggleLang(badge.lang)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex md:hidden flex-1 -mx-1 px-1 overflow-hidden">
|
||||
<div className="flex items-center gap-2 overflow-x-auto pb-2 scrollbar-hide snap-x snap-mandatory">
|
||||
{badges.map((badge) => (
|
||||
<TypeBadgeItem
|
||||
key={badge.lang}
|
||||
type={badge.lang}
|
||||
count={badge.count}
|
||||
isSelected={selectedLangs.has(badge.lang)}
|
||||
onToggle={() => onToggleLang(badge.lang)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedLangs.size > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-[var(--glass-border)]">
|
||||
<button
|
||||
onClick={handleClearAll}
|
||||
className="text-xs text-[var(--text-color-secondary)] hover:text-[var(--accent-color)]
|
||||
flex items-center gap-1 transition-colors"
|
||||
>
|
||||
<Icons.X size={12} />
|
||||
清除筛选 ({selectedLangs.size})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
@@ -168,6 +168,11 @@ export const VideoCard = memo<VideoCardProps>(({
|
||||
{video.vod_remarks}
|
||||
</p>
|
||||
)}
|
||||
{video.vod_lang && (
|
||||
<p className="text-xs text-[var(--text-color-secondary)] mt-1">
|
||||
{video.vod_lang}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
@@ -202,6 +202,11 @@ export const VideoGroupCard = memo<VideoGroupCardProps>(({
|
||||
{displayQuality}
|
||||
</p>
|
||||
)}
|
||||
{representative.vod_lang && (
|
||||
<p className="text-xs text-[var(--text-color-secondary)] mt-1">
|
||||
{representative.vod_lang}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
@@ -172,6 +172,17 @@ export const UtilityIcons = {
|
||||
<circle cx="5" cy="12" r="1" />
|
||||
</svg>
|
||||
),
|
||||
Languages: ({ className = "", size = 24 }: IconProps) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
||||
<path d="M5 8l6 6" />
|
||||
<path d="M4 14l6-6 2-3" />
|
||||
<path d="M2 5h12" />
|
||||
<path d="M7 2h1" />
|
||||
<path d="M22 22l-5-10-5 10" />
|
||||
<path d="M14 18h6" />
|
||||
</svg>
|
||||
),
|
||||
|
||||
Settings: ({ className = "", size = 24 }: IconProps) => (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
|
||||
Reference in New Issue
Block a user