Files
MoonTVPlus/src/components/ContinueWatching.tsx
2026-01-03 14:54:00 +08:00

219 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable no-console */
'use client';
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { AlertTriangle } from 'lucide-react';
import type { PlayRecord } from '@/lib/db.client';
import {
clearAllPlayRecords,
getAllPlayRecords,
subscribeToDataUpdates,
} from '@/lib/db.client';
import VideoCard from '@/components/VideoCard';
import VirtualScrollableRow from '@/components/VirtualScrollableRow';
interface ContinueWatchingProps {
className?: string;
}
export default function ContinueWatching({ className }: ContinueWatchingProps) {
const [playRecords, setPlayRecords] = useState<
(PlayRecord & { key: string })[]
>([]);
const [loading, setLoading] = useState(true);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
// 处理播放记录数据更新的函数
const updatePlayRecords = (allRecords: Record<string, PlayRecord>) => {
// 将记录转换为数组并根据 save_time 由近到远排序
const recordsArray = Object.entries(allRecords).map(([key, record]) => ({
...record,
key,
}));
// 按 save_time 降序排序(最新的在前面)
const sortedRecords = recordsArray.sort(
(a, b) => b.save_time - a.save_time
);
setPlayRecords(sortedRecords);
};
useEffect(() => {
const fetchPlayRecords = async () => {
try {
setLoading(true);
// 从缓存或API获取所有播放记录
const allRecords = await getAllPlayRecords();
updatePlayRecords(allRecords);
} catch (error) {
console.error('获取播放记录失败:', error);
setPlayRecords([]);
} finally {
setLoading(false);
}
};
fetchPlayRecords();
// 监听播放记录更新事件
const unsubscribe = subscribeToDataUpdates(
'playRecordsUpdated',
(newRecords: Record<string, PlayRecord>) => {
updatePlayRecords(newRecords);
}
);
return unsubscribe;
}, []);
// 如果没有播放记录,则不渲染组件
if (!loading && playRecords.length === 0) {
return null;
}
// 计算播放进度百分比
const getProgress = (record: PlayRecord) => {
if (record.total_time === 0) return 0;
return (record.play_time / record.total_time) * 100;
};
// 从 key 中解析 source 和 id
const parseKey = (key: string) => {
const [source, id] = key.split('+');
return { source, id };
};
// 处理清空确认
const handleClearConfirm = async () => {
await clearAllPlayRecords();
setPlayRecords([]);
setShowConfirmDialog(false);
};
return (
<>
<section className={`mb-8 ${className || ''}`}>
<div className='mb-4 flex items-center justify-between'>
<h2 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
</h2>
{!loading && playRecords.length > 0 && (
<button
className='text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
onClick={() => setShowConfirmDialog(true)}
>
</button>
)}
</div>
{loading ? (
// 加载状态显示灰色占位数据(使用原始 ScrollableRow
<div className="flex gap-2 overflow-x-auto scrollbar-hide">
{Array.from({ length: 8 }).map((_, index) => (
<div
key={index}
className='min-w-[180px] w-48 sm:min-w-[200px] sm:w-52'
>
<div className='relative aspect-[3/2] w-full overflow-hidden rounded-lg bg-gray-200 animate-pulse dark:bg-gray-800'>
<div className='absolute inset-0 bg-gray-300 dark:bg-gray-700'></div>
</div>
<div className='mt-1 h-1 bg-gray-200 rounded animate-pulse dark:bg-gray-800'></div>
<div className='mt-2 h-4 bg-gray-200 rounded animate-pulse dark:bg-gray-800 w-3/4'></div>
</div>
))}
</div>
) : (
// 使用虚拟滚动显示真实数据
<VirtualScrollableRow>
{playRecords.map((record) => {
const { source, id } = parseKey(record.key);
return (
<div
key={record.key}
className='min-w-[180px] w-48 sm:min-w-[200px] sm:w-52'
>
<VideoCard
id={id}
title={record.title}
poster={record.cover}
year={record.year}
source={source}
source_name={record.source_name}
progress={getProgress(record)}
episodes={record.total_episodes}
currentEpisode={record.index}
query={record.search_title}
from='playrecord'
onDelete={() =>
setPlayRecords((prev) =>
prev.filter((r) => r.key !== record.key)
)
}
type={record.total_episodes > 1 ? 'tv' : ''}
origin={record.origin}
orientation='horizontal'
playTime={record.play_time}
totalTime={record.total_time}
/>
</div>
);
})}
</VirtualScrollableRow>
)}
</section>
{/* 确认对话框 */}
{showConfirmDialog && createPortal(
<div
className='fixed inset-0 bg-black bg-opacity-50 z-[9999] flex items-center justify-center p-4 transition-opacity duration-300'
onClick={() => setShowConfirmDialog(false)}
>
<div
className='bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full border border-red-200 dark:border-red-800 transition-all duration-300'
onClick={(e) => e.stopPropagation()}
>
<div className="p-6">
{/* 图标和标题 */}
<div className="flex items-start gap-4 mb-4">
<div className="flex-shrink-0">
<AlertTriangle className="w-8 h-8 text-red-500" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
</p>
</div>
</div>
{/* 按钮组 */}
<div className="flex gap-3 mt-6">
<button
onClick={() => setShowConfirmDialog(false)}
className="flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors"
>
</button>
<button
onClick={handleClearConfirm}
className="flex-1 px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors"
>
</button>
</div>
</div>
</div>
</div>,
document.body
)}
</>
);
}