mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-16 09:13:42 +08:00
feat: centralize homepage logic into a new hook and refactor settings management with subscription and helper functions.
This commit is contained in:
+7
-65
@@ -1,84 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect, Suspense } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Suspense } from 'react';
|
||||
import { SearchForm } from '@/components/search/SearchForm';
|
||||
import { NoResults } from '@/components/search/NoResults';
|
||||
import { PopularFeatures } from '@/components/home/PopularFeatures';
|
||||
import { WatchHistorySidebar } from '@/components/history/WatchHistorySidebar';
|
||||
import { useSearchCache } from '@/lib/hooks/useSearchCache';
|
||||
import { useParallelSearch } from '@/lib/hooks/useParallelSearch';
|
||||
import { settingsStore } from '@/lib/store/settings-store';
|
||||
import { Navbar } from '@/components/layout/Navbar';
|
||||
import { SearchResults } from '@/components/home/SearchResults';
|
||||
import { useHomePage } from '@/lib/hooks/useHomePage';
|
||||
|
||||
function HomePage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { loadFromCache, saveToCache } = useSearchCache();
|
||||
const hasLoadedCache = useRef(false);
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
const [currentSortBy, setCurrentSortBy] = useState('default');
|
||||
|
||||
// Search stream hook
|
||||
const {
|
||||
query,
|
||||
hasSearched,
|
||||
loading,
|
||||
results,
|
||||
availableSources,
|
||||
completedSources,
|
||||
totalSources,
|
||||
performSearch,
|
||||
resetSearch,
|
||||
loadCachedResults,
|
||||
} = useParallelSearch(
|
||||
saveToCache,
|
||||
(q: string) => router.replace(`/?q=${encodeURIComponent(q)}`, { scroll: false })
|
||||
);
|
||||
|
||||
// Load sort preference on mount
|
||||
useEffect(() => {
|
||||
const settings = settingsStore.getSettings();
|
||||
setCurrentSortBy(settings.sortBy);
|
||||
}, []);
|
||||
|
||||
// Load cached results on mount
|
||||
useEffect(() => {
|
||||
if (hasLoadedCache.current) return;
|
||||
hasLoadedCache.current = true;
|
||||
|
||||
const urlQuery = searchParams.get('q');
|
||||
const cached = loadFromCache();
|
||||
|
||||
if (urlQuery) {
|
||||
setQuery(urlQuery);
|
||||
if (cached && cached.query === urlQuery && cached.results.length > 0) {
|
||||
|
||||
setHasSearched(true);
|
||||
loadCachedResults(cached.results, cached.availableSources);
|
||||
} else {
|
||||
|
||||
setTimeout(() => handleSearch(urlQuery), 100);
|
||||
}
|
||||
}
|
||||
}, [searchParams, loadFromCache, loadCachedResults]);
|
||||
|
||||
const handleSearch = (searchQuery: string) => {
|
||||
setQuery(searchQuery);
|
||||
setHasSearched(true);
|
||||
const settings = settingsStore.getSettings();
|
||||
// Filter enabled sources
|
||||
const enabledSources = settings.sources.filter(s => s.enabled);
|
||||
performSearch(searchQuery, enabledSources, currentSortBy as any);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setHasSearched(false);
|
||||
setQuery('');
|
||||
resetSearch();
|
||||
router.replace('/', { scroll: false });
|
||||
};
|
||||
handleSearch,
|
||||
handleReset,
|
||||
} = useHomePage();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { ThemeSwitcher } from '@/components/ThemeSwitcher';
|
||||
@@ -34,7 +35,16 @@ export function PlayerNavbar() {
|
||||
<span className="hidden sm:inline">返回</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/settings"
|
||||
className="w-10 h-10 flex items-center justify-center rounded-[var(--radius-full)] bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] hover:bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)] transition-all duration-200"
|
||||
aria-label="设置"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 -960 960 960" fill="currentColor">
|
||||
<path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z" />
|
||||
</svg>
|
||||
</Link>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sortOptions, type SortOption } from '@/lib/store/settings-store';
|
||||
import { type SortOption } from '@/lib/store/settings-store';
|
||||
import { sortOptions } from '@/lib/store/settings-helpers';
|
||||
|
||||
interface SortSettingsProps {
|
||||
sortBy: SortOption;
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useSearchCache } from '@/lib/hooks/useSearchCache';
|
||||
import { useParallelSearch } from '@/lib/hooks/useParallelSearch';
|
||||
import { settingsStore } from '@/lib/store/settings-store';
|
||||
|
||||
export function useHomePage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { loadFromCache, saveToCache } = useSearchCache();
|
||||
const hasLoadedCache = useRef(false);
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
const [currentSortBy, setCurrentSortBy] = useState('default');
|
||||
|
||||
// Search stream hook
|
||||
const {
|
||||
loading,
|
||||
results,
|
||||
availableSources,
|
||||
completedSources,
|
||||
totalSources,
|
||||
performSearch,
|
||||
resetSearch,
|
||||
loadCachedResults,
|
||||
applySorting,
|
||||
} = useParallelSearch(
|
||||
saveToCache,
|
||||
(q: string) => router.replace(`/?q=${encodeURIComponent(q)}`, { scroll: false })
|
||||
);
|
||||
|
||||
// Re-sort results when sort preference changes
|
||||
useEffect(() => {
|
||||
if (hasSearched && results.length > 0) {
|
||||
applySorting(currentSortBy as any);
|
||||
}
|
||||
}, [currentSortBy, applySorting, hasSearched, results.length]);
|
||||
|
||||
// Load sort preference on mount and subscribe to changes
|
||||
useEffect(() => {
|
||||
const updateSettings = () => {
|
||||
const settings = settingsStore.getSettings();
|
||||
setCurrentSortBy(settings.sortBy);
|
||||
};
|
||||
|
||||
// Initial load
|
||||
updateSettings();
|
||||
|
||||
// Subscribe to changes
|
||||
const unsubscribe = settingsStore.subscribe(updateSettings);
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
// Load cached results on mount
|
||||
useEffect(() => {
|
||||
if (hasLoadedCache.current) return;
|
||||
hasLoadedCache.current = true;
|
||||
|
||||
const urlQuery = searchParams.get('q');
|
||||
const cached = loadFromCache();
|
||||
|
||||
if (urlQuery) {
|
||||
setQuery(urlQuery);
|
||||
if (cached && cached.query === urlQuery && cached.results.length > 0) {
|
||||
setHasSearched(true);
|
||||
loadCachedResults(cached.results, cached.availableSources);
|
||||
} else {
|
||||
setTimeout(() => handleSearch(urlQuery), 100);
|
||||
}
|
||||
}
|
||||
}, [searchParams, loadFromCache, loadCachedResults]);
|
||||
|
||||
const handleSearch = (searchQuery: string) => {
|
||||
setQuery(searchQuery);
|
||||
setHasSearched(true);
|
||||
const settings = settingsStore.getSettings();
|
||||
// Filter enabled sources
|
||||
const enabledSources = settings.sources.filter(s => s.enabled);
|
||||
performSearch(searchQuery, enabledSources, currentSortBy as any);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setHasSearched(false);
|
||||
setQuery('');
|
||||
resetSearch();
|
||||
router.replace('/', { scroll: false });
|
||||
};
|
||||
|
||||
return {
|
||||
query,
|
||||
hasSearched,
|
||||
loading,
|
||||
results,
|
||||
availableSources,
|
||||
completedSources,
|
||||
totalSources,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { AppSettings } from './settings-store';
|
||||
|
||||
export const SEARCH_HISTORY_KEY = 'kvideo-search-history';
|
||||
export const WATCH_HISTORY_KEY = 'kvideo-watch-history';
|
||||
|
||||
export const sortOptions = {
|
||||
'default': '默认排序',
|
||||
'relevance': '按相关性',
|
||||
'latency-asc': '延迟低到高',
|
||||
'date-desc': '发布时间(新到旧)',
|
||||
'date-asc': '发布时间(旧到新)',
|
||||
'rating-desc': '按评分(高到低)',
|
||||
'name-asc': '按名称(A-Z)',
|
||||
'name-desc': '按名称(Z-A)',
|
||||
} as const;
|
||||
|
||||
export function exportSettings(settings: AppSettings, includeHistory: boolean = true): string {
|
||||
const exportData: Record<string, unknown> = {
|
||||
settings,
|
||||
};
|
||||
|
||||
if (includeHistory && typeof window !== 'undefined') {
|
||||
const searchHistory = localStorage.getItem(SEARCH_HISTORY_KEY);
|
||||
const watchHistory = localStorage.getItem(WATCH_HISTORY_KEY);
|
||||
|
||||
if (searchHistory) exportData.searchHistory = JSON.parse(searchHistory);
|
||||
if (watchHistory) exportData.watchHistory = JSON.parse(watchHistory);
|
||||
}
|
||||
|
||||
return JSON.stringify(exportData, null, 2);
|
||||
}
|
||||
|
||||
export function importSettings(
|
||||
jsonString: string,
|
||||
saveSettings: (settings: AppSettings) => void
|
||||
): boolean {
|
||||
try {
|
||||
const data = JSON.parse(jsonString);
|
||||
|
||||
if (data.settings) {
|
||||
saveSettings(data.settings);
|
||||
}
|
||||
|
||||
if (data.searchHistory && typeof window !== 'undefined') {
|
||||
localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(data.searchHistory));
|
||||
}
|
||||
|
||||
if (data.watchHistory && typeof window !== 'undefined') {
|
||||
localStorage.setItem(WATCH_HISTORY_KEY, JSON.stringify(data.watchHistory));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+19
-47
@@ -15,27 +15,16 @@ export type SortOption =
|
||||
| 'name-asc'
|
||||
| 'name-desc';
|
||||
|
||||
interface AppSettings {
|
||||
export interface AppSettings {
|
||||
sources: VideoSource[];
|
||||
sortBy: SortOption;
|
||||
searchHistory: boolean;
|
||||
watchHistory: boolean;
|
||||
}
|
||||
|
||||
const SETTINGS_KEY = 'kvideo-settings';
|
||||
const SEARCH_HISTORY_KEY = 'kvideo-search-history';
|
||||
const WATCH_HISTORY_KEY = 'kvideo-watch-history';
|
||||
import { exportSettings, importSettings, SEARCH_HISTORY_KEY, WATCH_HISTORY_KEY } from './settings-helpers';
|
||||
|
||||
export const sortOptions: Record<SortOption, string> = {
|
||||
'default': '默认排序',
|
||||
'relevance': '按相关性',
|
||||
'latency-asc': '延迟低到高',
|
||||
'date-desc': '发布时间(新到旧)',
|
||||
'date-asc': '发布时间(旧到新)',
|
||||
'rating-desc': '按评分(高到低)',
|
||||
'name-asc': '按名称(A-Z)',
|
||||
'name-desc': '按名称(Z-A)',
|
||||
};
|
||||
const SETTINGS_KEY = 'kvideo-settings';
|
||||
|
||||
export const getDefaultSources = (): VideoSource[] => DEFAULT_SOURCES;
|
||||
|
||||
@@ -79,49 +68,32 @@ export const settingsStore = {
|
||||
}
|
||||
},
|
||||
|
||||
listeners: new Set<() => void>(),
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
},
|
||||
|
||||
notifyListeners(): void {
|
||||
this.listeners.forEach((listener) => listener());
|
||||
},
|
||||
|
||||
saveSettings(settings: AppSettings): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
||||
this.notifyListeners();
|
||||
}
|
||||
},
|
||||
|
||||
exportSettings(includeHistory: boolean = true): string {
|
||||
const settings = this.getSettings();
|
||||
const exportData: Record<string, unknown> = {
|
||||
settings,
|
||||
};
|
||||
|
||||
if (includeHistory && typeof window !== 'undefined') {
|
||||
const searchHistory = localStorage.getItem(SEARCH_HISTORY_KEY);
|
||||
const watchHistory = localStorage.getItem(WATCH_HISTORY_KEY);
|
||||
|
||||
if (searchHistory) exportData.searchHistory = JSON.parse(searchHistory);
|
||||
if (watchHistory) exportData.watchHistory = JSON.parse(watchHistory);
|
||||
}
|
||||
|
||||
return JSON.stringify(exportData, null, 2);
|
||||
return exportSettings(this.getSettings(), includeHistory);
|
||||
},
|
||||
|
||||
importSettings(jsonString: string): boolean {
|
||||
try {
|
||||
const data = JSON.parse(jsonString);
|
||||
|
||||
if (data.settings) {
|
||||
this.saveSettings(data.settings);
|
||||
}
|
||||
|
||||
if (data.searchHistory && typeof window !== 'undefined') {
|
||||
localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(data.searchHistory));
|
||||
}
|
||||
|
||||
if (data.watchHistory && typeof window !== 'undefined') {
|
||||
localStorage.setItem(WATCH_HISTORY_KEY, JSON.stringify(data.watchHistory));
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return importSettings(jsonString, (s) => this.saveSettings(s));
|
||||
},
|
||||
|
||||
resetToDefaults(): void {
|
||||
|
||||
Reference in New Issue
Block a user