mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-16 01:03:42 +08:00
feat: Introduce adult content browsing with dynamic tag management, aggregated categories, and a video content grid.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import { Tag } from '@/components/home/SortableTag';
|
||||
|
||||
// 成人模式默认标签配置
|
||||
// 现在完全由API动态获取,此处仅保留类型定义和Storage Key
|
||||
export const ADULT_DEFAULT_TAGS: Tag[] = [];
|
||||
|
||||
export const ADULT_STORAGE_KEY = 'kvideo_adult_custom_tags';
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useInfiniteScroll } from '@/lib/hooks/useInfiniteScroll';
|
||||
|
||||
interface AdultVideo {
|
||||
vod_id: string | number;
|
||||
vod_name: string;
|
||||
vod_pic?: string;
|
||||
vod_remarks?: string;
|
||||
type_name?: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
const PAGE_LIMIT = 20;
|
||||
|
||||
export function useAdultContent(categoryValue: string) {
|
||||
const [videos, setVideos] = useState<AdultVideo[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const loadVideos = useCallback(async (pageNum: number, append = false) => {
|
||||
if (loading) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/adult/category?category=${encodeURIComponent(categoryValue)}&page=${pageNum}&limit=${PAGE_LIMIT}`
|
||||
);
|
||||
|
||||
if (!response.ok) throw new Error('Failed to fetch');
|
||||
|
||||
const data = await response.json();
|
||||
const newVideos = data.videos || [];
|
||||
|
||||
setVideos(prev => append ? [...prev, ...newVideos] : newVideos);
|
||||
setHasMore(newVideos.length === PAGE_LIMIT);
|
||||
} catch (error) {
|
||||
console.error('Failed to load videos:', error);
|
||||
setHasMore(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [loading, categoryValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
setVideos([]);
|
||||
setHasMore(true);
|
||||
loadVideos(1, false);
|
||||
}, [categoryValue]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const { prefetchRef, loadMoreRef } = useInfiniteScroll({
|
||||
hasMore,
|
||||
loading,
|
||||
page,
|
||||
onLoadMore: (nextPage) => {
|
||||
setPage(nextPage);
|
||||
loadVideos(nextPage, true);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
videos,
|
||||
loading,
|
||||
hasMore,
|
||||
prefetchRef,
|
||||
loadMoreRef,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Tag } from '@/components/home/SortableTag';
|
||||
import { DragEndEvent } from '@dnd-kit/core';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
import { ADULT_STORAGE_KEY } from '@/lib/constants/adult-tags';
|
||||
|
||||
export function useAdultTagManager() {
|
||||
const [tags, setTags] = useState<Tag[]>([]);
|
||||
const [selectedTag, setSelectedTag] = useState('recommend');
|
||||
const [showTagManager, setShowTagManager] = useState(false);
|
||||
const [newTagInput, setNewTagInput] = useState('');
|
||||
const [justAddedTag, setJustAddedTag] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Fetch tags from API
|
||||
useEffect(() => {
|
||||
const fetchTags = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/adult/types');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.tags && Array.isArray(data.tags)) {
|
||||
// Load saved order from local storage
|
||||
const savedTagsJson = localStorage.getItem(ADULT_STORAGE_KEY);
|
||||
if (savedTagsJson) {
|
||||
try {
|
||||
const savedTags = JSON.parse(savedTagsJson);
|
||||
// Merge API tags with saved order
|
||||
// 1. Keep saved tags that still exist in API
|
||||
// 2. Add new API tags to the end
|
||||
const apiTagMap = new Map<string, Tag>();
|
||||
if (Array.isArray(data.tags)) {
|
||||
data.tags.forEach((t: Tag) => apiTagMap.set(t.id, t));
|
||||
}
|
||||
|
||||
const mergedTags: Tag[] = [];
|
||||
const processedIds = new Set<string>();
|
||||
|
||||
// Process saved tags
|
||||
if (Array.isArray(savedTags)) {
|
||||
savedTags.forEach((savedTag: Tag) => {
|
||||
if (apiTagMap.has(savedTag.id)) {
|
||||
mergedTags.push(apiTagMap.get(savedTag.id)!);
|
||||
processedIds.add(savedTag.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add remaining API tags
|
||||
data.tags.forEach((tag: Tag) => {
|
||||
if (!processedIds.has(tag.id)) {
|
||||
mergedTags.push(tag);
|
||||
}
|
||||
});
|
||||
|
||||
setTags(mergedTags);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse saved tags', e);
|
||||
setTags(data.tags);
|
||||
}
|
||||
} else {
|
||||
setTags(data.tags);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch adult tags:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTags();
|
||||
}, []);
|
||||
|
||||
// Save tags to local storage whenever they change
|
||||
useEffect(() => {
|
||||
if (tags.length > 0 && !loading) {
|
||||
localStorage.setItem(ADULT_STORAGE_KEY, JSON.stringify(tags));
|
||||
}
|
||||
}, [tags, loading]);
|
||||
|
||||
const handleAddTag = () => {
|
||||
// Custom tag adding is disabled for dynamic tags mode as we fetch all available tags
|
||||
// But we keep the function signature for compatibility
|
||||
};
|
||||
|
||||
const handleDeleteTag = (tagId: string) => {
|
||||
// Instead of deleting, we could hide it, but for now let's just remove it from the list
|
||||
// It will reappear if local storage is cleared or if we implement a "hidden tags" feature
|
||||
// For now, simple removal from current view
|
||||
const newTags = tags.filter((t) => t.id !== tagId);
|
||||
setTags(newTags);
|
||||
|
||||
if (selectedTag === tagId) {
|
||||
setSelectedTag(newTags[0]?.id || '');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestoreDefaults = async () => {
|
||||
setLoading(true);
|
||||
localStorage.removeItem(ADULT_STORAGE_KEY);
|
||||
try {
|
||||
const response = await fetch('/api/adult/types');
|
||||
const data = await response.json();
|
||||
if (data.tags) {
|
||||
setTags(data.tags);
|
||||
setSelectedTag('recommend');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to restore tags:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
setTags((items) => {
|
||||
const oldIndex = items.findIndex((item) => item.id === active.id);
|
||||
const newIndex = items.findIndex((item) => item.id === over.id);
|
||||
return arrayMove(items, oldIndex, newIndex);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
tags,
|
||||
selectedTag,
|
||||
newTagInput,
|
||||
showTagManager,
|
||||
justAddedTag,
|
||||
loading,
|
||||
setSelectedTag,
|
||||
setNewTagInput,
|
||||
setShowTagManager,
|
||||
setJustAddedTag,
|
||||
handleAddTag,
|
||||
handleDeleteTag,
|
||||
handleRestoreDefaults,
|
||||
handleDragEnd,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user