diff --git a/README.md b/README.md
index d219352..78f863d 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,17 @@
- **语义化 HTML**:使用语义化标签提升可访问性
- **高对比度**:确保 4.5:1 的文字对比度
+## 🔒 隐藏模式
+
+本项目包含一个隐藏的"成人模式",仅通过特定操作激活:
+
+1. 在首页点击"管理标签"
+2. 在输入框中输入 **"色情"** 并添加
+3. 点击新添加的 **"色情"** 标签
+4. 系统将自动跳转至隐藏模式页面
+
+> **注意**:隐藏模式下的内容源与主页完全隔离,互不干扰。
+
## 🛠 技术栈
### 前端核心
diff --git a/app/globals.css b/app/globals.css
index ad547f4..f338ec5 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -6,4 +6,13 @@
@import './styles/effects.css';
@import './styles/glass.css';
@import "./styles/video-player.css";
-@import "./styles/search-history.css";
\ No newline at end of file
+@import "./styles/search-history.css";
+@keyframes jiggle {
+ 0% { transform: rotate(-1deg); }
+ 50% { transform: rotate(1deg); }
+ 100% { transform: rotate(-1deg); }
+}
+
+.animate-jiggle {
+ animation: jiggle 0.2s infinite;
+}
diff --git a/app/secret/page.tsx b/app/secret/page.tsx
new file mode 100644
index 0000000..dbadeff
--- /dev/null
+++ b/app/secret/page.tsx
@@ -0,0 +1,81 @@
+'use client';
+
+import { Suspense } from 'react';
+import { SearchForm } from '@/components/search/SearchForm';
+import { NoResults } from '@/components/search/NoResults';
+import { Navbar } from '@/components/layout/Navbar';
+import { SearchResults } from '@/components/home/SearchResults';
+import { useSecretHomePage } from '@/lib/hooks/useSecretHomePage';
+
+function SecretHomePage() {
+ const {
+ query,
+ hasSearched,
+ loading,
+ results,
+ availableSources,
+ completedSources,
+ totalSources,
+ handleSearch,
+ handleReset,
+ } = useSecretHomePage();
+
+ return (
+
+ {/* Glass Navbar */}
+
+
+ {/* Search Form - Separate from navbar */}
+
+
+
+
+ {/* Main Content */}
+
+ {/* Results Section */}
+ {(results.length >= 1 || (!loading && results.length > 0)) && (
+
+ )}
+
+ {/* No Results */}
+ {!loading && hasSearched && results.length === 0 && (
+
+ )}
+
+ {/* Empty State - Just show a dark placeholder or nothing for secret mode */}
+ {!loading && !hasSearched && (
+
+ )}
+
+
+ );
+}
+
+export default function SecretPage() {
+ return (
+
+
+
+ }>
+
+
+ );
+}
diff --git a/components/home/PopularFeatures.tsx b/components/home/PopularFeatures.tsx
index e9b77f1..00ceb60 100644
--- a/components/home/PopularFeatures.tsx
+++ b/components/home/PopularFeatures.tsx
@@ -26,6 +26,7 @@ export function PopularFeatures({ onSearch }: PopularFeaturesProps) {
handleAddTag,
handleDeleteTag,
handleRestoreDefaults,
+ handleDragEnd,
} = useTagManager();
const {
@@ -49,12 +50,19 @@ export function PopularFeatures({ onSearch }: PopularFeaturesProps) {
selectedTag={selectedTag}
showTagManager={showTagManager}
newTagInput={newTagInput}
- onTagSelect={setSelectedTag}
+ onTagSelect={(tagId) => {
+ if (tagId === 'custom_色情' || tags.find(t => t.id === tagId)?.label === '色情') {
+ window.location.href = '/secret';
+ return;
+ }
+ setSelectedTag(tagId);
+ }}
onTagDelete={handleDeleteTag}
onToggleManager={() => setShowTagManager(!showTagManager)}
onRestoreDefaults={handleRestoreDefaults}
onNewTagInputChange={setNewTagInput}
onAddTag={handleAddTag}
+ onDragEnd={handleDragEnd}
/>
void;
onNewTagInputChange: (value: string) => void;
onAddTag: () => void;
+ onDragEnd: (event: DragEndEvent) => void;
+}
+
+function SortableTag({
+ tag,
+ selectedTag,
+ showTagManager,
+ onTagSelect,
+ onTagDelete,
+}: {
+ tag: Tag;
+ selectedTag: string;
+ showTagManager: boolean;
+ onTagSelect: (id: string) => void;
+ onTagDelete: (id: string) => void;
+}) {
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({ id: tag.id, disabled: !showTagManager });
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ zIndex: isDragging ? 10 : 1,
+ opacity: isDragging ? 0.3 : 1,
+ };
+
+ return (
+
+
+
+ {showTagManager && (
+
+ )}
+
+
+ );
}
export function TagManager({
@@ -37,8 +118,46 @@ export function TagManager({
onRestoreDefaults,
onNewTagInputChange,
onAddTag,
+ onDragEnd,
}: TagManagerProps) {
- const isCustomTag = (tagId: string) => tagId.startsWith('custom_');
+ const scrollContainerRef = useRef(null);
+ const prevTagsLength = useRef(tags.length);
+ const [activeId, setActiveId] = useState(null);
+
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 8,
+ },
+ }),
+ useSensor(KeyboardSensor, {
+ coordinateGetter: sortableKeyboardCoordinates,
+ })
+ );
+
+ // Auto-scroll to end when new tag is added
+ useEffect(() => {
+ if (tags.length > prevTagsLength.current) {
+ if (scrollContainerRef.current) {
+ scrollContainerRef.current.scrollTo({
+ left: scrollContainerRef.current.scrollWidth,
+ behavior: 'smooth',
+ });
+ }
+ }
+ prevTagsLength.current = tags.length;
+ }, [tags.length]);
+
+ const handleDragStart = (event: DragStartEvent) => {
+ setActiveId(event.active.id as string);
+ };
+
+ const handleDragEnd = (event: DragEndEvent) => {
+ setActiveId(null);
+ onDragEnd(event);
+ };
+
+ const activeTag = tags.find((t) => t.id === activeId);
return (
<>
@@ -83,35 +202,43 @@ export function TagManager({
)}
{/* Tag Filter */}
-
- {tags.map((tag) => (
-
-
- {showTagManager && isCustomTag(tag.id) && (
-
+
+ ) : null}
+
+
>
);
}
diff --git a/components/home/hooks/useTagManager.ts b/components/home/hooks/useTagManager.ts
index 2b5803b..751fdbc 100644
--- a/components/home/hooks/useTagManager.ts
+++ b/components/home/hooks/useTagManager.ts
@@ -1,4 +1,6 @@
import { useState, useEffect } from 'react';
+import { DragEndEvent } from '@dnd-kit/core';
+import { arrayMove } from '@dnd-kit/sortable';
const DEFAULT_TAGS = [
{ id: 'popular', label: '热门', value: '热门' },
@@ -69,6 +71,16 @@ export function useTagManager() {
setShowTagManager(false);
};
+ const handleDragEnd = (event: DragEndEvent) => {
+ const { active, over } = event;
+
+ if (over && active.id !== over.id) {
+ const oldIndex = tags.findIndex((tag) => tag.id === active.id);
+ const newIndex = tags.findIndex((tag) => tag.id === over.id);
+ saveTags(arrayMove(tags, oldIndex, newIndex));
+ }
+ };
+
return {
tags,
selectedTag,
@@ -80,5 +92,6 @@ export function useTagManager() {
handleAddTag,
handleDeleteTag,
handleRestoreDefaults,
+ handleDragEnd,
};
}
diff --git a/lib/api/adult-sources.ts b/lib/api/adult-sources.ts
new file mode 100644
index 0000000..8d2386b
--- /dev/null
+++ b/lib/api/adult-sources.ts
@@ -0,0 +1,67 @@
+import type { VideoSource } from '@/lib/types';
+
+export const ADULT_SOURCES: VideoSource[] = [
+ {
+ id: 'ck',
+ name: 'CK',
+ baseUrl: 'https://www.ckzy1.com/api.php/provide/vod',
+ searchPath: '',
+ detailPath: '',
+ enabled: true,
+ priority: 1
+ },
+ {
+ id: 'jkun',
+ name: 'jkun',
+ baseUrl: 'https://jkunzyapi.com/api.php/provide/vod',
+ searchPath: '',
+ detailPath: '',
+ enabled: true,
+ priority: 2
+ },
+ {
+ id: 'souav',
+ name: 'souav',
+ baseUrl: 'https://api.souavzy.vip/api.php/provide/vod',
+ searchPath: '',
+ detailPath: '',
+ enabled: true,
+ priority: 3
+ },
+ {
+ id: '155',
+ name: '155',
+ baseUrl: 'https://155api.com/api.php/provide/vod',
+ searchPath: '',
+ detailPath: '',
+ enabled: true,
+ priority: 4
+ },
+ {
+ id: 'lsb',
+ name: 'lsb',
+ baseUrl: 'https://apilsbzy1.com/api.php/provide/vod',
+ searchPath: '',
+ detailPath: '',
+ enabled: true,
+ priority: 5
+ },
+ {
+ id: 'hsck',
+ name: '黄色仓库',
+ baseUrl: 'https://hsckzy.vip/api.php/provide/vod',
+ searchPath: '',
+ detailPath: '',
+ enabled: true,
+ priority: 6
+ },
+ {
+ id: 'yutu',
+ name: '玉兔',
+ baseUrl: 'https://yutuzy10.com/api.php/provide/vod',
+ searchPath: '',
+ detailPath: '',
+ enabled: true,
+ priority: 7
+ }
+];
diff --git a/lib/hooks/useSecretHomePage.ts b/lib/hooks/useSecretHomePage.ts
new file mode 100644
index 0000000..339647e
--- /dev/null
+++ b/lib/hooks/useSecretHomePage.ts
@@ -0,0 +1,83 @@
+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 { ADULT_SOURCES } from '@/lib/api/adult-sources';
+
+export function useSecretHomePage() {
+ 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(`/secret?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 cached results on mount
+ useEffect(() => {
+ if (hasLoadedCache.current) return;
+ hasLoadedCache.current = true;
+
+ const urlQuery = searchParams.get('q');
+ // Note: We might want to separate cache for secret mode, but for now sharing or not using cache might be safer.
+ // However, useSearchCache uses localStorage which is shared.
+ // If we want to avoid leaking secret searches to normal history, we might want to disable cache or use a different key.
+ // For simplicity and "hidden" nature, maybe we don't load cache from normal mode?
+ // But the user asked for "same as original page".
+
+ if (urlQuery) {
+ setQuery(urlQuery);
+ handleSearch(urlQuery);
+ }
+ }, [searchParams]);
+
+ const handleSearch = (searchQuery: string) => {
+ setQuery(searchQuery);
+ setHasSearched(true);
+ // Always use ADULT_SOURCES
+ performSearch(searchQuery, ADULT_SOURCES, currentSortBy as any);
+ };
+
+ const handleReset = () => {
+ setHasSearched(false);
+ setQuery('');
+ resetSearch();
+ router.replace('/secret', { scroll: false });
+ };
+
+ return {
+ query,
+ hasSearched,
+ loading,
+ results,
+ availableSources,
+ completedSources,
+ totalSources,
+ handleSearch,
+ handleReset,
+ };
+}
diff --git a/package-lock.json b/package-lock.json
index a2272d2..6bf01e4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,9 @@
"name": "kvideo",
"version": "0.1.0",
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
"@vercel/analytics": "^1.5.0",
"next": "16.0.3",
"react": "19.2.0",
@@ -279,6 +282,59 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@dnd-kit/accessibility": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
+ "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/core": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
+ "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@dnd-kit/accessibility": "^3.1.1",
+ "@dnd-kit/utilities": "^3.2.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/sortable": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
+ "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
+ "license": "MIT",
+ "dependencies": {
+ "@dnd-kit/utilities": "^3.2.2",
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "@dnd-kit/core": "^6.3.0",
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@dnd-kit/utilities": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
+ "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
diff --git a/package.json b/package.json
index dd61d42..4fcf25b 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,9 @@
"lint": "eslint"
},
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
"@vercel/analytics": "^1.5.0",
"next": "16.0.3",
"react": "19.2.0",