From b23199e632a87cc9937477f7e8788e3d8a80a4ba Mon Sep 17 00:00:00 2001 From: kuekhaoyang Date: Sun, 16 Nov 2025 23:09:56 +0800 Subject: [PATCH] Refactor video loading state management; improve loading messages and optimize fetch logic in player component --- app/page.tsx | 137 ++++++++++++++++++++++++++++++++++++++++---- app/player/page.tsx | 8 +-- 2 files changed, 129 insertions(+), 16 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index be1cdd8..578835b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,7 @@ 'use client'; -import { useState, useRef } from 'react'; -import { useRouter } from 'next/navigation'; +import { useState, useRef, useEffect, Suspense } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { ThemeSwitcher } from '@/components/ThemeSwitcher'; import { Button } from '@/components/ui/Button'; @@ -12,7 +12,20 @@ import { Icons } from '@/components/ui/Icon'; import { SearchLoadingAnimation } from '@/components/SearchLoadingAnimation'; import Image from 'next/image'; -export default function Home() { +// Cache interface +interface SearchCache { + query: string; + results: any[]; + availableSources: any[]; + timestamp: number; +} + +const CACHE_KEY = 'kvideo_search_cache'; +const CACHE_DURATION = 10 * 60 * 1000; // 10 minutes + +function HomePage() { + const router = useRouter(); + const searchParams = useSearchParams(); const [loading, setLoading] = useState(false); const [query, setQuery] = useState(''); const [results, setResults] = useState([]); @@ -23,12 +36,85 @@ export default function Home() { const [searchStage, setSearchStage] = useState<'searching' | 'checking'>('searching'); const [checkedVideos, setCheckedVideos] = useState(0); const [totalVideos, setTotalVideos] = useState(0); - const router = useRouter(); const abortControllerRef = useRef(null); + const hasLoadedCache = useRef(false); - const handleSearch = async (e: React.FormEvent) => { - e.preventDefault(); - if (!query.trim() || loading) return; // Prevent multiple searches + // Load cached results on mount + useEffect(() => { + if (hasLoadedCache.current) return; + hasLoadedCache.current = true; + + const urlQuery = searchParams.get('q'); + + // Try to load from cache + const cached = loadFromCache(); + + if (urlQuery) { + // URL has query parameter + setQuery(urlQuery); + + if (cached && cached.query === urlQuery) { + // Use cached results if they match the URL query + console.log('📦 Loading cached results for:', urlQuery); + setResults(cached.results); + setAvailableSources(cached.availableSources); + setHasSearched(true); + } else { + // Trigger automatic search for URL query + console.log('🔍 Auto-searching for URL query:', urlQuery); + setTimeout(() => performSearch(urlQuery), 100); + } + } else if (cached) { + // No URL query but cache exists - restore last search + console.log('📦 Restoring last search from cache:', cached.query); + setQuery(cached.query); + setResults(cached.results); + setAvailableSources(cached.availableSources); + setHasSearched(true); + + // Update URL to reflect cached search + router.replace(`/?q=${encodeURIComponent(cached.query)}`, { scroll: false }); + } + }, [searchParams, router]); + + // Cache helper functions + const saveToCache = (searchQuery: string, searchResults: any[], sources: any[]) => { + const cache: SearchCache = { + query: searchQuery, + results: searchResults, + availableSources: sources, + timestamp: Date.now(), + }; + try { + localStorage.setItem(CACHE_KEY, JSON.stringify(cache)); + console.log('💾 Saved search to cache:', searchQuery, searchResults.length, 'results'); + } catch (error) { + console.error('Failed to save cache:', error); + } + }; + + const loadFromCache = (): SearchCache | null => { + try { + const cached = localStorage.getItem(CACHE_KEY); + if (!cached) return null; + + const cache: SearchCache = JSON.parse(cached); + + // Check if cache is still valid + if (Date.now() - cache.timestamp > CACHE_DURATION) { + localStorage.removeItem(CACHE_KEY); + return null; + } + + return cache; + } catch (error) { + console.error('Failed to load cache:', error); + return null; + } + }; + + const performSearch = async (searchQuery: string) => { + if (!searchQuery.trim() || loading) return; // Abort any previous search if (abortControllerRef.current) { @@ -47,6 +133,9 @@ export default function Home() { setCheckedVideos(0); setTotalVideos(0); + // Update URL with query parameter + router.replace(`/?q=${encodeURIComponent(searchQuery)}`, { scroll: false }); + try { // Get all enabled source IDs const sourceIds = ['dytt', 'ruyi', 'baofeng', 'tianya', 'feifan', @@ -57,7 +146,7 @@ export default function Home() { const response = await fetch('/api/search-stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ query, sources: sourceIds }), + body: JSON.stringify({ query: searchQuery, sources: sourceIds }), signal: abortControllerRef.current.signal, }); @@ -158,6 +247,14 @@ export default function Home() { case 'complete': setCheckedVideos(data.totalVideos); setLoading(false); + + // Save final results to cache + const finalSourcesArray = Array.from(sourceVideoCounts.entries()).map(([sourceId, count]) => ({ + id: sourceId, + name: getSourceName(sourceId), + count, + })); + saveToCache(searchQuery, allVideos, finalSourcesArray); break; case 'error': @@ -179,6 +276,11 @@ export default function Home() { } }; + const handleSearch = async (e: React.FormEvent) => { + e.preventDefault(); + await performSearch(query); + }; + const getSourceName = (sourceId: string): string => { const sourceNames: Record = { 'dytt': '电影天堂', @@ -347,7 +449,7 @@ export default function Home() { href={videoUrl} > {/* Poster */}
@@ -391,9 +493,9 @@ export default function Home() {
- {/* Info */} -
-

+ {/* Info - Fixed height section */} +
+

{video.vod_name}

{video.vod_remarks && ( @@ -469,6 +571,7 @@ export default function Home() { onClick={() => { setHasSearched(false); setQuery(''); + router.replace('/', { scroll: false }); }} > 返回首页 @@ -479,3 +582,13 @@ export default function Home() {
); } + +export default function Home() { + return ( + +
+

}> + + + ); +} diff --git a/app/player/page.tsx b/app/player/page.tsx index c3f8cd0..a222b18 100644 --- a/app/player/page.tsx +++ b/app/player/page.tsx @@ -15,7 +15,7 @@ function PlayerContent() { const videoRef = useRef(null); const [videoData, setVideoData] = useState(null); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); const [currentEpisode, setCurrentEpisode] = useState(0); const [playUrl, setPlayUrl] = useState(''); const [videoError, setVideoError] = useState(''); @@ -54,12 +54,12 @@ function PlayerContent() { return; } + setLoading(true); fetchVideoDetails(); }, [videoId, source]); const fetchVideoDetails = async () => { try { - setLoading(true); setVideoError(''); // Clear previous errors const response = await fetch(`/api/detail?id=${videoId}&source=${source}`); const data = await response.json(); @@ -85,6 +85,7 @@ function PlayerContent() { }); setVideoData(data.data); + setLoading(false); if (data.data.episodes && data.data.episodes.length > 0) { const firstUrl = data.data.episodes[0].url; console.log('Setting initial play URL:', firstUrl); @@ -100,7 +101,6 @@ function PlayerContent() { } catch (error) { console.error('Failed to fetch video details:', error); setVideoError(error instanceof Error ? error.message : 'Failed to load video details. Please try another source.'); - } finally { setLoading(false); } }; @@ -188,7 +188,7 @@ function PlayerContent() { {loading ? (
-

正在检测视频源可用性...

+

正在加载视频详情...

) : videoError && !videoData ? (