mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-14 08:13:43 +08:00
feat: Implement customizable adult sources for secret mode with a dedicated settings page and improve settings persistence logic.
This commit is contained in:
+1
-1
@@ -24,7 +24,7 @@ function SecretHomePage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-black">
|
||||
{/* Glass Navbar */}
|
||||
<Navbar onReset={handleReset} />
|
||||
<Navbar onReset={handleReset} isSecretMode={true} />
|
||||
|
||||
{/* Search Form - Separate from navbar */}
|
||||
<div className="max-w-7xl mx-auto px-4 mt-6 mb-8 relative" style={{
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { settingsStore, getDefaultAdultSources, type SortOption } from '@/lib/store/settings-store';
|
||||
import type { VideoSource } from '@/lib/types';
|
||||
|
||||
export function useSecretSettingsPage() {
|
||||
const [adultSources, setAdultSources] = useState<VideoSource[]>([]);
|
||||
const [sortBy, setSortBy] = useState<SortOption>('default');
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
const [isRestoreDefaultsDialogOpen, setIsRestoreDefaultsDialogOpen] = useState(false);
|
||||
const [editingSource, setEditingSource] = useState<VideoSource | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const settings = settingsStore.getSettings();
|
||||
setAdultSources(settings.adultSources || []);
|
||||
setSortBy(settings.sortBy);
|
||||
}, []);
|
||||
|
||||
const handleSourcesChange = (newSources: VideoSource[]) => {
|
||||
setAdultSources(newSources);
|
||||
const currentSettings = settingsStore.getSettings();
|
||||
settingsStore.saveSettings({
|
||||
...currentSettings,
|
||||
adultSources: newSources,
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddSource = (source: VideoSource) => {
|
||||
const exists = adultSources.some(s => s.id === source.id);
|
||||
const updated = exists
|
||||
? adultSources.map(s => s.id === source.id ? source : s)
|
||||
: [...adultSources, source];
|
||||
handleSourcesChange(updated);
|
||||
setEditingSource(null);
|
||||
};
|
||||
|
||||
const handleEditSource = (source: VideoSource) => {
|
||||
setEditingSource(source);
|
||||
setIsAddModalOpen(true);
|
||||
};
|
||||
|
||||
const handleRestoreDefaults = () => {
|
||||
const defaults = getDefaultAdultSources();
|
||||
handleSourcesChange(defaults);
|
||||
setIsRestoreDefaultsDialogOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
adultSources,
|
||||
sortBy,
|
||||
isAddModalOpen,
|
||||
isRestoreDefaultsDialogOpen,
|
||||
setIsAddModalOpen,
|
||||
setIsRestoreDefaultsDialogOpen,
|
||||
setEditingSource,
|
||||
handleSourcesChange,
|
||||
handleAddSource,
|
||||
handleRestoreDefaults,
|
||||
editingSource,
|
||||
handleEditSource,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
'use client';
|
||||
|
||||
import { AddSourceModal } from '@/components/settings/AddSourceModal';
|
||||
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
|
||||
import { AdultSourceSettings } from '@/components/settings/AdultSourceSettings';
|
||||
import { SettingsHeader } from '@/components/settings/SettingsHeader';
|
||||
import { useSecretSettingsPage } from './hooks/useSecretSettingsPage';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function SecretSettingsPage() {
|
||||
const {
|
||||
adultSources,
|
||||
isAddModalOpen,
|
||||
isRestoreDefaultsDialogOpen,
|
||||
setIsAddModalOpen,
|
||||
setIsRestoreDefaultsDialogOpen,
|
||||
handleSourcesChange,
|
||||
handleAddSource,
|
||||
handleRestoreDefaults,
|
||||
editingSource,
|
||||
handleEditSource,
|
||||
setEditingSource,
|
||||
} = useSecretSettingsPage();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black">
|
||||
<div className="container mx-auto px-4 py-8 max-w-4xl space-y-8">
|
||||
{/* Custom Header for Secret Settings */}
|
||||
<div className="bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-sm)] p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/secret"
|
||||
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 cursor-pointer"
|
||||
aria-label="返回"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[var(--text-color)]">成人源设置</h1>
|
||||
<p className="text-sm text-[var(--text-color-secondary)]">管理成人内容来源</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Adult Source Management */}
|
||||
<AdultSourceSettings
|
||||
sources={adultSources}
|
||||
onSourcesChange={handleSourcesChange}
|
||||
onRestoreDefaults={() => setIsRestoreDefaultsDialogOpen(true)}
|
||||
onAddSource={() => {
|
||||
setEditingSource(null);
|
||||
setIsAddModalOpen(true);
|
||||
}}
|
||||
onEditSource={handleEditSource}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<AddSourceModal
|
||||
isOpen={isAddModalOpen}
|
||||
onClose={() => {
|
||||
setIsAddModalOpen(false);
|
||||
setEditingSource(null);
|
||||
}}
|
||||
onAdd={handleAddSource}
|
||||
existingIds={adultSources.map(s => s.id)}
|
||||
initialValues={editingSource}
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={isRestoreDefaultsDialogOpen}
|
||||
title="恢复默认成人源"
|
||||
message="这将重置所有成人源为默认配置。自定义源将被删除。是否继续?"
|
||||
confirmText="恢复"
|
||||
cancelText="取消"
|
||||
onConfirm={handleRestoreDefaults}
|
||||
onCancel={() => setIsRestoreDefaultsDialogOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -32,7 +32,9 @@ export function useSettingsPage() {
|
||||
|
||||
const handleSourcesChange = (newSources: VideoSource[]) => {
|
||||
setSources(newSources);
|
||||
const currentSettings = settingsStore.getSettings();
|
||||
settingsStore.saveSettings({
|
||||
...currentSettings,
|
||||
sources: newSources,
|
||||
sortBy,
|
||||
searchHistory: true,
|
||||
@@ -58,7 +60,9 @@ export function useSettingsPage() {
|
||||
|
||||
const handleSortChange = (newSort: SortOption) => {
|
||||
setSortBy(newSort);
|
||||
const currentSettings = settingsStore.getSettings();
|
||||
settingsStore.saveSettings({
|
||||
...currentSettings,
|
||||
sources,
|
||||
sortBy: newSort,
|
||||
searchHistory: true,
|
||||
@@ -70,7 +74,9 @@ export function useSettingsPage() {
|
||||
|
||||
const handlePasswordToggle = (enabled: boolean) => {
|
||||
setPasswordAccess(enabled);
|
||||
const currentSettings = settingsStore.getSettings();
|
||||
settingsStore.saveSettings({
|
||||
...currentSettings,
|
||||
sources,
|
||||
sortBy,
|
||||
searchHistory: true,
|
||||
@@ -83,7 +89,9 @@ export function useSettingsPage() {
|
||||
const handleAddPassword = (password: string) => {
|
||||
const updated = [...accessPasswords, password];
|
||||
setAccessPasswords(updated);
|
||||
const currentSettings = settingsStore.getSettings();
|
||||
settingsStore.saveSettings({
|
||||
...currentSettings,
|
||||
sources,
|
||||
sortBy,
|
||||
searchHistory: true,
|
||||
@@ -96,7 +104,9 @@ export function useSettingsPage() {
|
||||
const handleRemovePassword = (password: string) => {
|
||||
const updated = accessPasswords.filter(p => p !== password);
|
||||
setAccessPasswords(updated);
|
||||
const currentSettings = settingsStore.getSettings();
|
||||
settingsStore.saveSettings({
|
||||
...currentSettings,
|
||||
sources,
|
||||
sortBy,
|
||||
searchHistory: true,
|
||||
|
||||
@@ -5,9 +5,12 @@ import { Icons } from '@/components/ui/Icon';
|
||||
|
||||
interface NavbarProps {
|
||||
onReset: () => void;
|
||||
isSecretMode?: boolean;
|
||||
}
|
||||
|
||||
export function Navbar({ onReset }: NavbarProps) {
|
||||
export function Navbar({ onReset, isSecretMode = false }: NavbarProps) {
|
||||
const settingsHref = isSecretMode ? '/secret/settings' : '/settings';
|
||||
|
||||
return (
|
||||
<nav className="sticky top-0 z-[2000] pt-4 pb-2" style={{
|
||||
transform: 'translate3d(0, 0, 0)',
|
||||
@@ -19,7 +22,7 @@ export function Navbar({ onReset }: NavbarProps) {
|
||||
}}>
|
||||
<div className="flex items-center justify-between gap-2 sm:gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
href={isSecretMode ? '/secret' : '/'}
|
||||
className="flex items-center gap-2 sm:gap-3 hover:opacity-80 transition-opacity cursor-pointer min-w-0"
|
||||
onClick={onReset}
|
||||
>
|
||||
@@ -49,7 +52,7 @@ export function Navbar({ onReset }: NavbarProps) {
|
||||
<Icons.Github size={20} />
|
||||
</a>
|
||||
<Link
|
||||
href="/settings"
|
||||
href={settingsHref}
|
||||
className="w-8 h-8 sm:w-10 sm: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 cursor-pointer"
|
||||
aria-label="设置"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useState } from 'react';
|
||||
import { SourceManager } from '@/components/settings/SourceManager';
|
||||
import type { VideoSource } from '@/lib/types';
|
||||
import { ADULT_SOURCES } from '@/lib/api/adult-sources';
|
||||
|
||||
interface AdultSourceSettingsProps {
|
||||
sources: VideoSource[];
|
||||
onSourcesChange: (sources: VideoSource[]) => void;
|
||||
onRestoreDefaults: () => void;
|
||||
onAddSource: () => void;
|
||||
onEditSource?: (source: VideoSource) => void;
|
||||
}
|
||||
|
||||
export function AdultSourceSettings({
|
||||
sources,
|
||||
onSourcesChange,
|
||||
onRestoreDefaults,
|
||||
onAddSource,
|
||||
onEditSource,
|
||||
}: AdultSourceSettingsProps) {
|
||||
const [showAllSources, setShowAllSources] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const filteredSources = sources.filter(source =>
|
||||
source.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
source.baseUrl.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const displayedSources = showAllSources || searchQuery
|
||||
? filteredSources
|
||||
: filteredSources.slice(0, 10);
|
||||
|
||||
const handleToggle = (id: string) => {
|
||||
const updated = sources.map(s =>
|
||||
s.id === id ? { ...s, enabled: !s.enabled } : s
|
||||
);
|
||||
onSourcesChange(updated);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
const updated = sources.filter(s => s.id !== id);
|
||||
onSourcesChange(updated);
|
||||
};
|
||||
|
||||
const handleReorder = (id: string, direction: 'up' | 'down') => {
|
||||
const currentIndex = sources.findIndex(s => s.id === id);
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
const newIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
|
||||
if (newIndex < 0 || newIndex >= sources.length) return;
|
||||
|
||||
const updated = [...sources];
|
||||
[updated[currentIndex], updated[newIndex]] = [updated[newIndex], updated[currentIndex]];
|
||||
|
||||
// Update priorities
|
||||
updated.forEach((s, idx) => s.priority = idx + 1);
|
||||
onSourcesChange(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] shadow-[var(--shadow-sm)] p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-color)]">成人源管理</h2>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onRestoreDefaults}
|
||||
className="px-4 py-2 rounded-[var(--radius-2xl)] bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] text-sm font-medium hover:bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)] transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
恢复默认
|
||||
</button>
|
||||
<button
|
||||
onClick={onAddSource}
|
||||
className="px-4 py-2 rounded-[var(--radius-2xl)] bg-[var(--accent-color)] text-white text-sm font-semibold hover:brightness-110 hover:-translate-y-0.5 shadow-[var(--shadow-sm)] transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
+ 添加源
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-[var(--text-color-secondary)] mb-6">
|
||||
管理成人内容来源,调整优先级和启用状态
|
||||
</p>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="relative mb-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索源..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full px-4 py-2 pl-10 rounded-[var(--radius-2xl)] bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] placeholder-[var(--text-color-secondary)] focus:outline-none focus:ring-2 focus:ring-[var(--accent-color)] transition-all duration-200"
|
||||
/>
|
||||
<svg
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-color-secondary)]"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<SourceManager
|
||||
sources={displayedSources}
|
||||
onToggle={handleToggle}
|
||||
onDelete={handleDelete}
|
||||
onReorder={handleReorder}
|
||||
onEdit={onEditSource}
|
||||
defaultIds={ADULT_SOURCES.map(s => s.id)}
|
||||
/>
|
||||
{!searchQuery && sources.length > 10 && (
|
||||
<button
|
||||
onClick={() => setShowAllSources(!showAllSources)}
|
||||
className="w-full mt-4 px-4 py-3 rounded-[var(--radius-2xl)] bg-[var(--glass-bg)] border border-[var(--glass-border)] text-[var(--text-color)] text-sm font-medium hover:bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)] transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
{showAllSources ? '收起' : `显示全部 (${sources.length})`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useSearchCache } from '@/lib/hooks/useSearchCache';
|
||||
import { useParallelSearch } from '@/lib/hooks/useParallelSearch';
|
||||
import { ADULT_SOURCES } from '@/lib/api/adult-sources';
|
||||
import { settingsStore } from '@/lib/store/settings-store';
|
||||
|
||||
export function useSecretHomePage() {
|
||||
const router = useRouter();
|
||||
@@ -14,6 +14,12 @@ export function useSecretHomePage() {
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
const [currentSortBy, setCurrentSortBy] = useState('default');
|
||||
|
||||
// Get adult sources from settings store (supports user customization)
|
||||
const enabledAdultSources = useMemo(() => {
|
||||
const settings = settingsStore.getSettings();
|
||||
return settings.adultSources.filter(s => s.enabled);
|
||||
}, []);
|
||||
|
||||
// Search stream hook
|
||||
const {
|
||||
loading,
|
||||
@@ -58,8 +64,8 @@ export function useSecretHomePage() {
|
||||
const handleSearch = (searchQuery: string) => {
|
||||
setQuery(searchQuery);
|
||||
setHasSearched(true);
|
||||
// Always use ADULT_SOURCES
|
||||
performSearch(searchQuery, ADULT_SOURCES, currentSortBy as any);
|
||||
// Use enabled adult sources from settings
|
||||
performSearch(searchQuery, enabledAdultSources, currentSortBy as any);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import type { VideoSource } from '@/lib/types';
|
||||
import { DEFAULT_SOURCES } from '@/lib/api/default-sources';
|
||||
import { ADULT_SOURCES } from '@/lib/api/adult-sources';
|
||||
|
||||
export type SortOption =
|
||||
| 'default'
|
||||
@@ -17,6 +18,7 @@ export type SortOption =
|
||||
|
||||
export interface AppSettings {
|
||||
sources: VideoSource[];
|
||||
adultSources: VideoSource[];
|
||||
sortBy: SortOption;
|
||||
searchHistory: boolean;
|
||||
watchHistory: boolean;
|
||||
@@ -29,12 +31,14 @@ import { exportSettings, importSettings, SEARCH_HISTORY_KEY, WATCH_HISTORY_KEY }
|
||||
const SETTINGS_KEY = 'kvideo-settings';
|
||||
|
||||
export const getDefaultSources = (): VideoSource[] => DEFAULT_SOURCES;
|
||||
export const getDefaultAdultSources = (): VideoSource[] => ADULT_SOURCES;
|
||||
|
||||
export const settingsStore = {
|
||||
getSettings(): AppSettings {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
sources: getDefaultSources(),
|
||||
adultSources: getDefaultAdultSources(),
|
||||
sortBy: 'default',
|
||||
searchHistory: true,
|
||||
watchHistory: true,
|
||||
@@ -47,6 +51,7 @@ export const settingsStore = {
|
||||
if (!stored) {
|
||||
return {
|
||||
sources: getDefaultSources(),
|
||||
adultSources: getDefaultAdultSources(),
|
||||
sortBy: 'default',
|
||||
searchHistory: true,
|
||||
watchHistory: true,
|
||||
@@ -60,6 +65,7 @@ export const settingsStore = {
|
||||
// Validate that parsed data has all required properties
|
||||
return {
|
||||
sources: Array.isArray(parsed.sources) ? parsed.sources : getDefaultSources(),
|
||||
adultSources: Array.isArray(parsed.adultSources) ? parsed.adultSources : getDefaultAdultSources(),
|
||||
sortBy: parsed.sortBy || 'default',
|
||||
searchHistory: parsed.searchHistory !== undefined ? parsed.searchHistory : true,
|
||||
watchHistory: parsed.watchHistory !== undefined ? parsed.watchHistory : true,
|
||||
@@ -69,6 +75,7 @@ export const settingsStore = {
|
||||
} catch {
|
||||
return {
|
||||
sources: getDefaultSources(),
|
||||
adultSources: getDefaultAdultSources(),
|
||||
sortBy: 'default',
|
||||
searchHistory: true,
|
||||
watchHistory: true,
|
||||
|
||||
Reference in New Issue
Block a user