剧集更新提示
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 添加 new_episodes 字段到播放记录表
|
||||||
|
-- 版本: 1.0.1
|
||||||
|
-- 创建时间: 2026-02-19
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 为播放记录表添加 new_episodes 字段(用于显示新增剧集提示)
|
||||||
|
ALTER TABLE play_records ADD COLUMN new_episodes INTEGER;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 添加 new_episodes 字段到播放记录表
|
||||||
|
-- 版本: 1.0.1
|
||||||
|
-- 创建时间: 2026-02-19
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 为播放记录表添加 new_episodes 字段(用于显示新增剧集提示)
|
||||||
|
ALTER TABLE play_records ADD COLUMN new_episodes BIGINT;
|
||||||
@@ -216,6 +216,14 @@ async function refreshRecordAndFavorites() {
|
|||||||
|
|
||||||
const episodeCount = detail.episodes?.length || 0;
|
const episodeCount = detail.episodes?.length || 0;
|
||||||
if (episodeCount > 0 && episodeCount !== record.total_episodes) {
|
if (episodeCount > 0 && episodeCount !== record.total_episodes) {
|
||||||
|
// 计算新增的剧集数量
|
||||||
|
const newEpisodesCount = episodeCount > record.total_episodes
|
||||||
|
? episodeCount - record.total_episodes
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// 如果有新增剧集,累加到现有的 new_episodes 字段
|
||||||
|
const updatedNewEpisodes = (record.new_episodes || 0) + newEpisodesCount;
|
||||||
|
|
||||||
await db.savePlayRecord(user, source, id, {
|
await db.savePlayRecord(user, source, id, {
|
||||||
title: detail.title || record.title,
|
title: detail.title || record.title,
|
||||||
source_name: record.source_name,
|
source_name: record.source_name,
|
||||||
@@ -227,9 +235,10 @@ async function refreshRecordAndFavorites() {
|
|||||||
total_time: record.total_time,
|
total_time: record.total_time,
|
||||||
save_time: record.save_time,
|
save_time: record.save_time,
|
||||||
search_title: record.search_title,
|
search_title: record.search_title,
|
||||||
|
new_episodes: updatedNewEpisodes > 0 ? updatedNewEpisodes : undefined,
|
||||||
});
|
});
|
||||||
console.log(
|
console.log(
|
||||||
`更新播放记录: ${record.title} (${record.total_episodes} -> ${episodeCount})`
|
`更新播放记录: ${record.title} (${record.total_episodes} -> ${episodeCount}, 新增 ${newEpisodesCount} 集)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -211,3 +211,37 @@ div[data-media-provider] video {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 水波纹缩放动画 - 第一层 (快速扩散) */
|
||||||
|
@keyframes ping-scale {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
75%, 100% {
|
||||||
|
transform: scale(2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 水波纹缩放动画 - 第二层 (慢速脉冲) */
|
||||||
|
@keyframes pulse-scale {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.15);
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主体徽章缩放动画 */
|
||||||
|
@keyframes badge-scale {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) {
|
|||||||
</div>
|
</div>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
// 加载状态显示灰色占位数据(使用原始 ScrollableRow)
|
// 加载状态显示灰色占位数据(使用原始 ScrollableRow)
|
||||||
<div className="flex gap-2 overflow-x-auto scrollbar-hide">
|
<div className="flex gap-2 overflow-x-auto scrollbar-hide pt-2 pb-2">
|
||||||
{Array.from({ length: 8 }).map((_, index) => (
|
{Array.from({ length: 8 }).map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
@@ -140,41 +140,98 @@ export default function ContinueWatching({ className }: ContinueWatchingProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 使用虚拟滚动显示真实数据
|
// 使用虚拟滚动显示真实数据
|
||||||
<VirtualScrollableRow>
|
<div>
|
||||||
{playRecords.map((record) => {
|
<VirtualScrollableRow>
|
||||||
const { source, id } = parseKey(record.key);
|
{playRecords.map((record) => {
|
||||||
return (
|
const { source, id } = parseKey(record.key);
|
||||||
<div
|
return (
|
||||||
key={record.key}
|
<div
|
||||||
className='min-w-[180px] w-48 sm:min-w-[200px] sm:w-52'
|
key={record.key}
|
||||||
>
|
className='min-w-[180px] w-48 sm:min-w-[200px] sm:w-52'
|
||||||
<VideoCard
|
style={{ position: 'relative' }}
|
||||||
id={id}
|
>
|
||||||
title={record.title}
|
<VideoCard
|
||||||
poster={record.cover}
|
id={id}
|
||||||
year={record.year}
|
title={record.title}
|
||||||
source={source}
|
poster={record.cover}
|
||||||
source_name={record.source_name}
|
year={record.year}
|
||||||
progress={getProgress(record)}
|
source={source}
|
||||||
episodes={record.total_episodes}
|
source_name={record.source_name}
|
||||||
currentEpisode={record.index}
|
progress={getProgress(record)}
|
||||||
query={record.search_title}
|
episodes={record.total_episodes}
|
||||||
from='playrecord'
|
currentEpisode={record.index}
|
||||||
onDelete={() =>
|
query={record.search_title}
|
||||||
setPlayRecords((prev) =>
|
from='playrecord'
|
||||||
prev.filter((r) => r.key !== record.key)
|
onDelete={() =>
|
||||||
)
|
setPlayRecords((prev) =>
|
||||||
}
|
prev.filter((r) => r.key !== record.key)
|
||||||
type={record.total_episodes > 1 ? 'tv' : ''}
|
)
|
||||||
origin={record.origin}
|
}
|
||||||
orientation='horizontal'
|
type={record.total_episodes > 1 ? 'tv' : ''}
|
||||||
playTime={record.play_time}
|
origin={record.origin}
|
||||||
totalTime={record.total_time}
|
orientation='horizontal'
|
||||||
/>
|
playTime={record.play_time}
|
||||||
</div>
|
totalTime={record.total_time}
|
||||||
);
|
/>
|
||||||
})}
|
{/* 新增剧集提示 - 完全独立于 VideoCard */}
|
||||||
</VirtualScrollableRow>
|
{record.new_episodes && record.new_episodes > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-6px',
|
||||||
|
right: '-6px',
|
||||||
|
zIndex: 100,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
width: '28px',
|
||||||
|
height: '28px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 水波纹动画 - 第一层 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: '0',
|
||||||
|
borderRadius: '9999px',
|
||||||
|
backgroundColor: 'rgb(14 165 233)',
|
||||||
|
animation: 'ping-scale 1.5s cubic-bezier(0, 0, 0.2, 1) infinite',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 水波纹动画 - 第二层 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: '0',
|
||||||
|
borderRadius: '9999px',
|
||||||
|
backgroundColor: 'rgb(14 165 233)',
|
||||||
|
animation: 'pulse-scale 2.5s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 主体徽章 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: '0',
|
||||||
|
borderRadius: '9999px',
|
||||||
|
background: 'linear-gradient(to bottom right, rgb(14 165 233), rgb(2 132 199))',
|
||||||
|
color: 'white',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
|
||||||
|
animation: 'badge-scale 2s ease-in-out infinite',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+{record.new_episodes}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</VirtualScrollableRow>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1050,10 +1050,10 @@ const DetailPanel: React.FC<DetailPanelProps> = ({
|
|||||||
{actor.profile_path ? (
|
{actor.profile_path ? (
|
||||||
<div
|
<div
|
||||||
className="relative w-20 h-20 rounded-full overflow-hidden bg-gray-200 dark:bg-gray-700 mb-2 cursor-pointer hover:opacity-80 transition-opacity"
|
className="relative w-20 h-20 rounded-full overflow-hidden bg-gray-200 dark:bg-gray-700 mb-2 cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
onClick={() => handleImageClick(processImageUrl(getTMDBImageUrl(actor.profile_path, 'w185')))}
|
onClick={() => handleImageClick(processImageUrl(getTMDBImageUrl(actor.profile_path || null, 'w185')))}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={processImageUrl(getTMDBImageUrl(actor.profile_path, 'w185'))}
|
src={processImageUrl(getTMDBImageUrl(actor.profile_path || null, 'w185'))}
|
||||||
alt={actor.name}
|
alt={actor.name}
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import { useEffect, useRef, useState } from 'react';
|
|||||||
interface VirtualScrollableRowProps {
|
interface VirtualScrollableRowProps {
|
||||||
children: React.ReactNode[];
|
children: React.ReactNode[];
|
||||||
maxVisible?: number; // 最大可见数量
|
maxVisible?: number; // 最大可见数量
|
||||||
|
className?: string; // 额外的 CSS 类名
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function VirtualScrollableRow({
|
export default function VirtualScrollableRow({
|
||||||
children,
|
children,
|
||||||
maxVisible = 30, // 默认最多显示 30 个项目
|
maxVisible = 30, // 默认最多显示 30 个项目
|
||||||
|
className = '',
|
||||||
}: VirtualScrollableRowProps) {
|
}: VirtualScrollableRowProps) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [showLeftScroll, setShowLeftScroll] = useState(false);
|
const [showLeftScroll, setShowLeftScroll] = useState(false);
|
||||||
@@ -94,8 +96,8 @@ export default function VirtualScrollableRow({
|
|||||||
{/* 滚动容器 */}
|
{/* 滚动容器 */}
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="flex gap-2 overflow-x-auto scrollbar-hide scroll-smooth"
|
className={`flex gap-2 overflow-x-auto scrollbar-hide scroll-smooth ${className}`}
|
||||||
style={{ scrollBehavior: 'smooth' }}
|
style={{ scrollBehavior: 'smooth', paddingTop: '20px', paddingBottom: '20px', marginTop: '-20px', marginBottom: '-20px' }}
|
||||||
>
|
>
|
||||||
{/* 左侧占位符(用于保持滚动位置) */}
|
{/* 左侧占位符(用于保持滚动位置) */}
|
||||||
{visibleRange.start > 0 && (
|
{visibleRange.start > 0 && (
|
||||||
|
|||||||
+7
-4
@@ -66,9 +66,9 @@ export class D1Storage implements IStorage {
|
|||||||
INSERT INTO play_records (
|
INSERT INTO play_records (
|
||||||
username, key, title, source_name, cover, year,
|
username, key, title, source_name, cover, year,
|
||||||
episode_index, total_episodes, play_time, total_time,
|
episode_index, total_episodes, play_time, total_time,
|
||||||
save_time, search_title
|
save_time, search_title, new_episodes
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(username, key) DO UPDATE SET
|
ON CONFLICT(username, key) DO UPDATE SET
|
||||||
title = excluded.title,
|
title = excluded.title,
|
||||||
source_name = excluded.source_name,
|
source_name = excluded.source_name,
|
||||||
@@ -79,7 +79,8 @@ export class D1Storage implements IStorage {
|
|||||||
play_time = excluded.play_time,
|
play_time = excluded.play_time,
|
||||||
total_time = excluded.total_time,
|
total_time = excluded.total_time,
|
||||||
save_time = excluded.save_time,
|
save_time = excluded.save_time,
|
||||||
search_title = excluded.search_title
|
search_title = excluded.search_title,
|
||||||
|
new_episodes = excluded.new_episodes
|
||||||
`)
|
`)
|
||||||
.bind(
|
.bind(
|
||||||
userName,
|
userName,
|
||||||
@@ -93,7 +94,8 @@ export class D1Storage implements IStorage {
|
|||||||
record.play_time,
|
record.play_time,
|
||||||
record.total_time,
|
record.total_time,
|
||||||
record.save_time,
|
record.save_time,
|
||||||
record.search_title || ''
|
record.search_title || '',
|
||||||
|
record.new_episodes || null
|
||||||
)
|
)
|
||||||
.run();
|
.run();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -727,6 +729,7 @@ export class D1Storage implements IStorage {
|
|||||||
total_time: row.total_time,
|
total_time: row.total_time,
|
||||||
save_time: row.save_time,
|
save_time: row.save_time,
|
||||||
search_title: row.search_title || '',
|
search_title: row.search_title || '',
|
||||||
|
new_episodes: row.new_episodes || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export interface PlayRecord {
|
|||||||
save_time: number; // 记录保存时间(时间戳)
|
save_time: number; // 记录保存时间(时间戳)
|
||||||
search_title?: string; // 搜索时使用的标题
|
search_title?: string; // 搜索时使用的标题
|
||||||
origin?: 'vod' | 'live'; // 来源类型
|
origin?: 'vod' | 'live'; // 来源类型
|
||||||
|
new_episodes?: number; // 新增的剧集数量(用于显示更新提示)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 收藏类型 ----
|
// ---- 收藏类型 ----
|
||||||
|
|||||||
@@ -67,9 +67,9 @@ export class PostgresStorage implements IStorage {
|
|||||||
INSERT INTO play_records (
|
INSERT INTO play_records (
|
||||||
username, key, title, source_name, cover, year,
|
username, key, title, source_name, cover, year,
|
||||||
episode_index, total_episodes, play_time, total_time,
|
episode_index, total_episodes, play_time, total_time,
|
||||||
save_time, search_title
|
save_time, search_title, new_episodes
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||||
ON CONFLICT (username, key) DO UPDATE SET
|
ON CONFLICT (username, key) DO UPDATE SET
|
||||||
title = EXCLUDED.title,
|
title = EXCLUDED.title,
|
||||||
source_name = EXCLUDED.source_name,
|
source_name = EXCLUDED.source_name,
|
||||||
@@ -80,7 +80,8 @@ export class PostgresStorage implements IStorage {
|
|||||||
play_time = EXCLUDED.play_time,
|
play_time = EXCLUDED.play_time,
|
||||||
total_time = EXCLUDED.total_time,
|
total_time = EXCLUDED.total_time,
|
||||||
save_time = EXCLUDED.save_time,
|
save_time = EXCLUDED.save_time,
|
||||||
search_title = EXCLUDED.search_title
|
search_title = EXCLUDED.search_title,
|
||||||
|
new_episodes = EXCLUDED.new_episodes
|
||||||
`)
|
`)
|
||||||
.bind(
|
.bind(
|
||||||
userName,
|
userName,
|
||||||
@@ -94,7 +95,8 @@ export class PostgresStorage implements IStorage {
|
|||||||
record.play_time,
|
record.play_time,
|
||||||
record.total_time,
|
record.total_time,
|
||||||
record.save_time,
|
record.save_time,
|
||||||
record.search_title || ''
|
record.search_title || '',
|
||||||
|
record.new_episodes || null
|
||||||
)
|
)
|
||||||
.run();
|
.run();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -301,6 +303,7 @@ export class PostgresStorage implements IStorage {
|
|||||||
total_time: row.total_time,
|
total_time: row.total_time,
|
||||||
save_time: row.save_time,
|
save_time: row.save_time,
|
||||||
search_title: row.search_title || '',
|
search_title: row.search_title || '',
|
||||||
|
new_episodes: row.new_episodes || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface PlayRecord {
|
|||||||
total_time: number; // 总进度(秒)
|
total_time: number; // 总进度(秒)
|
||||||
save_time: number; // 记录保存时间(时间戳)
|
save_time: number; // 记录保存时间(时间戳)
|
||||||
search_title: string; // 搜索时使用的标题
|
search_title: string; // 搜索时使用的标题
|
||||||
|
new_episodes?: number; // 新增的剧集数量(用于显示更新提示)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 收藏数据结构
|
// 收藏数据结构
|
||||||
|
|||||||
Reference in New Issue
Block a user