From c5e2397f2a62edf35f9b87a603a3f06c9b9ea093 Mon Sep 17 00:00:00 2001 From: kuekhaoyang Date: Fri, 2 Jan 2026 13:54:27 +0800 Subject: [PATCH] feat: implement secret mode with separate history and favorites, and dedicated player links --- app/player/page.tsx | 13 +- app/secret/page.tsx | 3 +- components/favorites/FavoriteButton.tsx | 6 +- components/favorites/FavoritesItem.tsx | 6 +- components/favorites/FavoritesList.tsx | 4 +- components/favorites/FavoritesSidebar.tsx | 11 +- components/history/HistoryItem.tsx | 59 ++--- components/history/HistoryList.tsx | 14 +- components/history/WatchHistorySidebar.tsx | 6 +- components/home/SearchResults.tsx | 5 +- components/player/VideoPlayer.tsx | 9 +- components/player/desktop/DesktopOverlay.tsx | 4 +- .../player/desktop/DesktopRightControls.tsx | 8 +- components/search/VideoCard.tsx | 5 +- components/search/VideoGrid.tsx | 16 +- components/settings/AddSourceModal.tsx | 2 +- components/settings/import/FileImportTab.tsx | 2 +- components/settings/import/LinkImportTab.tsx | 2 +- lib/hooks/useVideoPlayer.ts | 8 +- lib/store/favorites-store.ts | 156 +++++++------ lib/store/history-store.ts | 208 ++++++++++-------- package-lock.json | 4 +- package.json | 2 +- 23 files changed, 296 insertions(+), 257 deletions(-) diff --git a/app/player/page.tsx b/app/player/page.tsx index 80080be..728e0f0 100644 --- a/app/player/page.tsx +++ b/app/player/page.tsx @@ -9,8 +9,7 @@ import { EpisodeList } from '@/components/player/EpisodeList'; import { PlayerError } from '@/components/player/PlayerError'; import { SourceSelector, SourceInfo } from '@/components/player/SourceSelector'; import { useVideoPlayer } from '@/lib/hooks/useVideoPlayer'; -import { useHistoryStore } from '@/lib/store/history-store'; -import { WatchHistorySidebar } from '@/components/history/WatchHistorySidebar'; +import { useHistory } from '@/lib/store/history-store'; import { FavoritesSidebar } from '@/components/favorites/FavoritesSidebar'; import { FavoriteButton } from '@/components/favorites/FavoriteButton'; import { PlayerNavbar } from '@/components/player/PlayerNavbar'; @@ -20,7 +19,8 @@ import Image from 'next/image'; function PlayerContent() { const searchParams = useSearchParams(); const router = useRouter(); - const { addToHistory } = useHistoryStore(); + const isSecret = searchParams.get('secret') === '1'; + const { addToHistory } = useHistory(isSecret); const videoId = searchParams.get('id'); const source = searchParams.get('source'); @@ -162,6 +162,7 @@ function PlayerContent() { totalEpisodes={videoData?.episodes?.length || 1} onNextEpisode={handleNextEpisode} isReversed={isReversed} + isSecret={isSecret} /> 收藏这个视频 @@ -227,10 +229,7 @@ function PlayerContent() { {/* Favorites Sidebar - Left */} - - - {/* Watch History Sidebar - Right */} - + ); } diff --git a/app/secret/page.tsx b/app/secret/page.tsx index 7d23816..608824e 100644 --- a/app/secret/page.tsx +++ b/app/secret/page.tsx @@ -52,6 +52,7 @@ function SecretHomePage() { results={results} availableSources={availableSources} loading={loading} + isSecret={true} /> )} @@ -67,7 +68,7 @@ function SecretHomePage() { {/* Favorites Sidebar - Left */} - + ); } diff --git a/components/favorites/FavoriteButton.tsx b/components/favorites/FavoriteButton.tsx index d042e70..3611120 100644 --- a/components/favorites/FavoriteButton.tsx +++ b/components/favorites/FavoriteButton.tsx @@ -6,7 +6,7 @@ 'use client'; import { memo, useCallback, useState, useEffect } from 'react'; -import { useFavoritesStore } from '@/lib/store/favorites-store'; +import { useFavorites } from '@/lib/store/favorites-store'; import { Icons } from '@/components/ui/Icon'; interface FavoriteButtonProps { @@ -21,6 +21,7 @@ interface FavoriteButtonProps { className?: string; size?: number; showTooltip?: boolean; + isSecret?: boolean; } export const FavoriteButton = memo(({ @@ -35,8 +36,9 @@ export const FavoriteButton = memo(({ className = '', size = 20, showTooltip = true, + isSecret = false, }) => { - const { isFavorite, toggleFavorite } = useFavoritesStore(); + const { isFavorite, toggleFavorite } = useFavorites(isSecret); const [isAnimating, setIsAnimating] = useState(false); const [isFav, setIsFav] = useState(false); diff --git a/components/favorites/FavoritesItem.tsx b/components/favorites/FavoritesItem.tsx index 39fc812..3f06a14 100644 --- a/components/favorites/FavoritesItem.tsx +++ b/components/favorites/FavoritesItem.tsx @@ -10,15 +10,19 @@ import type { FavoriteItem } from '@/lib/types'; interface FavoritesItemProps { item: FavoriteItem; onRemove: () => void; + isSecret?: boolean; } -export function FavoritesItem({ item, onRemove }: FavoritesItemProps) { +export function FavoritesItem({ item, onRemove, isSecret = false }: FavoritesItemProps) { const getVideoUrl = (): string => { const params = new URLSearchParams({ id: item.videoId.toString(), source: item.source, title: item.title, }); + if (isSecret) { + params.set('secret', '1'); + } return `/player?${params.toString()}`; }; diff --git a/components/favorites/FavoritesList.tsx b/components/favorites/FavoritesList.tsx index a270750..131aea5 100644 --- a/components/favorites/FavoritesList.tsx +++ b/components/favorites/FavoritesList.tsx @@ -9,9 +9,10 @@ import { FavoritesEmptyState } from './FavoritesEmptyState'; interface FavoritesListProps { favorites: FavoriteItem[]; onRemove: (videoId: string | number, source: string) => void; + isSecret?: boolean; } -export function FavoritesList({ favorites, onRemove }: FavoritesListProps) { +export function FavoritesList({ favorites, onRemove, isSecret = false }: FavoritesListProps) { if (favorites.length === 0) { return ; } @@ -23,6 +24,7 @@ export function FavoritesList({ favorites, onRemove }: FavoritesListProps) { key={`${item.source}:${item.videoId}`} item={item} onRemove={() => onRemove(item.videoId, item.source)} + isSecret={isSecret} /> ))} diff --git a/components/favorites/FavoritesSidebar.tsx b/components/favorites/FavoritesSidebar.tsx index 7a364c1..22cae94 100644 --- a/components/favorites/FavoritesSidebar.tsx +++ b/components/favorites/FavoritesSidebar.tsx @@ -6,7 +6,8 @@ 'use client'; import { useState, useEffect, useRef } from 'react'; -import { useFavoritesStore } from '@/lib/store/favorites-store'; +import { useFavorites } from '@/lib/store/favorites-store'; +import { WatchHistorySidebar } from '@/components/history/WatchHistorySidebar'; import { Icons } from '@/components/ui/Icon'; import { ConfirmDialog } from '@/components/ui/ConfirmDialog'; import { FavoritesHeader } from './FavoritesHeader'; @@ -14,7 +15,7 @@ import { FavoritesList } from './FavoritesList'; import { FavoritesFooter } from './FavoritesFooter'; import { trapFocus } from '@/lib/accessibility/focus-management'; -export function FavoritesSidebar() { +export function FavoritesSidebar({ isSecret = false }: { isSecret?: boolean }) { const [isOpen, setIsOpen] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; @@ -22,7 +23,7 @@ export function FavoritesSidebar() { source?: string; isClearAll?: boolean; }>({ isOpen: false }); - const { favorites, removeFavorite, clearFavorites } = useFavoritesStore(); + const { favorites, removeFavorite, clearFavorites } = useFavorites(isSecret); const sidebarRef = useRef(null); const cleanupFocusTrapRef = useRef<(() => void) | null>(null); @@ -115,6 +116,7 @@ export function FavoritesSidebar() { + {/* Watch History Sidebar - Right side */} + + {/* Confirm Dialog */} ; - playbackPosition: number; - duration: number; - timestamp: number; + item: VideoHistoryItem; onRemove: () => void; + isSecret?: boolean; } -export function HistoryItem({ - videoId, - source, - title, - poster, - episodeIndex, - episodes, - playbackPosition, - duration, - timestamp, - onRemove, -}: HistoryItemProps) { +export function HistoryItem({ item, onRemove, isSecret = false }: HistoryItemProps) { const getVideoUrl = (): string => { const params = new URLSearchParams({ - id: videoId.toString(), - source, - title, - episode: episodeIndex.toString(), + id: item.videoId.toString(), + source: item.source, + title: item.title, + episode: item.episodeIndex.toString(), }); + if (isSecret) { + params.set('secret', '1'); + } return `/player?${params.toString()}`; }; @@ -53,9 +39,9 @@ export function HistoryItem({ } }; - const progress = (playbackPosition / duration) * 100; - const episodeText = episodes && episodes.length > 0 - ? episodes[episodeIndex]?.name || `第${episodeIndex + 1}集` + const progress = (item.playbackPosition / item.duration) * 100; + const episodeText = item.episodes && item.episodes.length > 0 + ? item.episodes[item.episodeIndex]?.name || `第${item.episodeIndex + 1}集` : ''; return ( @@ -74,12 +60,12 @@ export function HistoryItem({ >
{/* Poster */} - + {/* Info */}

- {title} + {item.title}

{episodeText && (

@@ -87,8 +73,8 @@ export function HistoryItem({

)}
- {formatTime(playbackPosition)} / {formatTime(duration)} - {formatDate(timestamp)} + {formatTime(item.playbackPosition)} / {formatTime(item.duration)} + {formatDate(item.timestamp)}
@@ -96,14 +82,15 @@ export function HistoryItem({
{/* Favorite button */} {/* Delete button */} diff --git a/components/history/HistoryList.tsx b/components/history/HistoryList.tsx index 5e29c17..4d21040 100644 --- a/components/history/HistoryList.tsx +++ b/components/history/HistoryList.tsx @@ -5,9 +5,10 @@ import type { VideoHistoryItem } from '@/lib/types'; interface HistoryListProps { history: VideoHistoryItem[]; onRemove: (videoId: string | number, source: string) => void; + isSecret?: boolean; } -export function HistoryList({ history, onRemove }: HistoryListProps) { +export function HistoryList({ history, onRemove, isSecret = false }: HistoryListProps) { return (
( onRemove(item.videoId, item.source)} + isSecret={isSecret} /> ))}
diff --git a/components/history/WatchHistorySidebar.tsx b/components/history/WatchHistorySidebar.tsx index 1cdfb62..10609a6 100644 --- a/components/history/WatchHistorySidebar.tsx +++ b/components/history/WatchHistorySidebar.tsx @@ -6,7 +6,7 @@ 'use client'; import { useState, useEffect, useRef } from 'react'; -import { useHistoryStore } from '@/lib/store/history-store'; +import { useHistory } from '@/lib/store/history-store'; import { Icons } from '@/components/ui/Icon'; import { ConfirmDialog } from '@/components/ui/ConfirmDialog'; import { HistoryHeader } from './HistoryHeader'; @@ -14,7 +14,7 @@ import { HistoryList } from './HistoryList'; import { HistoryFooter } from './HistoryFooter'; import { trapFocus } from '@/lib/accessibility/focus-management'; -export function WatchHistorySidebar() { +export function WatchHistorySidebar({ isSecret = false }: { isSecret?: boolean }) { const [isOpen, setIsOpen] = useState(false); const [deleteConfirm, setDeleteConfirm] = useState<{ isOpen: boolean; @@ -22,7 +22,7 @@ export function WatchHistorySidebar() { source?: string; isClearAll?: boolean; }>({ isOpen: false }); - const { viewingHistory, removeFromHistory, clearHistory } = useHistoryStore(); + const { viewingHistory, removeFromHistory, clearHistory } = useHistory(isSecret); const sidebarRef = useRef(null); const cleanupFocusTrapRef = useRef<(() => void) | null>(null); diff --git a/components/home/SearchResults.tsx b/components/home/SearchResults.tsx index f8efeac..32442b9 100644 --- a/components/home/SearchResults.tsx +++ b/components/home/SearchResults.tsx @@ -11,9 +11,10 @@ interface SearchResultsProps { results: Video[]; availableSources: SourceBadge[]; loading: boolean; + isSecret?: boolean; } -export function SearchResults({ results, availableSources, loading }: SearchResultsProps) { +export function SearchResults({ results, availableSources, loading, isSecret = false }: SearchResultsProps) { // Source badges hook - filters by video source const { selectedSources, @@ -61,7 +62,7 @@ export function SearchResults({ results, availableSources, loading }: SearchResu )} {/* Display filtered videos (both source and type filters applied) */} - +
); } diff --git a/components/player/VideoPlayer.tsx b/components/player/VideoPlayer.tsx index b5c43a3..3437d20 100644 --- a/components/player/VideoPlayer.tsx +++ b/components/player/VideoPlayer.tsx @@ -3,7 +3,7 @@ import { useState, useRef, useEffect, useCallback } from 'react'; import { useSearchParams } from 'next/navigation'; import { Card } from '@/components/ui/Card'; -import { useHistoryStore } from '@/lib/store/history-store'; +import { useHistory } from '@/lib/store/history-store'; import { settingsStore } from '@/lib/store/settings-store'; import { CustomVideoPlayer } from './CustomVideoPlayer'; import { VideoPlayerError } from './VideoPlayerError'; @@ -18,6 +18,7 @@ interface VideoPlayerProps { totalEpisodes?: number; onNextEpisode?: () => void; isReversed?: boolean; + isSecret?: boolean; } export function VideoPlayer({ @@ -27,7 +28,8 @@ export function VideoPlayer({ onBack, totalEpisodes, onNextEpisode, - isReversed = false + isReversed = false, + isSecret = false }: VideoPlayerProps) { const [videoError, setVideoError] = useState(''); const [useProxy, setUseProxy] = useState(false); @@ -47,9 +49,8 @@ export function VideoPlayer({ // Use reactive hook to subscribe to history updates // This ensures the component re-renders when history is hydrated from localStorage - const viewingHistory = useHistoryStore(state => state.viewingHistory); + const { viewingHistory, addToHistory } = useHistory(isSecret); const searchParams = useSearchParams(); - const { addToHistory } = useHistoryStore(); // Get video metadata from URL params const source = searchParams.get('source') || ''; diff --git a/components/player/desktop/DesktopOverlay.tsx b/components/player/desktop/DesktopOverlay.tsx index 024c3d2..f3fd4d7 100644 --- a/components/player/desktop/DesktopOverlay.tsx +++ b/components/player/desktop/DesktopOverlay.tsx @@ -119,7 +119,7 @@ export function DesktopOverlay({
- -{skipBackwardAmount}s + -{skipBackwardAmount}秒
)} @@ -129,7 +129,7 @@ export function DesktopOverlay({
- +{skipForwardAmount}s + +{skipForwardAmount}秒
)} diff --git a/components/player/desktop/DesktopRightControls.tsx b/components/player/desktop/DesktopRightControls.tsx index 71be67d..d18bc08 100644 --- a/components/player/desktop/DesktopRightControls.tsx +++ b/components/player/desktop/DesktopRightControls.tsx @@ -48,8 +48,8 @@ export function DesktopRightControls({ @@ -62,8 +62,8 @@ export function DesktopRightControls({ diff --git a/components/search/VideoCard.tsx b/components/search/VideoCard.tsx index 827d552..5b88ecf 100644 --- a/components/search/VideoCard.tsx +++ b/components/search/VideoCard.tsx @@ -18,6 +18,7 @@ interface VideoCardProps { cardId: string; isActive: boolean; onCardClick: (e: React.MouseEvent, cardId: string, videoUrl: string) => void; + isSecret?: boolean; } export const VideoCard = memo(({ @@ -25,7 +26,8 @@ export const VideoCard = memo(({ videoUrl, cardId, isActive, - onCardClick + onCardClick, + isSecret = false }) => { return (
(({ remarks={video.vod_remarks} size={16} className="shadow-md" + isSecret={isSecret} />
diff --git a/components/search/VideoGrid.tsx b/components/search/VideoGrid.tsx index 1451726..32966dd 100644 --- a/components/search/VideoGrid.tsx +++ b/components/search/VideoGrid.tsx @@ -9,9 +9,10 @@ import { Video } from '@/lib/types'; interface VideoGridProps { videos: Video[]; className?: string; + isSecret?: boolean; } -export const VideoGrid = memo(function VideoGrid({ videos, className = '' }: VideoGridProps) { +export const VideoGrid = memo(function VideoGrid({ videos, className = '', isSecret = false }: VideoGridProps) { const [activeCardId, setActiveCardId] = useState(null); const [visibleCount, setVisibleCount] = useState(24); const [displayMode, setDisplayMode] = useState<'normal' | 'grouped'>('normal'); @@ -99,17 +100,23 @@ export const VideoGrid = memo(function VideoGrid({ videos, className = '' }: Vid if (displayMode === 'grouped') return []; return videos.map((video, index) => { - const videoUrl = `/player?${new URLSearchParams({ + const params: Record = { id: String(video.vod_id), source: video.source, title: video.vod_name, - }).toString()}`; + }; + + if (isSecret) { + params.secret = '1'; + } + + const videoUrl = `/player?${new URLSearchParams(params).toString()}`; const cardId = `${video.vod_id}-${index}`; return { video, videoUrl, cardId }; }); - }, [videos, displayMode]); + }, [videos, displayMode, isSecret]); // Grouped mode items const groupItems = useMemo(() => { @@ -157,6 +164,7 @@ export const VideoGrid = memo(function VideoGrid({ videos, className = '' }: Vid cardId={cardId} isActive={isActive} onCardClick={handleCardClick} + isSecret={isSecret} /> ); }) diff --git a/components/settings/AddSourceModal.tsx b/components/settings/AddSourceModal.tsx index 736f907..6e0a021 100644 --- a/components/settings/AddSourceModal.tsx +++ b/components/settings/AddSourceModal.tsx @@ -55,7 +55,7 @@ export function AddSourceModal({ isOpen, onClose, onAdd, existingIds, initialVal

- 选择之前导出的设置文件(JSON 格式)。支持新旧版本格式。 + 选择之前导出的设置文件(JSON 配置文件)。支持新旧版本格式。

- 支持 JSON 格式的单个或多个源配置链接 + 支持 JSON 配置文件格式的单个或多个源配置链接

{error && ( diff --git a/lib/hooks/useVideoPlayer.ts b/lib/hooks/useVideoPlayer.ts index 827d5c1..fc47e8f 100644 --- a/lib/hooks/useVideoPlayer.ts +++ b/lib/hooks/useVideoPlayer.ts @@ -85,7 +85,7 @@ export function useVideoPlayer( if (!response.ok) { if (response.status === 404) { - setVideoError(data.error || 'This video source is not available. Please go back and try another source.'); + setVideoError(data.error || '该视频源不可用。请返回并尝试其他来源。'); setLoading(false); return; } @@ -108,15 +108,15 @@ export function useVideoPlayer( setCurrentEpisode(validIndex); setPlayUrl(episodeUrl); } else { - setVideoError('No playable episodes available for this video from this source'); + setVideoError('该来源没有可播放的剧集'); setLoading(false); } } else { - throw new Error(data.error || 'Invalid response from API'); + throw new Error(data.error || '来自 API 的响应无效'); } } catch (error) { console.error('Failed to fetch video details:', error); - setVideoError(error instanceof Error ? error.message : 'Failed to load video details.'); + setVideoError(error instanceof Error ? error.message : '加载视频详情失败。'); setLoading(false); } }, [videoId, source]); diff --git a/lib/store/favorites-store.ts b/lib/store/favorites-store.ts index 298dd01..6f5e007 100644 --- a/lib/store/favorites-store.ts +++ b/lib/store/favorites-store.ts @@ -9,10 +9,11 @@ import type { FavoriteItem } from '@/lib/types'; const MAX_FAVORITES = 100; -interface FavoritesStore { +interface FavoritesState { favorites: FavoriteItem[]; +} - // Actions +interface FavoritesActions { addFavorite: (item: Omit) => void; removeFavorite: (videoId: string | number, source: string) => void; toggleFavorite: (item: Omit) => boolean; @@ -21,6 +22,8 @@ interface FavoritesStore { importFavorites: (favorites: FavoriteItem[]) => void; } +interface FavoritesStore extends FavoritesState, FavoritesActions { } + /** * Generate unique identifier for a favorite item */ @@ -31,84 +34,97 @@ function generateFavoriteId( return `${source}:${videoId}`; } -export const useFavoritesStore = create()( - persist( - (set, get) => ({ - favorites: [], +const createFavoritesStore = (name: string) => + create()( + persist( + (set, get) => ({ + favorites: [], - addFavorite: (item) => { - const favoriteId = generateFavoriteId(item.videoId, item.source); + addFavorite: (item) => { + const favoriteId = generateFavoriteId(item.videoId, item.source); - set((state) => { - // Check if already exists + set((state) => { + // Check if already exists + const exists = state.favorites.some( + (fav) => generateFavoriteId(fav.videoId, fav.source) === favoriteId + ); + + if (exists) { + return state; + } + + const newFavorite: FavoriteItem = { + ...item, + addedAt: Date.now(), + }; + + let newFavorites = [newFavorite, ...state.favorites]; + + // Limit favorites size + if (newFavorites.length > MAX_FAVORITES) { + newFavorites = newFavorites.slice(0, MAX_FAVORITES); + } + + return { favorites: newFavorites }; + }); + }, + + removeFavorite: (videoId, source) => { + const favoriteId = generateFavoriteId(videoId, source); + + set((state) => ({ + favorites: state.favorites.filter( + (fav) => generateFavoriteId(fav.videoId, fav.source) !== favoriteId + ), + })); + }, + + toggleFavorite: (item) => { + const state = get(); + const favoriteId = generateFavoriteId(item.videoId, item.source); const exists = state.favorites.some( (fav) => generateFavoriteId(fav.videoId, fav.source) === favoriteId ); if (exists) { - return state; + state.removeFavorite(item.videoId, item.source); + return false; + } else { + state.addFavorite(item); + return true; } + }, - const newFavorite: FavoriteItem = { - ...item, - addedAt: Date.now(), - }; + isFavorite: (videoId, source) => { + const state = get(); + const favoriteId = generateFavoriteId(videoId, source); + return state.favorites.some( + (fav) => generateFavoriteId(fav.videoId, fav.source) === favoriteId + ); + }, - let newFavorites = [newFavorite, ...state.favorites]; + clearFavorites: () => { + set({ favorites: [] }); + }, - // Limit favorites size - if (newFavorites.length > MAX_FAVORITES) { - newFavorites = newFavorites.slice(0, MAX_FAVORITES); - } + importFavorites: (favorites) => { + set({ favorites }); + }, + }), + { + name, + } + ) + ); - return { favorites: newFavorites }; - }); - }, +export const useFavoritesStore = createFavoritesStore('kvideo-favorites-store'); +export const useSecretFavoritesStore = createFavoritesStore('kvideo-secret-favorites-store'); - removeFavorite: (videoId, source) => { - const favoriteId = generateFavoriteId(videoId, source); - - set((state) => ({ - favorites: state.favorites.filter( - (fav) => generateFavoriteId(fav.videoId, fav.source) !== favoriteId - ), - })); - }, - - toggleFavorite: (item) => { - const state = get(); - const favoriteId = generateFavoriteId(item.videoId, item.source); - const exists = state.favorites.some( - (fav) => generateFavoriteId(fav.videoId, fav.source) === favoriteId - ); - - if (exists) { - state.removeFavorite(item.videoId, item.source); - return false; - } else { - state.addFavorite(item); - return true; - } - }, - - isFavorite: (videoId, source) => { - const state = get(); - const favoriteId = generateFavoriteId(videoId, source); - return state.favorites.some( - (fav) => generateFavoriteId(fav.videoId, fav.source) === favoriteId - ); - }, - - clearFavorites: () => { - set({ favorites: [] }); - }, - - importFavorites: (favorites) => { - set({ favorites }); - }, - }), - { - name: 'kvideo-favorites-store', - } - ) -); +/** + * Helper hook to get the appropriate favorites store + */ +export function useFavorites(isSecret = false) { + const normalStore = useFavoritesStore(); + const secretStore = useSecretFavoritesStore(); + return isSecret ? secretStore : normalStore; +} diff --git a/lib/store/history-store.ts b/lib/store/history-store.ts index 7b485bb..5038ef6 100644 --- a/lib/store/history-store.ts +++ b/lib/store/history-store.ts @@ -10,10 +10,11 @@ import { clearSegmentsForUrl, clearAllCache } from '@/lib/utils/cacheManager'; const MAX_HISTORY_ITEMS = 50; -interface HistoryStore { +interface HistoryState { viewingHistory: VideoHistoryItem[]; +} - // Actions +interface HistoryActions { addToHistory: ( videoId: string | number, title: string, @@ -31,6 +32,8 @@ interface HistoryStore { importHistory: (history: VideoHistoryItem[]) => void; } +interface HistoryStore extends HistoryState, HistoryActions { } + /** * Generate unique identifier for deduplication */ @@ -42,107 +45,120 @@ function generateShowIdentifier( return `${source}:${videoId}:${title.toLowerCase().trim()}`; } -export const useHistoryStore = create()( - persist( - (set, get) => ({ - viewingHistory: [], +const createHistoryStore = (name: string) => + create()( + persist( + (set, get) => ({ + viewingHistory: [], - addToHistory: ( - videoId, - title, - url, - episodeIndex, - source, - playbackPosition, - duration, - poster, - episodes = [] - ) => { - const showIdentifier = generateShowIdentifier(title, source, videoId); - const timestamp = Date.now(); + addToHistory: ( + videoId, + title, + url, + episodeIndex, + source, + playbackPosition, + duration, + poster, + episodes = [] + ) => { + const showIdentifier = generateShowIdentifier(title, source, videoId); + const timestamp = Date.now(); - set((state) => { - // Check if item already exists - const existingIndex = state.viewingHistory.findIndex( - (item) => item.showIdentifier === showIdentifier + set((state) => { + // Check if item already exists + const existingIndex = state.viewingHistory.findIndex( + (item) => item.showIdentifier === showIdentifier + ); + + let newHistory: VideoHistoryItem[]; + + if (existingIndex !== -1) { + // Update existing item and move to top + const updatedItem: VideoHistoryItem = { + ...state.viewingHistory[existingIndex], + url, + episodeIndex, + playbackPosition, + duration, + timestamp, + episodes: episodes.length > 0 ? episodes : state.viewingHistory[existingIndex].episodes, + }; + + newHistory = [ + updatedItem, + ...state.viewingHistory.filter((_, index) => index !== existingIndex), + ]; + } else { + // Add new item at the top + const newItem: VideoHistoryItem = { + videoId, + title, + url, + episodeIndex, + source, + timestamp, + playbackPosition, + duration, + poster, + episodes, + showIdentifier, + }; + + newHistory = [newItem, ...state.viewingHistory]; + } + + // Limit history size + if (newHistory.length > MAX_HISTORY_ITEMS) { + newHistory = newHistory.slice(0, MAX_HISTORY_ITEMS); + } + + return { viewingHistory: newHistory }; + }); + }, + + removeFromHistory: (videoId, source) => { + const state = get(); + const itemToRemove = state.viewingHistory.find( + (item) => item.videoId === videoId && item.source === source ); - let newHistory: VideoHistoryItem[]; - - if (existingIndex !== -1) { - // Update existing item and move to top - const updatedItem: VideoHistoryItem = { - ...state.viewingHistory[existingIndex], - url, - episodeIndex, - playbackPosition, - duration, - timestamp, - episodes: episodes.length > 0 ? episodes : state.viewingHistory[existingIndex].episodes, - }; - - newHistory = [ - updatedItem, - ...state.viewingHistory.filter((_, index) => index !== existingIndex), - ]; - } else { - // Add new item at the top - const newItem: VideoHistoryItem = { - videoId, - title, - url, - episodeIndex, - source, - timestamp, - playbackPosition, - duration, - poster, - episodes, - showIdentifier, - }; - - newHistory = [newItem, ...state.viewingHistory]; + if (itemToRemove) { + // Clear cache for this video + clearSegmentsForUrl(itemToRemove.url); } - // Limit history size - if (newHistory.length > MAX_HISTORY_ITEMS) { - newHistory = newHistory.slice(0, MAX_HISTORY_ITEMS); - } + set((state) => ({ + viewingHistory: state.viewingHistory.filter( + (item) => !(item.videoId === videoId && item.source === source) + ), + })); + }, - return { viewingHistory: newHistory }; - }); - }, + clearHistory: () => { + // Clear all cached segments + clearAllCache(); + set({ viewingHistory: [] }); + }, - removeFromHistory: (videoId, source) => { - const state = get(); - const itemToRemove = state.viewingHistory.find( - (item) => item.videoId === videoId && item.source === source - ); + importHistory: (history) => { + set({ viewingHistory: history }); + }, + }), + { + name, + } + ) + ); - if (itemToRemove) { - // Clear cache for this video - clearSegmentsForUrl(itemToRemove.url); - } +export const useHistoryStore = createHistoryStore('kvideo-history-store'); +export const useSecretHistoryStore = createHistoryStore('kvideo-secret-history-store'); - set((state) => ({ - viewingHistory: state.viewingHistory.filter( - (item) => !(item.videoId === videoId && item.source === source) - ), - })); - }, - - clearHistory: () => { - // Clear all cached segments - clearAllCache(); - set({ viewingHistory: [] }); - }, - - importHistory: (history) => { - set({ viewingHistory: history }); - }, - }), - { - name: 'kvideo-history-store', - } - ) -); +/** + * Helper hook to get the appropriate history store + */ +export function useHistory(isSecret = false) { + const normalStore = useHistoryStore(); + const secretStore = useSecretHistoryStore(); + return isSecret ? secretStore : normalStore; +} diff --git a/package-lock.json b/package-lock.json index 078d026..642b1a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kvideo", - "version": "3.8.8", + "version": "3.8.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kvideo", - "version": "3.8.8", + "version": "3.8.9", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", diff --git a/package.json b/package.json index 97f0824..5dde87f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kvideo", - "version": "3.8.8", + "version": "3.8.9", "private": true, "scripts": { "dev": "next dev",