mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-15 08:43:44 +08:00
Refactor video loading state management; improve loading messages and optimize fetch logic in player component
This commit is contained in:
+125
-12
@@ -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<any[]>([]);
|
||||
@@ -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<AbortController | null>(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<string, string> = {
|
||||
'dytt': '电影天堂',
|
||||
@@ -347,7 +449,7 @@ export default function Home() {
|
||||
href={videoUrl}
|
||||
>
|
||||
<Card
|
||||
className={`p-0 overflow-hidden group cursor-pointer ${video.isNew ? 'animate-scale-in' : ''}`}
|
||||
className={`p-0 overflow-hidden group cursor-pointer flex flex-col h-full ${video.isNew ? 'animate-scale-in' : ''}`}
|
||||
>
|
||||
{/* Poster */}
|
||||
<div className="relative aspect-[2/3] bg-[color-mix(in_srgb,var(--glass-bg)_50%,transparent)] overflow-hidden" style={{ borderRadius: 'var(--radius-2xl) var(--radius-2xl) 0 0' }}>
|
||||
@@ -391,9 +493,9 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="p-3">
|
||||
<h4 className="font-semibold text-sm text-[var(--text-color)] line-clamp-2 group-hover:text-[var(--accent-color)] transition-colors">
|
||||
{/* Info - Fixed height section */}
|
||||
<div className="p-3 flex-1 flex flex-col">
|
||||
<h4 className="font-semibold text-sm text-[var(--text-color)] line-clamp-2 min-h-[2.5rem] group-hover:text-[var(--accent-color)] transition-colors">
|
||||
{video.vod_name}
|
||||
</h4>
|
||||
{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() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<Suspense fallback={<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-4 border-[var(--accent-color)] border-t-transparent"></div>
|
||||
</div>}>
|
||||
<HomePage />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-4
@@ -15,7 +15,7 @@ function PlayerContent() {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
const [videoData, setVideoData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentEpisode, setCurrentEpisode] = useState(0);
|
||||
const [playUrl, setPlayUrl] = useState('');
|
||||
const [videoError, setVideoError] = useState<string>('');
|
||||
@@ -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 ? (
|
||||
<div className="flex flex-col items-center justify-center py-20">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-4 border-[var(--accent-color)] border-t-transparent mb-4"></div>
|
||||
<p className="text-[var(--text-color-secondary)]">正在检测视频源可用性...</p>
|
||||
<p className="text-[var(--text-color-secondary)]">正在加载视频详情...</p>
|
||||
</div>
|
||||
) : videoError && !videoData ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
|
||||
Reference in New Issue
Block a user