diff --git a/src/app/api/emby/list/route.ts b/src/app/api/emby/list/route.ts index 2f45bf6..a8a41fa 100644 --- a/src/app/api/emby/list/route.ts +++ b/src/app/api/emby/list/route.ts @@ -13,12 +13,19 @@ export async function GET(request: NextRequest) { const pageSize = parseInt(searchParams.get('pageSize') || '20'); const parentId = searchParams.get('parentId') || undefined; const embyKey = searchParams.get('embyKey') || undefined; + const sortBy = searchParams.get('sortBy') || 'SortName'; + const sortOrder = searchParams.get('sortOrder') || 'Ascending'; try { - // 检查缓存 - const cached = getCachedEmbyList(page, pageSize, parentId, embyKey); - if (cached) { - return NextResponse.json(cached); + // 判断是否是默认排序(只有默认排序才使用缓存) + const isDefaultSort = sortBy === 'SortName' && sortOrder === 'Ascending'; + + // 只有默认排序才检查缓存 + if (isDefaultSort) { + const cached = getCachedEmbyList(page, pageSize, parentId, embyKey); + if (cached) { + return NextResponse.json(cached); + } } // 获取Emby客户端 @@ -30,8 +37,8 @@ export async function GET(request: NextRequest) { IncludeItemTypes: 'Movie,Series', Recursive: true, Fields: 'Overview,ProductionYear', - SortBy: 'SortName', - SortOrder: 'Ascending', + SortBy: sortBy, + SortOrder: sortOrder, StartIndex: (page - 1) * pageSize, Limit: pageSize, }); @@ -55,8 +62,10 @@ export async function GET(request: NextRequest) { total: result.TotalRecordCount, }; - // 缓存结果 - setCachedEmbyList(page, pageSize, response, parentId, embyKey); + // 只有默认排序才缓存结果 + if (isDefaultSort) { + setCachedEmbyList(page, pageSize, response, parentId, embyKey); + } return NextResponse.json(response); } catch (error) { diff --git a/src/app/private-library/page.tsx b/src/app/private-library/page.tsx index 2f36979..3854e5d 100644 --- a/src/app/private-library/page.tsx +++ b/src/app/private-library/page.tsx @@ -4,7 +4,8 @@ import { useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useState, useRef, useMemo } from 'react'; -import { Film } from 'lucide-react'; +import { Film, ArrowUpDown, ArrowDownWideNarrow, ArrowUpNarrowWide } from 'lucide-react'; +import { createPortal } from 'react-dom'; import CapsuleSwitch from '@/components/CapsuleSwitch'; import PageLayout from '@/components/PageLayout'; @@ -74,6 +75,13 @@ export default function PrivateLibraryPage() { const [embyViews, setEmbyViews] = useState([]); const [selectedView, setSelectedView] = useState('all'); const [loadingViews, setLoadingViews] = useState(false); + // Emby排序状态 + const [sortBy, setSortBy] = useState('SortName'); + const [sortOrder, setSortOrder] = useState<'Ascending' | 'Descending'>('Ascending'); + const [showSortDropdown, setShowSortDropdown] = useState(false); + const [sortDropdownPosition, setSortDropdownPosition] = useState<{ x: number; y: number; width: number }>({ x: 0, y: 0, width: 0 }); + const sortButtonRef = useRef(null); + const sortDropdownRef = useRef(null); // 小雅相关状态 const [xiaoyaPath, setXiaoyaPath] = useState('/'); const [xiaoyaFolders, setXiaoyaFolders] = useState>([]); @@ -217,6 +225,20 @@ export default function PrivateLibraryPage() { isFetchingRef.current = false; }, [selectedView]); + // 切换排序时重置状态(但不在初始化时执行) + useEffect(() => { + if (!isInitializedRef.current) return; + if (sourceType !== 'emby') return; + + setPage(1); + setVideos([]); + setHasMore(true); + setError(''); + setLoading(false); + setLoadingMore(false); + isFetchingRef.current = false; + }, [sortBy, sortOrder, sourceType]); + // 获取 Emby 媒体库列表 useEffect(() => { if (sourceType !== 'emby' || !embyKey) return; @@ -290,6 +312,101 @@ export default function PrivateLibraryPage() { scrollContainerRef.current.scrollLeft = scrollLeftRef.current - walk; }; + // 排序相关函数 + const sortOptions = [ + { label: '名称', value: 'SortName' }, + { label: '加入时间', value: 'DateCreated' }, + { label: '发行日期', value: 'PremiereDate' }, + { label: '年份', value: 'ProductionYear' }, + { label: '评分', value: 'CommunityRating' }, + ]; + + const getSortDisplayText = () => { + const option = sortOptions.find((opt) => opt.value === sortBy); + return option?.label || '排序'; + }; + + const isDefaultSort = () => { + return sortBy === 'SortName' && sortOrder === 'Ascending'; + }; + + const calculateSortDropdownPosition = () => { + const element = sortButtonRef.current; + if (element) { + const rect = element.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const isMobile = viewportWidth < 768; + + let x = rect.left; + const minWidth = 200; + let dropdownWidth = Math.max(rect.width, minWidth); + let useFixedWidth = false; + + if (isMobile) { + const padding = 16; + const maxWidth = viewportWidth - padding * 2; + dropdownWidth = Math.min(dropdownWidth, maxWidth); + useFixedWidth = true; + + if (x + dropdownWidth > viewportWidth - padding) { + x = viewportWidth - dropdownWidth - padding; + } + if (x < padding) { + x = padding; + } + } + + setSortDropdownPosition({ x, y: rect.bottom + 4, width: useFixedWidth ? dropdownWidth : rect.width }); + } + }; + + const handleSortButtonClick = () => { + if (showSortDropdown) { + setShowSortDropdown(false); + } else { + setShowSortDropdown(true); + calculateSortDropdownPosition(); + } + }; + + const handleSortOptionSelect = (value: string) => { + setSortBy(value); + setShowSortDropdown(false); + }; + + const toggleSortOrder = () => { + setSortOrder(sortOrder === 'Ascending' ? 'Descending' : 'Ascending'); + }; + + // 点击外部关闭排序下拉框 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + sortDropdownRef.current && + !sortDropdownRef.current.contains(event.target as Node) && + sortButtonRef.current && + !sortButtonRef.current.contains(event.target as Node) + ) { + setShowSortDropdown(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + // 滚动时关闭排序下拉框 + useEffect(() => { + const handleScroll = () => { + if (showSortDropdown) { + setShowSortDropdown(false); + } + }; + document.body.addEventListener('scroll', handleScroll, { passive: true }); + return () => { + document.body.removeEventListener('scroll', handleScroll); + }; + }, [showSortDropdown]); + // 加载数据的函数 useEffect(() => { const fetchVideos = async () => { @@ -335,7 +452,7 @@ export default function PrivateLibraryPage() { ? `/api/openlist/list?page=${page}&pageSize=${pageSize}` : sourceType === 'xiaoya' ? `/api/xiaoya/browse?path=${encodeURIComponent(xiaoyaPath)}` - : `/api/emby/list?page=${page}&pageSize=${pageSize}${selectedView !== 'all' ? `&parentId=${selectedView}` : ''}&embyKey=${embyKey}`; + : `/api/emby/list?page=${page}&pageSize=${pageSize}${selectedView !== 'all' ? `&parentId=${selectedView}` : ''}&embyKey=${embyKey}&sortBy=${sortBy}&sortOrder=${sortOrder}`; const response = await fetch(endpoint, { signal: abortController.signal }); @@ -404,7 +521,7 @@ export default function PrivateLibraryPage() { abortControllerRef.current.abort(); } }; - }, [sourceType, embyKey, page, selectedView, xiaoyaPath, runtimeConfig]); + }, [sourceType, embyKey, page, selectedView, xiaoyaPath, runtimeConfig, sortBy, sortOrder]); const handleVideoClick = (video: Video) => { // 构建source参数 @@ -592,6 +709,99 @@ export default function PrivateLibraryPage() { )} + {/* Emby 排序选择器 */} + {sourceType === 'emby' && ( +
+
+ 排序 +
+
+
+ {/* 排序字段选择 */} +
+ +
+ + {/* 排序方向切换 */} +
+ +
+
+
+
+ )} + + {/* 排序下拉框 Portal */} + {mounted && showSortDropdown && createPortal( +
+
+
+ {sortOptions.map((option) => ( + + ))} +
+
+
, + document.body + )} + {error && (

{error}