feat: centralize homepage logic into a new hook and refactor settings management with subscription and helper functions.

This commit is contained in:
kuekhaoyang
2025-11-23 11:41:48 +08:00
parent 9f189ff810
commit 5e9af1b89e
6 changed files with 196 additions and 114 deletions
+7 -65
View File
@@ -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">
+11 -1
View File
@@ -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>
+2 -1
View File
@@ -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;
+101
View File
@@ -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,
};
}
+56
View File
@@ -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
View File
@@ -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 {