feat: add right sidebar collapse

This commit is contained in:
shinya
2025-07-08 13:23:27 +08:00
parent abed52c766
commit e412e2d765
8 changed files with 97 additions and 35 deletions

View File

@@ -3,7 +3,8 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { getConfig } from '@/lib/config'; import { getConfig } from '@/lib/config';
import { getStorage, IStorage } from '@/lib/db'; import { getStorage } from '@/lib/db';
import { IStorage } from '@/lib/types';
export const runtime = 'edge'; export const runtime = 'edge';

View File

@@ -3,7 +3,8 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { getConfig } from '@/lib/config'; import { getConfig } from '@/lib/config';
import { getStorage, IStorage } from '@/lib/db'; import { getStorage } from '@/lib/db';
import { IStorage } from '@/lib/types';
export const runtime = 'edge'; export const runtime = 'edge';

View File

@@ -2,7 +2,8 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { db, Favorite } from '@/lib/db'; import { db } from '@/lib/db';
import { Favorite } from '@/lib/types';
export const runtime = 'edge'; export const runtime = 'edge';

View File

@@ -3,7 +3,7 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
import { PlayRecord } from '@/lib/db'; import { PlayRecord } from '@/lib/types';
export const runtime = 'edge'; export const runtime = 'edge';

View File

@@ -109,6 +109,10 @@ function PlayPageClient() {
null null
); );
// 折叠状态(仅在 lg 及以上屏幕有效)
const [isEpisodeSelectorCollapsed, setIsEpisodeSelectorCollapsed] =
useState(false);
// 播放进度保存相关 // 播放进度保存相关
const saveIntervalRef = useRef<NodeJS.Timeout | null>(null); const saveIntervalRef = useRef<NodeJS.Timeout | null>(null);
const lastSaveTimeRef = useRef<number>(0); const lastSaveTimeRef = useRef<number>(0);
@@ -1207,7 +1211,7 @@ function PlayPageClient() {
return ( return (
<PageLayout activePath='/play'> <PageLayout activePath='/play'>
<div className='flex flex-col gap-6 py-4 px-5 lg:px-10 xl:px-20'> <div className='flex flex-col gap-3 py-4 px-5 lg:px-10 xl:px-20'>
{/* 第一行:影片标题 */} {/* 第一行:影片标题 */}
<div className='py-1'> <div className='py-1'>
<h1 className='text-xl font-semibold text-gray-900 dark:text-gray-100'> <h1 className='text-xl font-semibold text-gray-900 dark:text-gray-100'>
@@ -1220,30 +1224,89 @@ function PlayPageClient() {
</h1> </h1>
</div> </div>
{/* 第二行:播放器和选集 */} {/* 第二行:播放器和选集 */}
<div className='grid grid-cols-1 md:grid-cols-4 gap-4 lg:h-[500px] xl:h-[650px]'> <div className='space-y-2'>
{/* 播放器 */} {/* 折叠控制 - 仅在 lg 及以上屏幕显示 */}
<div className='md:col-span-3 h-full'> <div className='hidden lg:flex justify-end'>
<div <button
ref={artRef} onClick={() =>
className='bg-black w-full h-[300px] lg:h-full rounded-xl overflow-hidden border border-white/0 dark:border-white/30' setIsEpisodeSelectorCollapsed(!isEpisodeSelectorCollapsed)
></div> }
className='group relative flex items-center space-x-1.5 px-3 py-1.5 rounded-full bg-white/80 hover:bg-white dark:bg-gray-800/80 dark:hover:bg-gray-800 backdrop-blur-sm border border-gray-200/50 dark:border-gray-700/50 shadow-sm hover:shadow-md transition-all duration-200'
title={
isEpisodeSelectorCollapsed ? '显示选集面板' : '隐藏选集面板'
}
>
<svg
className={`w-3.5 h-3.5 text-gray-500 dark:text-gray-400 transition-transform duration-200 ${
isEpisodeSelectorCollapsed ? 'rotate-180' : 'rotate-0'
}`}
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
d='M9 5l7 7-7 7'
/>
</svg>
<span className='text-xs font-medium text-gray-600 dark:text-gray-300'>
{isEpisodeSelectorCollapsed ? '显示' : '隐藏'}
</span>
{/* 精致的状态指示点 */}
<div
className={`absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full transition-all duration-200 ${
isEpisodeSelectorCollapsed
? 'bg-orange-400 animate-pulse'
: 'bg-green-400'
}`}
></div>
</button>
</div> </div>
{/* 选集和换源 */} <div
<div className='md:col-span-1 h-[300px] lg:h-full md:overflow-hidden'> className={`grid gap-4 lg:h-[500px] xl:h-[650px] transition-all duration-300 ease-in-out ${
<EpisodeSelector isEpisodeSelectorCollapsed
totalEpisodes={totalEpisodes} ? 'grid-cols-1'
value={currentEpisodeIndex + 1} : 'grid-cols-1 md:grid-cols-4'
onChange={handleEpisodeChange} }`}
onSourceChange={handleSourceChange} >
currentSource={currentSource} {/* 播放器 */}
currentId={currentId} <div
videoTitle={videoTitle} className={`h-full transition-all duration-300 ease-in-out ${
availableSources={availableSources} isEpisodeSelectorCollapsed ? 'col-span-1' : 'md:col-span-3'
onSearchSources={handleSearchSources} }`}
sourceSearchLoading={sourceSearchLoading} >
sourceSearchError={sourceSearchError} <div
/> ref={artRef}
className='bg-black w-full h-[300px] lg:h-full rounded-xl overflow-hidden border border-white/0 dark:border-white/30 shadow-lg'
></div>
</div>
{/* 选集和换源 - 在移动端始终显示,在 lg 及以上可折叠 */}
<div
className={`h-[300px] lg:h-full md:overflow-hidden transition-all duration-300 ease-in-out ${
isEpisodeSelectorCollapsed
? 'md:col-span-1 lg:hidden lg:opacity-0 lg:scale-95'
: 'md:col-span-1 lg:opacity-100 lg:scale-100'
}`}
>
<EpisodeSelector
totalEpisodes={totalEpisodes}
value={currentEpisodeIndex + 1}
onChange={handleEpisodeChange}
onSourceChange={handleSourceChange}
currentSource={currentSource}
currentId={currentId}
videoTitle={videoTitle}
availableSources={availableSources}
onSearchSources={handleSearchSources}
sourceSearchLoading={sourceSearchLoading}
sourceSearchError={sourceSearchError}
/>
</div>
</div> </div>
</div> </div>

View File

@@ -1,10 +1,8 @@
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ /* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
import { AdminConfig } from './admin.types';
import { RedisStorage } from './redis.db'; import { RedisStorage } from './redis.db';
import { AdminConfig, Favorite, IStorage, PlayRecord } from './types'; import { Favorite, IStorage, PlayRecord } from './types';
// 重新导出类型,保持向后兼容
export type { AdminConfig, Favorite, IStorage, PlayRecord };
// storage type 常量: 'localstorage' | 'database',默认 'localstorage' // storage type 常量: 'localstorage' | 'database',默认 'localstorage'
const STORAGE_TYPE = const STORAGE_TYPE =

View File

@@ -2,7 +2,8 @@
import { createClient, RedisClientType } from 'redis'; import { createClient, RedisClientType } from 'redis';
import { AdminConfig, Favorite, IStorage, PlayRecord } from './types'; import { AdminConfig } from './admin.types';
import { Favorite, IStorage, PlayRecord } from './types';
// 搜索历史最大条数 // 搜索历史最大条数
const SEARCH_HISTORY_LIMIT = 20; const SEARCH_HISTORY_LIMIT = 20;

View File

@@ -109,6 +109,3 @@ export interface DoubanResult {
message: string; message: string;
list: DoubanItem[]; list: DoubanItem[];
} }
// 导出AdminConfig类型
export { AdminConfig };