From 91cdb7a1d29fd2557e486a8fffaf0e90446682da Mon Sep 17 00:00:00 2001 From: mtvpls Date: Sat, 27 Dec 2025 12:31:26 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E9=A6=96=E9=A1=B5=E7=9A=84?= =?UTF-8?q?=E6=94=B6=E8=97=8F=E5=A4=B9=E5=88=87=E6=8D=A2=E5=8D=A1=EF=BC=8C?= =?UTF-8?q?=E6=94=B9=E6=88=90=E4=BB=8E=E7=94=A8=E6=88=B7=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E8=BF=9B=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/page.tsx | 160 ++----------------------- src/components/FavoritesPanel.tsx | 192 ++++++++++++++++++++++++++++++ src/components/UserMenu.tsx | 26 ++++ 3 files changed, 226 insertions(+), 152 deletions(-) create mode 100644 src/components/FavoritesPanel.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index 72187ca..80c0274 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,18 +10,10 @@ import { BangumiCalendarData, GetBangumiCalendarData, } from '@/lib/bangumi.client'; -// 客户端收藏 API -import { - clearAllFavorites, - getAllFavorites, - getAllPlayRecords, - subscribeToDataUpdates, -} from '@/lib/db.client'; import { getDoubanCategories } from '@/lib/douban.client'; import { getTMDBImageUrl, TMDBItem } from '@/lib/tmdb.client'; import { DoubanItem } from '@/lib/types'; -import CapsuleSwitch from '@/components/CapsuleSwitch'; import ContinueWatching from '@/components/ContinueWatching'; import PageLayout from '@/components/PageLayout'; import ScrollableRow from '@/components/ScrollableRow'; @@ -31,7 +23,7 @@ import HttpWarningDialog from '@/components/HttpWarningDialog'; import BannerCarousel from '@/components/BannerCarousel'; function HomeClient() { - const [activeTab, setActiveTab] = useState<'home' | 'favorites'>('home'); + // 移除了 activeTab 状态,收藏夹功能已移到 UserMenu const [hotMovies, setHotMovies] = useState([]); const [hotTvShows, setHotTvShows] = useState([]); const [hotVarietyShows, setHotVarietyShows] = useState([]); @@ -58,22 +50,6 @@ function HomeClient() { } }, [announcement]); - // 收藏夹数据 - type FavoriteItem = { - id: string; - source: string; - title: string; - poster: string; - episodes: number; - source_name: string; - currentEpisode?: number; - search_title?: string; - origin?: 'vod' | 'live'; - }; - - const [favoriteItems, setFavoriteItems] = useState([]); - const favoritesFetchedRef = useRef(false); - useEffect(() => { const fetchRecommendData = async () => { try { @@ -147,74 +123,7 @@ function HomeClient() { fetchRecommendData(); }, []); - // 处理收藏数据更新的函数 - const updateFavoriteItems = useCallback( - async (allFavorites: Record) => { - const allPlayRecords = await getAllPlayRecords(); - // 根据保存时间排序(从近到远) - const sorted = Object.entries(allFavorites) - .sort(([, a], [, b]) => b.save_time - a.save_time) - .map(([key, fav]) => { - const plusIndex = key.indexOf('+'); - const source = key.slice(0, plusIndex); - const id = key.slice(plusIndex + 1); - - // 查找对应的播放记录,获取当前集数 - const playRecord = allPlayRecords[key]; - const currentEpisode = playRecord?.index; - - return { - id, - source, - title: fav.title, - year: fav.year, - poster: fav.cover, - episodes: fav.total_episodes, - source_name: fav.source_name, - currentEpisode, - search_title: fav?.search_title, - origin: fav?.origin, - } as FavoriteItem; - }); - setFavoriteItems(sorted); - }, - [] - ); - - // 当切换到收藏夹时加载收藏数据(使用 ref 防止重复加载) - useEffect(() => { - if (activeTab !== 'favorites') { - favoritesFetchedRef.current = false; - return; - } - - // 已经加载过就不再加载 - if (favoritesFetchedRef.current) return; - - favoritesFetchedRef.current = true; - - const loadFavorites = async () => { - const allFavorites = await getAllFavorites(); - await updateFavoriteItems(allFavorites); - }; - - loadFavorites(); - }, [activeTab, updateFavoriteItems]); - - // 监听收藏更新事件(独立的 useEffect) - useEffect(() => { - if (activeTab !== 'favorites') return; - - const unsubscribe = subscribeToDataUpdates( - 'favoritesUpdated', - (newFavorites: Record) => { - updateFavoriteItems(newFavorites); - } - ); - - return unsubscribe; - }, [activeTab, updateFavoriteItems]); const handleCloseAnnouncement = (announcement: string) => { setShowAnnouncement(false); @@ -223,67 +132,15 @@ function HomeClient() { return ( - {/* TMDB 热门轮播图 - 只在首页显示,且占满宽度 */} - {activeTab === 'home' && ( -
- -
- )} + {/* TMDB 热门轮播图 */} +
+ +
- {/* Tab 切换移到轮播图下方 */} -
- setActiveTab(value as 'home' | 'favorites')} - /> -
-
- {activeTab === 'favorites' ? ( - // 收藏夹视图 -
-
-

- 我的收藏 -

- {favoriteItems.length > 0 && ( - - )} -
-
- {favoriteItems.map((item) => ( -
- 1 ? 'tv' : ''} - /> -
- ))} - {favoriteItems.length === 0 && ( -
- 暂无收藏内容 -
- )} -
-
- ) : ( - // 首页视图 - <> + {/* 首页内容 */} + <> {/* 继续观看 */} @@ -566,8 +423,7 @@ function HomeClient() { )} - - )} +
diff --git a/src/components/FavoritesPanel.tsx b/src/components/FavoritesPanel.tsx new file mode 100644 index 0000000..467980d --- /dev/null +++ b/src/components/FavoritesPanel.tsx @@ -0,0 +1,192 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +'use client'; + +import { Star, X } from 'lucide-react'; +import { useEffect, useState } from 'react'; + +import { + clearAllFavorites, + getAllFavorites, + getAllPlayRecords, + subscribeToDataUpdates, +} from '@/lib/db.client'; +import VideoCard from '@/components/VideoCard'; + +interface FavoriteItem { + id: string; + source: string; + title: string; + year: string; + poster: string; + episodes?: number; + source_name?: string; + currentEpisode?: number; + search_title?: string; + origin?: string; +} + +interface FavoritesPanelProps { + isOpen: boolean; + onClose: () => void; +} + +export const FavoritesPanel: React.FC = ({ + isOpen, + onClose, +}) => { + const [favoriteItems, setFavoriteItems] = useState([]); + const [loading, setLoading] = useState(false); + + // 加载收藏数据 + const loadFavorites = async () => { + setLoading(true); + try { + const allFavorites = await getAllFavorites(); + const allPlayRecords = await getAllPlayRecords(); + + // 根据保存时间排序(从近到远) + const sorted = Object.entries(allFavorites) + .sort(([, a], [, b]) => b.save_time - a.save_time) + .map(([key, fav]) => { + const plusIndex = key.indexOf('+'); + const source = key.slice(0, plusIndex); + const id = key.slice(plusIndex + 1); + + // 查找对应的播放记录,获取当前集数 + const playRecord = allPlayRecords[key]; + const currentEpisode = playRecord?.index; + + return { + id, + source, + title: fav.title, + year: fav.year, + poster: fav.cover, + episodes: fav.total_episodes, + source_name: fav.source_name, + currentEpisode, + search_title: fav?.search_title, + origin: fav?.origin, + } as FavoriteItem; + }); + setFavoriteItems(sorted); + } catch (error) { + console.error('加载收藏失败:', error); + } finally { + setLoading(false); + } + }; + + // 清空所有收藏 + const handleClearAll = async () => { + try { + await clearAllFavorites(); + setFavoriteItems([]); + } catch (error) { + console.error('清空收藏失败:', error); + } + }; + + // 打开面板时加载收藏 + useEffect(() => { + if (isOpen) { + loadFavorites(); + } + }, [isOpen]); + + // 监听收藏变化,实时移除已取消收藏的项目 + useEffect(() => { + const unsubscribe = subscribeToDataUpdates(async (event) => { + if (event === 'favoritesUpdated' && isOpen) { + // 获取最新的收藏列表 + const allFavorites = await getAllFavorites(); + const currentKeys = Object.keys(allFavorites); + + // 过滤掉已经不在收藏中的项目 + setFavoriteItems((prevItems) => + prevItems.filter((item) => { + const key = `${item.source}+${item.id}`; + return currentKeys.includes(key); + }) + ); + } + }); + + return () => { + unsubscribe(); + }; + }, [isOpen]); + + return ( + <> + {/* 背景遮罩 */} +
+ + {/* 收藏面板 */} +
+ {/* 标题栏 */} +
+
+ +

+ 我的收藏 +

+ {favoriteItems.length > 0 && ( + + {favoriteItems.length} 项 + + )} +
+
+ {favoriteItems.length > 0 && ( + + )} + +
+
+ + {/* 收藏列表 */} +
+ {loading ? ( +
+
+
+ ) : favoriteItems.length === 0 ? ( +
+ +

暂无收藏内容

+
+ ) : ( +
+ {favoriteItems.map((item) => ( +
+ 1 ? 'tv' : ''} + /> +
+ ))} +
+ )} +
+
+ + ); +}; diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx index a31c53b..4c8714f 100644 --- a/src/components/UserMenu.tsx +++ b/src/components/UserMenu.tsx @@ -14,6 +14,7 @@ import { Rss, Settings, Shield, + Star, User, X, } from 'lucide-react'; @@ -30,6 +31,7 @@ import { useVersionCheck } from './VersionCheckProvider'; import { VersionPanel } from './VersionPanel'; import { OfflineDownloadPanel } from './OfflineDownloadPanel'; import { NotificationPanel } from './NotificationPanel'; +import { FavoritesPanel } from './FavoritesPanel'; interface AuthInfo { username?: string; @@ -46,6 +48,7 @@ export const UserMenu: React.FC = () => { const [isVersionPanelOpen, setIsVersionPanelOpen] = useState(false); const [isOfflineDownloadPanelOpen, setIsOfflineDownloadPanelOpen] = useState(false); const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false); + const [isFavoritesPanelOpen, setIsFavoritesPanelOpen] = useState(false); const [authInfo, setAuthInfo] = useState(null); const [storageType, setStorageType] = useState('localstorage'); const [mounted, setMounted] = useState(false); @@ -700,6 +703,18 @@ export const UserMenu: React.FC = () => { )} + {/* 我的收藏按钮 */} + + {/* 设置按钮 */}