mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-05-20 04:47:30 +08:00
feat: favorites in aggreagte page
This commit is contained in:
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { LinkIcon } from 'lucide-react';
|
import { Heart, LinkIcon } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { Suspense, useEffect, useState } from 'react';
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { isFavorited, toggleFavorite } from '@/lib/db.client';
|
||||||
import { SearchResult } from '@/lib/types';
|
import { SearchResult } from '@/lib/types';
|
||||||
|
|
||||||
import PageLayout from '@/components/PageLayout';
|
import PageLayout from '@/components/PageLayout';
|
||||||
@@ -142,6 +143,88 @@ function AggregatePageClient() {
|
|||||||
// 详情映射,便于快速获取每个源的集数
|
// 详情映射,便于快速获取每个源的集数
|
||||||
const sourceDetailMap = new Map(results.map((d) => [d.source, d]));
|
const sourceDetailMap = new Map(results.map((d) => [d.source, d]));
|
||||||
|
|
||||||
|
// 新增:播放源卡片组件,包含收藏逻辑
|
||||||
|
const SourceCard = ({ src }: { src: SearchResult }) => {
|
||||||
|
const d = sourceDetailMap.get(src.source);
|
||||||
|
const epCount = d ? d.episodes.length : src.episodes.length;
|
||||||
|
|
||||||
|
const [favorited, setFavorited] = useState(false);
|
||||||
|
|
||||||
|
// 初次加载检查收藏状态
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const fav = await isFavorited(src.source, src.id);
|
||||||
|
setFavorited(fav);
|
||||||
|
} catch {
|
||||||
|
/* 忽略错误 */
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [src.source, src.id]);
|
||||||
|
|
||||||
|
// 切换收藏状态
|
||||||
|
const handleToggleFavorite = async (
|
||||||
|
e: React.MouseEvent<HTMLSpanElement | SVGElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newState = await toggleFavorite(src.source, src.id, {
|
||||||
|
title: src.title,
|
||||||
|
source_name: src.source_name,
|
||||||
|
year: src.year,
|
||||||
|
cover: src.poster,
|
||||||
|
total_episodes: src.episodes.length,
|
||||||
|
save_time: Date.now(),
|
||||||
|
});
|
||||||
|
setFavorited(newState);
|
||||||
|
} catch {
|
||||||
|
/* 忽略错误 */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={src.source}
|
||||||
|
href={`/play?source=${src.source}&id=${
|
||||||
|
src.id
|
||||||
|
}&title=${encodeURIComponent(src.title)}${
|
||||||
|
src.year ? `&year=${src.year}` : ''
|
||||||
|
}&from=aggregate`}
|
||||||
|
className='group relative flex items-center justify-center w-full h-14 bg-gray-500/80 hover:bg-green-500 dark:bg-gray-700/80 dark:hover:bg-green-600 rounded-lg transition-colors'
|
||||||
|
>
|
||||||
|
{/* 收藏爱心 */}
|
||||||
|
<span
|
||||||
|
onClick={handleToggleFavorite}
|
||||||
|
title={favorited ? '移除收藏' : '加入收藏'}
|
||||||
|
className={`absolute top-[2px] left-1 inline-flex items-center justify-center cursor-pointer transition-opacity duration-200 ${
|
||||||
|
favorited ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Heart
|
||||||
|
className={`w-5 h-5 ${
|
||||||
|
favorited ? 'text-red-500' : 'text-white/90'
|
||||||
|
}`}
|
||||||
|
strokeWidth={2}
|
||||||
|
fill={favorited ? 'currentColor' : 'none'}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* 名称 */}
|
||||||
|
<span className='px-1 text-white text-sm font-medium truncate whitespace-nowrap'>
|
||||||
|
{src.source_name}
|
||||||
|
</span>
|
||||||
|
{/* 集数徽标 */}
|
||||||
|
{epCount && epCount > 1 ? (
|
||||||
|
<span className='absolute top-[2px] right-1 text-[10px] font-semibold text-green-900 bg-green-300/90 rounded-full px-1 pointer-events-none'>
|
||||||
|
{epCount}集
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout activePath='/aggregate'>
|
<PageLayout activePath='/aggregate'>
|
||||||
<div className='flex flex-col min-h-full px-2 sm:px-10 pt-4 sm:pt-8 pb-[calc(3.5rem+env(safe-area-inset-bottom))] overflow-visible'>
|
<div className='flex flex-col min-h-full px-2 sm:px-10 pt-4 sm:pt-8 pb-[calc(3.5rem+env(safe-area-inset-bottom))] overflow-visible'>
|
||||||
@@ -247,32 +330,9 @@ function AggregatePageClient() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid grid-cols-3 gap-2 sm:grid-cols-[repeat(auto-fill,_minmax(6rem,_1fr))] sm:gap-4 justify-start'>
|
<div className='grid grid-cols-3 gap-2 sm:grid-cols-[repeat(auto-fill,_minmax(6rem,_1fr))] sm:gap-4 justify-start'>
|
||||||
{uniqueSources.map((src) => {
|
{uniqueSources.map((src) => (
|
||||||
const d = sourceDetailMap.get(src.source);
|
<SourceCard key={src.source} src={src} />
|
||||||
const epCount = d ? d.episodes.length : src.episodes.length;
|
))}
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={src.source}
|
|
||||||
href={`/play?source=${src.source}&id=${
|
|
||||||
src.id
|
|
||||||
}&title=${encodeURIComponent(src.title)}${
|
|
||||||
src.year ? `&year=${src.year}` : ''
|
|
||||||
}&from=aggregate`}
|
|
||||||
className='relative flex items-center justify-center w-full h-14 bg-gray-500/80 hover:bg-green-500 dark:bg-gray-700/80 dark:hover:bg-green-600 rounded-lg transition-colors'
|
|
||||||
>
|
|
||||||
{/* 名称 */}
|
|
||||||
<span className='px-1 text-white text-sm font-medium truncate whitespace-nowrap'>
|
|
||||||
{src.source_name}
|
|
||||||
</span>
|
|
||||||
{/* 集数徽标 */}
|
|
||||||
{epCount && epCount > 1 ? (
|
|
||||||
<span className='absolute top-[2px] right-1 text-[10px] font-semibold text-green-900 bg-green-300/90 rounded-full px-1 pointer-events-none'>
|
|
||||||
{epCount}集
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user