diff --git a/app/favorites/page.tsx b/app/favorites/page.tsx
new file mode 100644
index 0000000..b335e5d
--- /dev/null
+++ b/app/favorites/page.tsx
@@ -0,0 +1,73 @@
+'use client';
+
+import { Suspense, useState, useMemo } from 'react';
+import { useRouter } from 'next/navigation';
+import { Navbar } from '@/components/layout/Navbar';
+import { FavoritesPageHeader } from '@/components/favorites/FavoritesPageHeader';
+import { FavoritesGrid } from '@/components/favorites/FavoritesGrid';
+import { FavoritesSidebar } from '@/components/favorites/FavoritesSidebar';
+import { WatchHistorySidebar } from '@/components/history/WatchHistorySidebar';
+import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
+import { useFavoritesStore } from '@/lib/store/favorites-store';
+
+function FavoritesPage() {
+ const router = useRouter();
+ const { favorites, clearFavorites } = useFavoritesStore();
+ const [sortBy, setSortBy] = useState<'date' | 'title'>('date');
+ const [isClearDialogOpen, setIsClearDialogOpen] = useState(false);
+
+ const sortedFavorites = useMemo(() => {
+ if (sortBy === 'title') {
+ return [...favorites].sort((a, b) =>
+ a.title.localeCompare(b.title, 'zh-CN')
+ );
+ }
+ return favorites; // already newest-first from store
+ }, [favorites, sortBy]);
+
+ return (
+
+ router.push('/')} />
+
+
+ setIsClearDialogOpen(true)}
+ />
+
+
+
+
+
+
+
+ {
+ clearFavorites();
+ setIsClearDialogOpen(false);
+ }}
+ onCancel={() => setIsClearDialogOpen(false)}
+ dangerous
+ />
+
+ );
+}
+
+export default function Favorites() {
+ return (
+
+
+
+ }>
+
+
+ );
+}
diff --git a/app/premium/favorites/page.tsx b/app/premium/favorites/page.tsx
new file mode 100644
index 0000000..212f74d
--- /dev/null
+++ b/app/premium/favorites/page.tsx
@@ -0,0 +1,73 @@
+'use client';
+
+import { Suspense, useState, useMemo } from 'react';
+import { useRouter } from 'next/navigation';
+import { Navbar } from '@/components/layout/Navbar';
+import { FavoritesPageHeader } from '@/components/favorites/FavoritesPageHeader';
+import { FavoritesGrid } from '@/components/favorites/FavoritesGrid';
+import { FavoritesSidebar } from '@/components/favorites/FavoritesSidebar';
+import { WatchHistorySidebar } from '@/components/history/WatchHistorySidebar';
+import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
+import { usePremiumFavoritesStore } from '@/lib/store/favorites-store';
+
+function PremiumFavoritesPage() {
+ const router = useRouter();
+ const { favorites, clearFavorites } = usePremiumFavoritesStore();
+ const [sortBy, setSortBy] = useState<'date' | 'title'>('date');
+ const [isClearDialogOpen, setIsClearDialogOpen] = useState(false);
+
+ const sortedFavorites = useMemo(() => {
+ if (sortBy === 'title') {
+ return [...favorites].sort((a, b) =>
+ a.title.localeCompare(b.title, 'zh-CN')
+ );
+ }
+ return favorites;
+ }, [favorites, sortBy]);
+
+ return (
+
+ router.push('/premium')} isPremiumMode />
+
+
+ setIsClearDialogOpen(true)}
+ />
+
+
+
+
+
+
+
+ {
+ clearFavorites();
+ setIsClearDialogOpen(false);
+ }}
+ onCancel={() => setIsClearDialogOpen(false)}
+ dangerous
+ />
+
+ );
+}
+
+export default function PremiumFavorites() {
+ return (
+
+
+
+ }>
+
+
+ );
+}
diff --git a/components/favorites/FavoritesGrid.tsx b/components/favorites/FavoritesGrid.tsx
new file mode 100644
index 0000000..46e7037
--- /dev/null
+++ b/components/favorites/FavoritesGrid.tsx
@@ -0,0 +1,105 @@
+'use client';
+
+import { useState, useRef, useCallback, useEffect, memo } from 'react';
+import { VideoCard } from '@/components/search/VideoCard';
+import { FavoritesEmptyState } from './FavoritesEmptyState';
+import type { FavoriteItem, Video } from '@/lib/types';
+
+interface FavoritesGridProps {
+ favorites: FavoriteItem[];
+ isPremium?: boolean;
+}
+
+export const FavoritesGrid = memo(function FavoritesGrid({
+ favorites,
+ isPremium = false
+}: FavoritesGridProps) {
+ const [activeCardId, setActiveCardId] = useState(null);
+ const [visibleCount, setVisibleCount] = useState(24);
+ const loadMoreRef = useRef(null);
+ const observerRef = useRef(null);
+
+ // Convert FavoriteItem to Video format
+ const videos: Video[] = favorites.map((favorite) => ({
+ vod_id: favorite.videoId,
+ vod_name: favorite.title,
+ vod_pic: favorite.poster,
+ vod_remarks: favorite.remarks,
+ vod_year: favorite.year,
+ type_name: favorite.type,
+ source: favorite.source,
+ sourceName: favorite.sourceName,
+ }));
+
+ // Setup intersection observer for infinite scroll
+ useEffect(() => {
+ if (observerRef.current) {
+ observerRef.current.disconnect();
+ }
+
+ observerRef.current = new IntersectionObserver(
+ (entries) => {
+ const first = entries[0];
+ if (first.isIntersecting && visibleCount < videos.length) {
+ setVisibleCount((prev) => Math.min(prev + 24, videos.length));
+ }
+ },
+ { threshold: 0.1 }
+ );
+
+ if (loadMoreRef.current) {
+ observerRef.current.observe(loadMoreRef.current);
+ }
+
+ return () => {
+ if (observerRef.current) {
+ observerRef.current.disconnect();
+ }
+ };
+ }, [visibleCount, videos.length]);
+
+ const handleCardClick = useCallback((
+ e: React.MouseEvent,
+ cardId: string,
+ videoUrl: string
+ ) => {
+ setActiveCardId(cardId);
+ }, []);
+
+ if (videos.length === 0) {
+ return ;
+ }
+
+ return (
+ <>
+
+ {videos.slice(0, visibleCount).map((video) => {
+ const cardId = `${video.source}:${video.vod_id}`;
+ const videoUrl = `/player?id=${video.vod_id}&source=${video.source}&title=${encodeURIComponent(video.vod_name)}${isPremium ? '&premium=1' : ''}`;
+ const isActive = activeCardId === cardId;
+
+ return (
+
+ );
+ })}
+
+
+ {/* Load more trigger */}
+ {visibleCount < videos.length && (
+
+ )}
+ >
+ );
+});
diff --git a/components/favorites/FavoritesPageHeader.tsx b/components/favorites/FavoritesPageHeader.tsx
new file mode 100644
index 0000000..b4fd4fa
--- /dev/null
+++ b/components/favorites/FavoritesPageHeader.tsx
@@ -0,0 +1,84 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+import { Icons } from '@/components/ui/Icon';
+
+interface FavoritesPageHeaderProps {
+ count: number;
+ sortBy: 'date' | 'title';
+ onSortChange: (sort: 'date' | 'title') => void;
+ onClearAll: () => void;
+}
+
+export function FavoritesPageHeader({
+ count,
+ sortBy,
+ onSortChange,
+ onClearAll
+}: FavoritesPageHeaderProps) {
+ const router = useRouter();
+
+ return (
+
+
+
+
+
+
+
+
+
我的收藏
+
+ 共 {count} 个视频
+
+
+
+
+
+ {/* Sort buttons */}
+
+
+
+
+
+ {/* Clear all button */}
+ {count > 0 && (
+
+ )}
+
+
+
+ );
+}
diff --git a/components/layout/Navbar.tsx b/components/layout/Navbar.tsx
index 0fbb83d..5d6a982 100644
--- a/components/layout/Navbar.tsx
+++ b/components/layout/Navbar.tsx
@@ -17,6 +17,7 @@ interface NavbarProps {
export function Navbar({ onReset, isPremiumMode = false }: NavbarProps) {
const settingsHref = isPremiumMode ? '/premium/settings' : '/settings';
+ const favoritesHref = isPremiumMode ? '/premium/favorites' : '/favorites';
const [session] = useState(() => getSession());
const { iptvEnabled } = useRuntimeFeatures();
@@ -104,6 +105,13 @@ export function Navbar({ onReset, isPremiumMode = false }: NavbarProps) {
>
+
+
+