增加轮播图高度配置

This commit is contained in:
mtvpls
2026-05-23 11:18:27 +08:00
parent 40ab3346ee
commit f3ec0d3019
2 changed files with 853 additions and 320 deletions
+136 -70
View File
@@ -1,11 +1,21 @@
'use client'; 'use client';
import { ChevronLeft, ChevronRight, Play, Volume2, VolumeX } from 'lucide-react'; import {
ChevronLeft,
ChevronRight,
Play,
Volume2,
VolumeX,
} from 'lucide-react';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useRef,useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { type TMDBItem,getGenreNames, getTMDBImageUrl } from '@/lib/tmdb.client'; import {
type TMDBItem,
getGenreNames,
getTMDBImageUrl,
} from '@/lib/tmdb.client';
import { getDoubanDetail } from '@/lib/douban.client'; import { getDoubanDetail } from '@/lib/douban.client';
import ProxyImage from '@/components/ProxyImage'; import ProxyImage from '@/components/ProxyImage';
@@ -15,6 +25,21 @@ interface BannerCarouselProps {
delayLoad?: boolean; // 是否延迟加载(等页面加载完毕后再加载) delayLoad?: boolean; // 是否延迟加载(等页面加载完毕后再加载)
} }
type HomeBannerHeightScale = '1' | '1.5' | '2';
const bannerHeightClassMap: Record<HomeBannerHeightScale, string> = {
'1': 'h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px]',
'1.5': 'h-[300px] sm:h-[450px] md:h-[600px] lg:h-[750px]',
'2': 'h-[400px] sm:h-[600px] md:h-[800px] lg:h-[1000px]',
};
const getSavedBannerHeightScale = (): HomeBannerHeightScale => {
if (typeof window === 'undefined') return '1';
const saved = localStorage.getItem('homeBannerHeightScale');
return saved === '1.5' || saved === '2' ? saved : '1';
};
// 扩展TMDBItem类型以支持TX数据源的额外字段 // 扩展TMDBItem类型以支持TX数据源的额外字段
interface BannerItem extends TMDBItem { interface BannerItem extends TMDBItem {
subtitle?: string; // TX数据源的子标题 subtitle?: string; // TX数据源的子标题
@@ -23,7 +48,10 @@ interface BannerItem extends TMDBItem {
genres?: string[]; // 豆瓣数据源的类型标签 genres?: string[]; // 豆瓣数据源的类型标签
} }
export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = false }: BannerCarouselProps) { export default function BannerCarousel({
autoPlayInterval = 5000,
delayLoad = false,
}: BannerCarouselProps) {
const router = useRouter(); const router = useRouter();
const [items, setItems] = useState<BannerItem[]>([]); const [items, setItems] = useState<BannerItem[]>([]);
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
@@ -36,6 +64,8 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
const [dataSource, setDataSource] = useState<string>(''); // 当前数据源 const [dataSource, setDataSource] = useState<string>(''); // 当前数据源
const [trailersLoaded, setTrailersLoaded] = useState(false); // 预告片是否已加载 const [trailersLoaded, setTrailersLoaded] = useState(false); // 预告片是否已加载
const [isMuted, setIsMuted] = useState(true); // 视频是否静音(默认静音) const [isMuted, setIsMuted] = useState(true); // 视频是否静音(默认静音)
const [bannerHeightScale, setBannerHeightScale] =
useState<HomeBannerHeightScale>('1'); // 轮播图高度倍率
const videoRef = useRef<HTMLVideoElement>(null); // 视频元素引用 const videoRef = useRef<HTMLVideoElement>(null); // 视频元素引用
const videoRefs = useRef<Map<number, HTMLVideoElement>>(new Map()); // 所有视频元素的引用 const videoRefs = useRef<Map<number, HTMLVideoElement>>(new Map()); // 所有视频元素的引用
const touchStartX = useRef(0); const touchStartX = useRef(0);
@@ -95,6 +125,20 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
if (setting !== null) { if (setting !== null) {
setEnableTrailers(setting === 'true'); setEnableTrailers(setting === 'true');
} }
setBannerHeightScale(getSavedBannerHeightScale());
const handleHomeModulesUpdated = () => {
setBannerHeightScale(getSavedBannerHeightScale());
};
window.addEventListener('homeModulesUpdated', handleHomeModulesUpdated);
return () => {
window.removeEventListener(
'homeModulesUpdated',
handleHomeModulesUpdated
);
};
}, []); }, []);
// 延迟加载:等待页面加载完毕后再开始加载轮播图数据 // 延迟加载:等待页面加载完毕后再开始加载轮播图数据
@@ -204,10 +248,13 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
// 保存到 localStorage(使用数据源特定的key // 保存到 localStorage(使用数据源特定的key
try { try {
localStorage.setItem(cacheKey, JSON.stringify({ localStorage.setItem(
data: result.list, cacheKey,
timestamp: Date.now() JSON.stringify({
})); data: result.list,
timestamp: Date.now(),
})
);
} catch (e) { } catch (e) {
// localStorage 可能已满,忽略错误 // localStorage 可能已满,忽略错误
console.error('保存到 localStorage 失败:', e); console.error('保存到 localStorage 失败:', e);
@@ -227,7 +274,12 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
// 前端获取豆瓣预告片 // 前端获取豆瓣预告片
useEffect(() => { useEffect(() => {
// 只有在启用预告片、数据源是豆瓣、有数据且未加载预告片时才执行 // 只有在启用预告片、数据源是豆瓣、有数据且未加载预告片时才执行
if (!enableTrailers || dataSource !== 'Douban' || items.length === 0 || trailersLoaded) { if (
!enableTrailers ||
dataSource !== 'Douban' ||
items.length === 0 ||
trailersLoaded
) {
return; return;
} }
@@ -241,9 +293,10 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
const detail = await getDoubanDetail(item.id.toString()); const detail = await getDoubanDetail(item.id.toString());
// 获取预告片链接(取第一个) // 获取预告片链接(取第一个)
const trailerUrl = detail.trailers && detail.trailers.length > 0 const trailerUrl =
? detail.trailers[0].video_url detail.trailers && detail.trailers.length > 0
: null; ? detail.trailers[0].video_url
: null;
return { return {
...item, ...item,
@@ -298,7 +351,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
setSkipNextAutoPlay(false); setSkipNextAutoPlay(false);
return; return;
} }
setCurrentIndex((prev) => (prev + 1) % items.length); setCurrentIndex((prev) => (prev + 1) % items.length);
}, autoPlayInterval); }, autoPlayInterval);
@@ -350,7 +403,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
// 防止在手动切换过程中触发 // 防止在手动切换过程中触发
if (isManualChange.current) return; if (isManualChange.current) return;
if (!touchStartX.current) return; if (!touchStartX.current) return;
// 如果有滑动,则执行滑动逻辑 // 如果有滑动,则执行滑动逻辑
if (touchEndX.current !== 0) { if (touchEndX.current !== 0) {
const distance = touchStartX.current - touchEndX.current; const distance = touchStartX.current - touchEndX.current;
@@ -374,13 +427,15 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
if (isLoading || !shouldLoad) { if (isLoading || !shouldLoad) {
return ( return (
<div className="relative w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] bg-gradient-to-b from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900 overflow-hidden flex items-center justify-center"> <div
className={`relative w-full ${bannerHeightClassMap[bannerHeightScale]} bg-gradient-to-b from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900 overflow-hidden flex items-center justify-center`}
>
<Image <Image
src="/logo.png" src='/logo.png'
alt="MoonTVPlus" alt='MoonTVPlus'
width={120} width={120}
height={120} height={120}
className="opacity-50" className='opacity-50'
priority priority
/> />
</div> </div>
@@ -395,7 +450,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
return ( return (
<div <div
className="relative w-full h-[200px] sm:h-[300px] md:h-[400px] lg:h-[500px] overflow-hidden group" className={`relative w-full ${bannerHeightClassMap[bannerHeightScale]} overflow-hidden group`}
onMouseEnter={() => setIsPaused(true)} onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)} onMouseLeave={() => setIsPaused(false)}
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
@@ -409,7 +464,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
}} }}
> >
{/* 背景图片或视频 */} {/* 背景图片或视频 */}
<div className="absolute inset-0"> <div className='absolute inset-0'>
{items.map((item, index) => ( {items.map((item, index) => (
<div <div
key={item.id} key={item.id}
@@ -419,7 +474,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
> >
{item.trailer_url && enableTrailers ? ( {item.trailer_url && enableTrailers ? (
/* 显示豆瓣直链视频 */ /* 显示豆瓣直链视频 */
<div className="absolute inset-0 overflow-hidden"> <div className='absolute inset-0 overflow-hidden'>
<video <video
ref={(el) => { ref={(el) => {
if (el) { if (el) {
@@ -429,20 +484,20 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
} }
}} }}
src={getVideoUrl(item.trailer_url) || undefined} src={getVideoUrl(item.trailer_url) || undefined}
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 min-w-full min-h-full w-auto h-auto object-cover" className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 min-w-full min-h-full w-auto h-auto object-cover'
muted={isMuted} muted={isMuted}
loop loop
playsInline playsInline
preload="metadata" preload='metadata'
/> />
</div> </div>
) : item.video_key && isYouTubeAccessible && enableTrailers ? ( ) : item.video_key && isYouTubeAccessible && enableTrailers ? (
/* 显示YouTube视频 */ /* 显示YouTube视频 */
<div className="absolute inset-0 overflow-hidden"> <div className='absolute inset-0 overflow-hidden'>
<iframe <iframe
src={`https://www.youtube.com/embed/${item.video_key}?listType=playlist&autoplay=1&mute=1&controls=0&loop=1&playlist=${item.video_key}&modestbranding=1&rel=0&showinfo=0&vq=hd1080&hd=1&disablekb=1&fs=0&iv_load_policy=3`} src={`https://www.youtube.com/embed/${item.video_key}?listType=playlist&autoplay=1&mute=1&controls=0&loop=1&playlist=${item.video_key}&modestbranding=1&rel=0&showinfo=0&vq=hd1080&hd=1&disablekb=1&fs=0&iv_load_policy=3`}
className="absolute top-1/2 left-1/2 pointer-events-none" className='absolute top-1/2 left-1/2 pointer-events-none'
allow="autoplay; encrypted-media" allow='autoplay; encrypted-media'
style={{ style={{
border: 'none', border: 'none',
width: '100vw', width: '100vw',
@@ -456,54 +511,65 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
) : ( ) : (
/* 显示图片 */ /* 显示图片 */
<ProxyImage <ProxyImage
originalSrc={getImageUrl(item.backdrop_path || item.poster_path)} originalSrc={getImageUrl(
item.backdrop_path || item.poster_path
)}
alt={item.title} alt={item.title}
className="absolute inset-0 w-full h-full object-cover" className='absolute inset-0 w-full h-full object-cover'
loading={index === 0 ? 'eager' : 'lazy'} loading={index === 0 ? 'eager' : 'lazy'}
/> />
)} )}
{/* 渐变遮罩 */} {/* 渐变遮罩 */}
<div className="absolute inset-0 bg-gradient-to-r from-black/80 via-black/50 to-transparent"></div> <div className='absolute inset-0 bg-gradient-to-r from-black/80 via-black/50 to-transparent'></div>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent"></div> <div className='absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent'></div>
</div> </div>
))} ))}
</div> </div>
{/* 内容信息 */} {/* 内容信息 */}
<div className="absolute inset-0 flex items-end p-8 md:p-12 pointer-events-none"> <div className='absolute inset-0 flex items-end p-8 md:p-12 pointer-events-none'>
<div className="max-w-2xl space-y-4"> <div className='max-w-2xl space-y-4'>
<h2 className="text-3xl md:text-5xl font-bold text-white drop-shadow-lg"> <h2 className='text-3xl md:text-5xl font-bold text-white drop-shadow-lg'>
{currentItem.title} {currentItem.title}
</h2> </h2>
<div className="flex items-center gap-2 md:gap-3 text-sm md:text-base text-white/90 flex-wrap"> <div className='flex items-center gap-2 md:gap-3 text-sm md:text-base text-white/90 flex-wrap'>
{currentItem.vote_average > 0 && ( {currentItem.vote_average > 0 && (
<span className="px-2 py-1 bg-yellow-500 text-black font-semibold rounded"> <span className='px-2 py-1 bg-yellow-500 text-black font-semibold rounded'>
{currentItem.vote_average.toFixed(1)} {currentItem.vote_average.toFixed(1)}
</span> </span>
)} )}
{/* 显示标签:优先TX的tags,其次豆瓣的genres,最后TMDB的genre_ids */} {/* 显示标签:优先TX的tags,其次豆瓣的genres,最后TMDB的genre_ids */}
{currentItem.tags && currentItem.tags.length > 0 ? ( {currentItem.tags && currentItem.tags.length > 0
currentItem.tags.slice(0, 3).map((tag, index) => ( ? currentItem.tags.slice(0, 3).map((tag, index) => (
<span key={index} className="px-2 py-1 bg-white/20 backdrop-blur-sm rounded text-sm"> <span
{tag} key={index}
</span> className='px-2 py-1 bg-white/20 backdrop-blur-sm rounded text-sm'
)) >
) : currentItem.genres && Array.isArray(currentItem.genres) && currentItem.genres.length > 0 ? ( {tag}
/* 显示豆瓣数据源的标签 */ </span>
currentItem.genres.slice(0, 3).map((genre, index) => ( ))
<span key={index} className="px-2 py-1 bg-white/20 backdrop-blur-sm rounded text-sm"> : currentItem.genres &&
{genre} Array.isArray(currentItem.genres) &&
</span> currentItem.genres.length > 0
)) ? /* 显示豆瓣数据源的标签 */
) : ( currentItem.genres.slice(0, 3).map((genre, index) => (
/* 显示TMDB数据源的类型标签 */ <span
getGenreNames(currentItem.genre_ids, 3).map(genre => ( key={index}
<span key={genre} className="px-2 py-1 bg-white/20 backdrop-blur-sm rounded text-sm"> className='px-2 py-1 bg-white/20 backdrop-blur-sm rounded text-sm'
{genre} >
</span> {genre}
)) </span>
)} ))
: /* 显示TMDB数据源的类型标签 */
getGenreNames(currentItem.genre_ids, 3).map((genre) => (
<span
key={genre}
className='px-2 py-1 bg-white/20 backdrop-blur-sm rounded text-sm'
>
{genre}
</span>
))}
{currentItem.release_date && ( {currentItem.release_date && (
<span>{currentItem.release_date}</span> <span>{currentItem.release_date}</span>
)} )}
@@ -515,14 +581,14 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
e.stopPropagation(); e.stopPropagation();
handlePlay(currentItem.title); handlePlay(currentItem.title);
}} }}
className="hidden md:flex items-center gap-2 px-6 py-3 bg-gray-500/30 hover:bg-gray-500/50 backdrop-blur-sm text-white font-semibold rounded-lg transition-all pointer-events-auto" className='hidden md:flex items-center gap-2 px-6 py-3 bg-gray-500/30 hover:bg-gray-500/50 backdrop-blur-sm text-white font-semibold rounded-lg transition-all pointer-events-auto'
> >
<Play className="w-5 h-5 fill-white" /> <Play className='w-5 h-5 fill-white' />
</button> </button>
{currentItem.overview && ( {currentItem.overview && (
<p className="text-sm md:text-base text-white/80 line-clamp-3 drop-shadow-md"> <p className='text-sm md:text-base text-white/80 line-clamp-3 drop-shadow-md'>
{currentItem.overview} {currentItem.overview}
</p> </p>
)} )}
@@ -532,36 +598,36 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
{/* 左右切换按钮 - 只在桌面端显示 */} {/* 左右切换按钮 - 只在桌面端显示 */}
<button <button
onClick={goToPrevious} onClick={goToPrevious}
className="hidden md:flex absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 bg-black/30 hover:bg-black/60 text-white rounded-full items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300" className='hidden md:flex absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 bg-black/30 hover:bg-black/60 text-white rounded-full items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300'
aria-label="上一张" aria-label='上一张'
> >
<ChevronLeft className="w-8 h-8" /> <ChevronLeft className='w-8 h-8' />
</button> </button>
<button <button
onClick={goToNext} onClick={goToNext}
className="hidden md:flex absolute right-4 top-1/2 -translate-y-1/2 w-12 h-12 bg-black/30 hover:bg-black/60 text-white rounded-full items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300" className='hidden md:flex absolute right-4 top-1/2 -translate-y-1/2 w-12 h-12 bg-black/30 hover:bg-black/60 text-white rounded-full items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300'
aria-label="下一张" aria-label='下一张'
> >
<ChevronRight className="w-8 h-8" /> <ChevronRight className='w-8 h-8' />
</button> </button>
{/* 音量控制按钮 - 只在有豆瓣预告片时显示 */} {/* 音量控制按钮 - 只在有豆瓣预告片时显示 */}
{currentItem.trailer_url && enableTrailers && ( {currentItem.trailer_url && enableTrailers && (
<button <button
onClick={toggleMute} onClick={toggleMute}
className="absolute top-2 right-2 md:top-4 md:right-4 w-8 h-8 md:w-10 md:h-10 bg-black/30 hover:bg-black/60 text-white rounded-full flex items-center justify-center transition-all duration-300 z-10" className='absolute top-2 right-2 md:top-4 md:right-4 w-8 h-8 md:w-10 md:h-10 bg-black/30 hover:bg-black/60 text-white rounded-full flex items-center justify-center transition-all duration-300 z-10'
aria-label={isMuted ? "开启声音" : "关闭声音"} aria-label={isMuted ? '开启声音' : '关闭声音'}
> >
{isMuted ? ( {isMuted ? (
<VolumeX className="w-4 h-4 md:w-5 md:h-5" /> <VolumeX className='w-4 h-4 md:w-5 md:h-5' />
) : ( ) : (
<Volume2 className="w-4 h-4 md:w-5 md:h-5" /> <Volume2 className='w-4 h-4 md:w-5 md:h-5' />
)} )}
</button> </button>
)} )}
{/* 指示器 */} {/* 指示器 */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2"> <div className='absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2'>
{items.map((_, index) => ( {items.map((_, index) => (
<button <button
key={index} key={index}
+717 -250
View File
@@ -65,17 +65,20 @@ export const UserMenu: React.FC = () => {
const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false); const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
const [isSubscribeOpen, setIsSubscribeOpen] = useState(false); const [isSubscribeOpen, setIsSubscribeOpen] = useState(false);
const [isVersionPanelOpen, setIsVersionPanelOpen] = useState(false); const [isVersionPanelOpen, setIsVersionPanelOpen] = useState(false);
const [isOfflineDownloadPanelOpen, setIsOfflineDownloadPanelOpen] = useState(false); const [isOfflineDownloadPanelOpen, setIsOfflineDownloadPanelOpen] =
useState(false);
const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false); const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false);
const [isFavoritesPanelOpen, setIsFavoritesPanelOpen] = useState(false); const [isFavoritesPanelOpen, setIsFavoritesPanelOpen] = useState(false);
const [isEmailSettingsOpen, setIsEmailSettingsOpen] = useState(false); const [isEmailSettingsOpen, setIsEmailSettingsOpen] = useState(false);
const [isDeviceManagementOpen, setIsDeviceManagementOpen] = useState(false); const [isDeviceManagementOpen, setIsDeviceManagementOpen] = useState(false);
const [isEcoAppsOpen, setIsEcoAppsOpen] = useState(false); const [isEcoAppsOpen, setIsEcoAppsOpen] = useState(false);
const [isReportOpen, setIsReportOpen] = useState(false); const [isReportOpen, setIsReportOpen] = useState(false);
const [isDownloadManagementOpen, setIsDownloadManagementOpen] = useState(false); const [isDownloadManagementOpen, setIsDownloadManagementOpen] =
useState(false);
const [authInfo, setAuthInfo] = useState<AuthInfo | null>(null); const [authInfo, setAuthInfo] = useState<AuthInfo | null>(null);
const [storageType, setStorageType] = useState<string>('localstorage'); const [storageType, setStorageType] = useState<string>('localstorage');
const [displayStorageType, setDisplayStorageType] = useState<string>('localstorage'); const [displayStorageType, setDisplayStorageType] =
useState<string>('localstorage');
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
@@ -86,12 +89,25 @@ export const UserMenu: React.FC = () => {
const [tvboxToken, setTvboxToken] = useState(''); const [tvboxToken, setTvboxToken] = useState('');
const [isResettingToken, setIsResettingToken] = useState(false); const [isResettingToken, setIsResettingToken] = useState(false);
const [isLoadingSubscribeUrl, setIsLoadingSubscribeUrl] = useState(false); const [isLoadingSubscribeUrl, setIsLoadingSubscribeUrl] = useState(false);
const [subscribeAdFilterEnabled, setSubscribeAdFilterEnabled] = useState(false); const [subscribeAdFilterEnabled, setSubscribeAdFilterEnabled] =
const [subscribeYellowFilterEnabled, setSubscribeYellowFilterEnabled] = useState(false); useState(false);
const [subscribeYellowFilterEnabled, setSubscribeYellowFilterEnabled] =
useState(false);
// Body 滚动锁定 - 使用 overflow 方式避免布局问题 // Body 滚动锁定 - 使用 overflow 方式避免布局问题
useEffect(() => { useEffect(() => {
if (isProfileCenterOpen || isSettingsOpen || isChangePasswordOpen || isSubscribeOpen || isOfflineDownloadPanelOpen || isEmailSettingsOpen || isDeviceManagementOpen || isEcoAppsOpen || isReportOpen || isDownloadManagementOpen) { if (
isProfileCenterOpen ||
isSettingsOpen ||
isChangePasswordOpen ||
isSubscribeOpen ||
isOfflineDownloadPanelOpen ||
isEmailSettingsOpen ||
isDeviceManagementOpen ||
isEcoAppsOpen ||
isReportOpen ||
isDownloadManagementOpen
) {
const body = document.body; const body = document.body;
const html = document.documentElement; const html = document.documentElement;
@@ -104,13 +120,23 @@ export const UserMenu: React.FC = () => {
html.style.overflow = 'hidden'; html.style.overflow = 'hidden';
return () => { return () => {
// 恢复所有原始样式 // 恢复所有原始样式
body.style.overflow = originalBodyOverflow; body.style.overflow = originalBodyOverflow;
html.style.overflow = originalHtmlOverflow; html.style.overflow = originalHtmlOverflow;
}; };
} }
}, [isProfileCenterOpen, isSettingsOpen, isChangePasswordOpen, isSubscribeOpen, isOfflineDownloadPanelOpen, isEmailSettingsOpen, isDeviceManagementOpen, isEcoAppsOpen, isReportOpen, isDownloadManagementOpen]); }, [
isProfileCenterOpen,
isSettingsOpen,
isChangePasswordOpen,
isSubscribeOpen,
isOfflineDownloadPanelOpen,
isEmailSettingsOpen,
isDeviceManagementOpen,
isEcoAppsOpen,
isReportOpen,
isDownloadManagementOpen,
]);
// 设置相关状态 // 设置相关状态
const [defaultAggregateSearch, setDefaultAggregateSearch] = useState(true); const [defaultAggregateSearch, setDefaultAggregateSearch] = useState(true);
@@ -121,30 +147,44 @@ export const UserMenu: React.FC = () => {
const [fluidSearch, setFluidSearch] = useState(true); const [fluidSearch, setFluidSearch] = useState(true);
const [tmdbBackdropDisabled, setTmdbBackdropDisabled] = useState(false); const [tmdbBackdropDisabled, setTmdbBackdropDisabled] = useState(false);
const [enableTrailers, setEnableTrailers] = useState(false); const [enableTrailers, setEnableTrailers] = useState(false);
const [doubanDataSource, setDoubanDataSource] = useState('cmliussss-cdn-tencent'); const [doubanDataSource, setDoubanDataSource] = useState(
const [doubanDataSourceBackup, setDoubanDataSourceBackup] = useState('direct'); 'cmliussss-cdn-tencent'
const [doubanImageProxyType, setDoubanImageProxyType] = useState('cmliussss-cdn-tencent'); );
const [doubanImageProxyTypeBackup, setDoubanImageProxyTypeBackup] = useState('server'); const [doubanDataSourceBackup, setDoubanDataSourceBackup] =
useState('direct');
const [doubanImageProxyType, setDoubanImageProxyType] = useState(
'cmliussss-cdn-tencent'
);
const [doubanImageProxyTypeBackup, setDoubanImageProxyTypeBackup] =
useState('server');
const [doubanImageProxyUrl, setDoubanImageProxyUrl] = useState(''); const [doubanImageProxyUrl, setDoubanImageProxyUrl] = useState('');
const [doubanProxyUrlBackup, setDoubanProxyUrlBackup] = useState(''); const [doubanProxyUrlBackup, setDoubanProxyUrlBackup] = useState('');
const [doubanImageProxyUrlBackup, setDoubanImageProxyUrlBackup] = useState(''); const [doubanImageProxyUrlBackup, setDoubanImageProxyUrlBackup] =
useState('');
const [isDoubanDropdownOpen, setIsDoubanDropdownOpen] = useState(false); const [isDoubanDropdownOpen, setIsDoubanDropdownOpen] = useState(false);
const [isDoubanBackupDropdownOpen, setIsDoubanBackupDropdownOpen] = useState(false); const [isDoubanBackupDropdownOpen, setIsDoubanBackupDropdownOpen] =
useState(false);
const [isDoubanImageProxyDropdownOpen, setIsDoubanImageProxyDropdownOpen] = const [isDoubanImageProxyDropdownOpen, setIsDoubanImageProxyDropdownOpen] =
useState(false); useState(false);
const [isDoubanImageProxyBackupDropdownOpen, setIsDoubanImageProxyBackupDropdownOpen] = const [
useState(false); isDoubanImageProxyBackupDropdownOpen,
setIsDoubanImageProxyBackupDropdownOpen,
] = useState(false);
const [bufferStrategy, setBufferStrategy] = useState('medium'); const [bufferStrategy, setBufferStrategy] = useState('medium');
const [nextEpisodePreCache, setNextEpisodePreCache] = useState(true); const [nextEpisodePreCache, setNextEpisodePreCache] = useState(true);
const [nextEpisodeDanmakuPreload, setNextEpisodeDanmakuPreload] = useState(true); const [nextEpisodeDanmakuPreload, setNextEpisodeDanmakuPreload] =
useState(true);
const [disableAutoLoadDanmaku, setDisableAutoLoadDanmaku] = useState(false); const [disableAutoLoadDanmaku, setDisableAutoLoadDanmaku] = useState(false);
const [danmakuMaxCount, setDanmakuMaxCount] = useState(0); const [danmakuMaxCount, setDanmakuMaxCount] = useState(0);
const [danmakuHeatmapDisabled, setDanmakuHeatmapDisabled] = useState(false); const [danmakuHeatmapDisabled, setDanmakuHeatmapDisabled] = useState(false);
const [searchTraditionalToSimplified, setSearchTraditionalToSimplified] = useState(false); const [searchTraditionalToSimplified, setSearchTraditionalToSimplified] =
useState(false);
const [exactSearch, setExactSearch] = useState(true); const [exactSearch, setExactSearch] = useState(true);
const [maxConcurrentDownloads, setMaxConcurrentDownloads] = useState(6); const [maxConcurrentDownloads, setMaxConcurrentDownloads] = useState(6);
const [downloadThreadsPerTask, setDownloadThreadsPerTask] = useState(6); const [downloadThreadsPerTask, setDownloadThreadsPerTask] = useState(6);
const [downloadMode, setDownloadMode] = useState<'browser' | 'filesystem'>('browser'); const [downloadMode, setDownloadMode] = useState<'browser' | 'filesystem'>(
'browser'
);
const [filesystemSavePath, setFilesystemSavePath] = useState<string>(''); const [filesystemSavePath, setFilesystemSavePath] = useState<string>('');
// 邮件通知设置 // 邮件通知设置
@@ -179,7 +219,9 @@ export const UserMenu: React.FC = () => {
const [isDoubanSectionOpen, setIsDoubanSectionOpen] = useState(false); const [isDoubanSectionOpen, setIsDoubanSectionOpen] = useState(false);
// TMDB 图片设置 // TMDB 图片设置
const [tmdbImageBaseUrl, setTmdbImageBaseUrl] = useState('https://image.tmdb.org'); const [tmdbImageBaseUrl, setTmdbImageBaseUrl] = useState(
'https://image.tmdb.org'
);
const [isUsageSectionOpen, setIsUsageSectionOpen] = useState(false); const [isUsageSectionOpen, setIsUsageSectionOpen] = useState(false);
const [isDownloadSectionOpen, setIsDownloadSectionOpen] = useState(false); const [isDownloadSectionOpen, setIsDownloadSectionOpen] = useState(false);
const [isBufferSectionOpen, setIsBufferSectionOpen] = useState(false); const [isBufferSectionOpen, setIsBufferSectionOpen] = useState(false);
@@ -194,6 +236,8 @@ export const UserMenu: React.FC = () => {
order: number; order: number;
} }
type HomeBannerHeightScale = '1' | '1.5' | '2';
const defaultHomeModules: HomeModule[] = [ const defaultHomeModules: HomeModule[] = [
{ id: 'hotMovies', name: '热门电影', enabled: true, order: 0 }, { id: 'hotMovies', name: '热门电影', enabled: true, order: 0 },
{ id: 'hotDuanju', name: '热播短剧', enabled: true, order: 1 }, { id: 'hotDuanju', name: '热播短剧', enabled: true, order: 1 },
@@ -203,9 +247,23 @@ export const UserMenu: React.FC = () => {
{ id: 'upcomingContent', name: '即将上映', enabled: true, order: 5 }, { id: 'upcomingContent', name: '即将上映', enabled: true, order: 5 },
]; ];
const [homeModules, setHomeModules] = useState<HomeModule[]>(defaultHomeModules); const [homeModules, setHomeModules] =
useState<HomeModule[]>(defaultHomeModules);
const [homeBannerEnabled, setHomeBannerEnabled] = useState(true); const [homeBannerEnabled, setHomeBannerEnabled] = useState(true);
const [homeContinueWatchingEnabled, setHomeContinueWatchingEnabled] = useState(true); const [homeBannerHeightScale, setHomeBannerHeightScale] =
useState<HomeBannerHeightScale>('1');
const [homeContinueWatchingEnabled, setHomeContinueWatchingEnabled] =
useState(true);
const homeBannerHeightOptions: {
value: HomeBannerHeightScale;
label: string;
description: string;
}[] = [
{ value: '1', label: '标准', description: '1x' },
{ value: '1.5', label: '增高', description: '1.5x' },
{ value: '2', label: '特高', description: '2x' },
];
// 豆瓣数据源选项 // 豆瓣数据源选项
const doubanDataSourceOptions = [ const doubanDataSourceOptions = [
@@ -229,8 +287,14 @@ export const UserMenu: React.FC = () => {
{ value: 'cmliussss-cdn-ali', label: '豆瓣 CDN By CMLiussss(阿里云)' }, { value: 'cmliussss-cdn-ali', label: '豆瓣 CDN By CMLiussss(阿里云)' },
{ value: 'baidu', label: '百度图片代理' }, { value: 'baidu', label: '百度图片代理' },
{ value: 'custom', label: '自定义代理' }, { value: 'custom', label: '自定义代理' },
{ value: 'direct', label: '直连(浏览器直接请求豆瓣,可能需要浏览器插件才能正常显示)' }, {
{ value: 'img3', label: '豆瓣官方精品 CDN(阿里云,可能需要浏览器插件才能正常显示)' }, value: 'direct',
label: '直连(浏览器直接请求豆瓣,可能需要浏览器插件才能正常显示)',
},
{
value: 'img3',
label: '豆瓣官方精品 CDN(阿里云,可能需要浏览器插件才能正常显示)',
},
]; ];
// 缓冲策略选项 // 缓冲策略选项
@@ -249,7 +313,9 @@ export const UserMenu: React.FC = () => {
// 清除弹幕缓存相关状态 // 清除弹幕缓存相关状态
const [isClearingCache, setIsClearingCache] = useState(false); const [isClearingCache, setIsClearingCache] = useState(false);
const [clearCacheMessage, setClearCacheMessage] = useState<string | null>(null); const [clearCacheMessage, setClearCacheMessage] = useState<string | null>(
null
);
const [danmakuCacheUsage, setDanmakuCacheUsage] = useState('计算中...'); const [danmakuCacheUsage, setDanmakuCacheUsage] = useState('计算中...');
// 确保组件已挂载 // 确保组件已挂载
@@ -300,7 +366,10 @@ export const UserMenu: React.FC = () => {
if (globalWindow.__loadingNotifications) { if (globalWindow.__loadingNotifications) {
// 如果正在加载,等待加载完成后获取结果 // 如果正在加载,等待加载完成后获取结果
const checkInterval = setInterval(() => { const checkInterval = setInterval(() => {
if (!globalWindow.__loadingNotifications && globalWindow.__unreadNotificationCount !== undefined) { if (
!globalWindow.__loadingNotifications &&
globalWindow.__unreadNotificationCount !== undefined
) {
setUnreadCount(globalWindow.__unreadNotificationCount); setUnreadCount(globalWindow.__unreadNotificationCount);
clearInterval(checkInterval); clearInterval(checkInterval);
} }
@@ -340,14 +409,18 @@ export const UserMenu: React.FC = () => {
window.addEventListener('notificationsUpdated', handleNotificationsUpdated); window.addEventListener('notificationsUpdated', handleNotificationsUpdated);
return () => { return () => {
window.removeEventListener('notificationsUpdated', handleNotificationsUpdated); window.removeEventListener(
'notificationsUpdated',
handleNotificationsUpdated
);
}; };
}, []); }, []);
// 从运行时配置读取订阅是否启用 // 从运行时配置读取订阅是否启用
useEffect(() => { useEffect(() => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const enabled = (window as any).RUNTIME_CONFIG?.ENABLE_TVBOX_SUBSCRIBE || false; const enabled =
(window as any).RUNTIME_CONFIG?.ENABLE_TVBOX_SUBSCRIBE || false;
setSubscribeEnabled(enabled); setSubscribeEnabled(enabled);
} }
}, []); }, []);
@@ -363,7 +436,13 @@ export const UserMenu: React.FC = () => {
const token = data.token; const token = data.token;
setTvboxToken(token); setTvboxToken(token);
setSubscribeUrl(buildSubscribeUrl(token, subscribeAdFilterEnabled, subscribeYellowFilterEnabled)); setSubscribeUrl(
buildSubscribeUrl(
token,
subscribeAdFilterEnabled,
subscribeYellowFilterEnabled
)
);
} }
} catch (error) { } catch (error) {
console.error('获取订阅URL失败:', error); console.error('获取订阅URL失败:', error);
@@ -393,11 +472,18 @@ export const UserMenu: React.FC = () => {
const token = data.token; const token = data.token;
setTvboxToken(token); setTvboxToken(token);
setSubscribeUrl(buildSubscribeUrl(token, subscribeAdFilterEnabled, subscribeYellowFilterEnabled)); setSubscribeUrl(
buildSubscribeUrl(
token,
subscribeAdFilterEnabled,
subscribeYellowFilterEnabled
)
);
if (messageEl) { if (messageEl) {
messageEl.textContent = '订阅token已重置!'; messageEl.textContent = '订阅token已重置!';
messageEl.className = 'text-xs text-center text-green-600 dark:text-green-400 mt-2'; messageEl.className =
'text-xs text-center text-green-600 dark:text-green-400 mt-2';
messageEl.classList.remove('hidden'); messageEl.classList.remove('hidden');
setTimeout(() => { setTimeout(() => {
messageEl.classList.add('hidden'); messageEl.classList.add('hidden');
@@ -407,7 +493,8 @@ export const UserMenu: React.FC = () => {
const data = await response.json(); const data = await response.json();
if (messageEl) { if (messageEl) {
messageEl.textContent = data.error || '重置失败,请重试'; messageEl.textContent = data.error || '重置失败,请重试';
messageEl.className = 'text-xs text-center text-red-600 dark:text-red-400 mt-2'; messageEl.className =
'text-xs text-center text-red-600 dark:text-red-400 mt-2';
messageEl.classList.remove('hidden'); messageEl.classList.remove('hidden');
} }
} }
@@ -416,7 +503,8 @@ export const UserMenu: React.FC = () => {
const messageEl = document.getElementById('tvbox-token-message'); const messageEl = document.getElementById('tvbox-token-message');
if (messageEl) { if (messageEl) {
messageEl.textContent = '重置失败,请重试'; messageEl.textContent = '重置失败,请重试';
messageEl.className = 'text-xs text-center text-red-600 dark:text-red-400 mt-2'; messageEl.className =
'text-xs text-center text-red-600 dark:text-red-400 mt-2';
messageEl.classList.remove('hidden'); messageEl.classList.remove('hidden');
} }
} finally { } finally {
@@ -426,7 +514,11 @@ export const UserMenu: React.FC = () => {
}); });
}; };
const buildSubscribeUrl = (token: string, adFilter: boolean, yellowFilter: boolean) => { const buildSubscribeUrl = (
token: string,
adFilter: boolean,
yellowFilter: boolean
) => {
const currentOrigin = window.location.origin; const currentOrigin = window.location.origin;
const url = new URL('/api/tvbox/subscribe', currentOrigin); const url = new URL('/api/tvbox/subscribe', currentOrigin);
url.searchParams.set('token', token); url.searchParams.set('token', token);
@@ -465,7 +557,8 @@ export const UserMenu: React.FC = () => {
const savedDoubanDataSource = localStorage.getItem('doubanDataSource'); const savedDoubanDataSource = localStorage.getItem('doubanDataSource');
const defaultDoubanProxyType = const defaultDoubanProxyType =
(window as any).RUNTIME_CONFIG?.DOUBAN_PROXY_TYPE || 'cmliussss-cdn-tencent'; (window as any).RUNTIME_CONFIG?.DOUBAN_PROXY_TYPE ||
'cmliussss-cdn-tencent';
if (savedDoubanDataSource !== null) { if (savedDoubanDataSource !== null) {
setDoubanDataSource(savedDoubanDataSource); setDoubanDataSource(savedDoubanDataSource);
} else if (defaultDoubanProxyType) { } else if (defaultDoubanProxyType) {
@@ -495,7 +588,8 @@ export const UserMenu: React.FC = () => {
'doubanImageProxyType' 'doubanImageProxyType'
); );
const defaultDoubanImageProxyType = const defaultDoubanImageProxyType =
(window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY_TYPE || 'cmliussss-cdn-tencent'; (window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY_TYPE ||
'cmliussss-cdn-tencent';
if (savedDoubanImageProxyType !== null) { if (savedDoubanImageProxyType !== null) {
setDoubanImageProxyType(savedDoubanImageProxyType); setDoubanImageProxyType(savedDoubanImageProxyType);
} else if (defaultDoubanImageProxyType) { } else if (defaultDoubanImageProxyType) {
@@ -516,7 +610,9 @@ export const UserMenu: React.FC = () => {
const savedDoubanImageProxyTypeBackup = localStorage.getItem( const savedDoubanImageProxyTypeBackup = localStorage.getItem(
'doubanImageProxyTypeBackup' 'doubanImageProxyTypeBackup'
); );
setDoubanImageProxyTypeBackup(savedDoubanImageProxyTypeBackup || 'server'); setDoubanImageProxyTypeBackup(
savedDoubanImageProxyTypeBackup || 'server'
);
const savedDoubanImageProxyUrlBackup = localStorage.getItem( const savedDoubanImageProxyUrlBackup = localStorage.getItem(
'doubanImageProxyUrlBackup' 'doubanImageProxyUrlBackup'
@@ -553,7 +649,9 @@ export const UserMenu: React.FC = () => {
setFluidSearch(defaultFluidSearch); setFluidSearch(defaultFluidSearch);
} }
const savedTmdbBackdropDisabled = localStorage.getItem('tmdb_backdrop_disabled'); const savedTmdbBackdropDisabled = localStorage.getItem(
'tmdb_backdrop_disabled'
);
if (savedTmdbBackdropDisabled !== null) { if (savedTmdbBackdropDisabled !== null) {
setTmdbBackdropDisabled(savedTmdbBackdropDisabled === 'true'); setTmdbBackdropDisabled(savedTmdbBackdropDisabled === 'true');
} }
@@ -568,17 +666,23 @@ export const UserMenu: React.FC = () => {
setBufferStrategy(savedBufferStrategy); setBufferStrategy(savedBufferStrategy);
} }
const savedNextEpisodePreCache = localStorage.getItem('nextEpisodePreCache'); const savedNextEpisodePreCache = localStorage.getItem(
'nextEpisodePreCache'
);
if (savedNextEpisodePreCache !== null) { if (savedNextEpisodePreCache !== null) {
setNextEpisodePreCache(savedNextEpisodePreCache === 'true'); setNextEpisodePreCache(savedNextEpisodePreCache === 'true');
} }
const savedNextEpisodeDanmakuPreload = localStorage.getItem('nextEpisodeDanmakuPreload'); const savedNextEpisodeDanmakuPreload = localStorage.getItem(
'nextEpisodeDanmakuPreload'
);
if (savedNextEpisodeDanmakuPreload !== null) { if (savedNextEpisodeDanmakuPreload !== null) {
setNextEpisodeDanmakuPreload(savedNextEpisodeDanmakuPreload === 'true'); setNextEpisodeDanmakuPreload(savedNextEpisodeDanmakuPreload === 'true');
} }
const savedDisableAutoLoadDanmaku = localStorage.getItem('disableAutoLoadDanmaku'); const savedDisableAutoLoadDanmaku = localStorage.getItem(
'disableAutoLoadDanmaku'
);
if (savedDisableAutoLoadDanmaku !== null) { if (savedDisableAutoLoadDanmaku !== null) {
setDisableAutoLoadDanmaku(savedDisableAutoLoadDanmaku === 'true'); setDisableAutoLoadDanmaku(savedDisableAutoLoadDanmaku === 'true');
} else { } else {
@@ -592,7 +696,9 @@ export const UserMenu: React.FC = () => {
setDanmakuMaxCount(parseInt(savedDanmakuMaxCount, 10)); setDanmakuMaxCount(parseInt(savedDanmakuMaxCount, 10));
} }
const savedDanmakuHeatmapDisabled = localStorage.getItem('danmaku_heatmap_disabled'); const savedDanmakuHeatmapDisabled = localStorage.getItem(
'danmaku_heatmap_disabled'
);
if (savedDanmakuHeatmapDisabled !== null) { if (savedDanmakuHeatmapDisabled !== null) {
setDanmakuHeatmapDisabled(savedDanmakuHeatmapDisabled === 'true'); setDanmakuHeatmapDisabled(savedDanmakuHeatmapDisabled === 'true');
} }
@@ -602,9 +708,24 @@ export const UserMenu: React.FC = () => {
setHomeBannerEnabled(savedHomeBannerEnabled === 'true'); setHomeBannerEnabled(savedHomeBannerEnabled === 'true');
} }
const savedHomeContinueWatchingEnabled = localStorage.getItem('homeContinueWatchingEnabled'); const savedHomeBannerHeightScale = localStorage.getItem(
'homeBannerHeightScale'
);
if (
savedHomeBannerHeightScale === '1' ||
savedHomeBannerHeightScale === '1.5' ||
savedHomeBannerHeightScale === '2'
) {
setHomeBannerHeightScale(savedHomeBannerHeightScale);
}
const savedHomeContinueWatchingEnabled = localStorage.getItem(
'homeContinueWatchingEnabled'
);
if (savedHomeContinueWatchingEnabled !== null) { if (savedHomeContinueWatchingEnabled !== null) {
setHomeContinueWatchingEnabled(savedHomeContinueWatchingEnabled === 'true'); setHomeContinueWatchingEnabled(
savedHomeContinueWatchingEnabled === 'true'
);
} }
// 加载首页模块配置 // 加载首页模块配置
@@ -618,9 +739,13 @@ export const UserMenu: React.FC = () => {
} }
// 加载搜索繁体转简体设置 // 加载搜索繁体转简体设置
const savedSearchTraditionalToSimplified = localStorage.getItem('searchTraditionalToSimplified'); const savedSearchTraditionalToSimplified = localStorage.getItem(
'searchTraditionalToSimplified'
);
if (savedSearchTraditionalToSimplified !== null) { if (savedSearchTraditionalToSimplified !== null) {
setSearchTraditionalToSimplified(savedSearchTraditionalToSimplified === 'true'); setSearchTraditionalToSimplified(
savedSearchTraditionalToSimplified === 'true'
);
} }
// 加载精确搜索设置 // 加载精确搜索设置
@@ -630,25 +755,33 @@ export const UserMenu: React.FC = () => {
} }
// 加载最大同时下载限制设置 // 加载最大同时下载限制设置
const savedMaxConcurrentDownloads = localStorage.getItem('maxConcurrentDownloads'); const savedMaxConcurrentDownloads = localStorage.getItem(
'maxConcurrentDownloads'
);
if (savedMaxConcurrentDownloads !== null) { if (savedMaxConcurrentDownloads !== null) {
setMaxConcurrentDownloads(Number(savedMaxConcurrentDownloads)); setMaxConcurrentDownloads(Number(savedMaxConcurrentDownloads));
} }
// 加载单任务线程数设置 // 加载单任务线程数设置
const savedDownloadThreadsPerTask = localStorage.getItem('downloadThreadsPerTask'); const savedDownloadThreadsPerTask = localStorage.getItem(
'downloadThreadsPerTask'
);
if (savedDownloadThreadsPerTask !== null) { if (savedDownloadThreadsPerTask !== null) {
setDownloadThreadsPerTask(Number(savedDownloadThreadsPerTask)); setDownloadThreadsPerTask(Number(savedDownloadThreadsPerTask));
} }
// 加载下载模式设置 // 加载下载模式设置
const savedDownloadMode = localStorage.getItem('downloadMode'); const savedDownloadMode = localStorage.getItem('downloadMode');
if (savedDownloadMode === 'browser' || savedDownloadMode === 'filesystem') { if (
savedDownloadMode === 'browser' ||
savedDownloadMode === 'filesystem'
) {
setDownloadMode(savedDownloadMode); setDownloadMode(savedDownloadMode);
} }
// 加载保存路径设置 // 加载保存路径设置
const savedFilesystemSavePath = localStorage.getItem('filesystemSavePath'); const savedFilesystemSavePath =
localStorage.getItem('filesystemSavePath');
if (savedFilesystemSavePath !== null) { if (savedFilesystemSavePath !== null) {
setFilesystemSavePath(savedFilesystemSavePath); setFilesystemSavePath(savedFilesystemSavePath);
} }
@@ -763,7 +896,8 @@ export const UserMenu: React.FC = () => {
setConfirmDialog({ setConfirmDialog({
isOpen: true, isOpen: true,
title: '登出所有设备', title: '登出所有设备',
message: '确定要登出所有设备吗?这将清除所有设备的登录状态(包括当前设备)。', message:
'确定要登出所有设备吗?这将清除所有设备的登录状态(包括当前设备)。',
onConfirm: async () => { onConfirm: async () => {
setConfirmDialog({ ...confirmDialog, isOpen: false }); setConfirmDialog({ ...confirmDialog, isOpen: false });
try { try {
@@ -789,7 +923,11 @@ export const UserMenu: React.FC = () => {
const getDeviceIcon = (deviceInfo: string) => { const getDeviceIcon = (deviceInfo: string) => {
const info = deviceInfo.toLowerCase(); const info = deviceInfo.toLowerCase();
if (info.includes('mobile') || info.includes('iphone') || info.includes('android')) { if (
info.includes('mobile') ||
info.includes('iphone') ||
info.includes('android')
) {
return Smartphone; return Smartphone;
} }
@@ -932,11 +1070,22 @@ export const UserMenu: React.FC = () => {
console.error('复制失败:', error); console.error('复制失败:', error);
} }
}; };
useEffect(() => { useEffect(() => {
if (!tvboxToken || !isSubscribeOpen) return; if (!tvboxToken || !isSubscribeOpen) return;
setSubscribeUrl(buildSubscribeUrl(tvboxToken, subscribeAdFilterEnabled, subscribeYellowFilterEnabled)); setSubscribeUrl(
}, [tvboxToken, subscribeAdFilterEnabled, subscribeYellowFilterEnabled, isSubscribeOpen]); buildSubscribeUrl(
tvboxToken,
subscribeAdFilterEnabled,
subscribeYellowFilterEnabled
)
);
}, [
tvboxToken,
subscribeAdFilterEnabled,
subscribeYellowFilterEnabled,
isSubscribeOpen,
]);
const handleSubmitChangePassword = async () => { const handleSubmitChangePassword = async () => {
setPasswordError(''); setPasswordError('');
@@ -1043,11 +1192,16 @@ export const UserMenu: React.FC = () => {
const handleDownloadModeChange = (mode: 'browser' | 'filesystem') => { const handleDownloadModeChange = (mode: 'browser' | 'filesystem') => {
// 如果选择 filesystem 模式,先检测浏览器是否支持 // 如果选择 filesystem 模式,先检测浏览器是否支持
if (mode === 'filesystem' && typeof window !== 'undefined' && !('showDirectoryPicker' in window)) { if (
mode === 'filesystem' &&
typeof window !== 'undefined' &&
!('showDirectoryPicker' in window)
) {
setConfirmDialog({ setConfirmDialog({
isOpen: true, isOpen: true,
title: '浏览器不支持', title: '浏览器不支持',
message: '您的浏览器不支持 File System Access API,请使用 Chrome 86+ 或 Edge 86+', message:
'您的浏览器不支持 File System Access API,请使用 Chrome 86+ 或 Edge 86+',
onConfirm: () => { onConfirm: () => {
setConfirmDialog({ ...confirmDialog, isOpen: false }); setConfirmDialog({ ...confirmDialog, isOpen: false });
}, },
@@ -1085,18 +1239,28 @@ export const UserMenu: React.FC = () => {
// 创建 activeTasks 表(如果不存在) // 创建 activeTasks 表(如果不存在)
if (!db.objectStoreNames.contains('activeTasks')) { if (!db.objectStoreNames.contains('activeTasks')) {
const activeStore = db.createObjectStore('activeTasks', { keyPath: 'id' }); const activeStore = db.createObjectStore('activeTasks', {
keyPath: 'id',
});
activeStore.createIndex('status', 'status', { unique: false }); activeStore.createIndex('status', 'status', { unique: false });
activeStore.createIndex('createdAt', 'createdAt', { unique: false }); activeStore.createIndex('createdAt', 'createdAt', {
unique: false,
});
} }
// 创建 completedTasks 表(如果不存在) // 创建 completedTasks 表(如果不存在)
if (!db.objectStoreNames.contains('completedTasks')) { if (!db.objectStoreNames.contains('completedTasks')) {
const completedStore = db.createObjectStore('completedTasks', { keyPath: 'id' }); const completedStore = db.createObjectStore('completedTasks', {
keyPath: 'id',
});
completedStore.createIndex('source', 'source', { unique: false }); completedStore.createIndex('source', 'source', { unique: false });
completedStore.createIndex('videoId', 'videoId', { unique: false }); completedStore.createIndex('videoId', 'videoId', { unique: false });
completedStore.createIndex('completedAt', 'completedAt', { unique: false }); completedStore.createIndex('completedAt', 'completedAt', {
completedStore.createIndex('sourceVideoId', ['source', 'videoId'], { unique: false }); unique: false,
});
completedStore.createIndex('sourceVideoId', ['source', 'videoId'], {
unique: false,
});
} }
}; };
@@ -1280,6 +1444,14 @@ export const UserMenu: React.FC = () => {
} }
}; };
const handleHomeBannerHeightScaleChange = (value: HomeBannerHeightScale) => {
setHomeBannerHeightScale(value);
if (typeof window !== 'undefined') {
localStorage.setItem('homeBannerHeightScale', value);
window.dispatchEvent(new CustomEvent('homeModulesUpdated'));
}
};
const handleHomeContinueWatchingToggle = (value: boolean) => { const handleHomeContinueWatchingToggle = (value: boolean) => {
setHomeContinueWatchingEnabled(value); setHomeContinueWatchingEnabled(value);
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@@ -1290,7 +1462,7 @@ export const UserMenu: React.FC = () => {
// 首页模块配置处理函数 // 首页模块配置处理函数
const handleHomeModuleToggle = (id: string, enabled: boolean) => { const handleHomeModuleToggle = (id: string, enabled: boolean) => {
const updatedModules = homeModules.map(module => const updatedModules = homeModules.map((module) =>
module.id === id ? { ...module, enabled } : module module.id === id ? { ...module, enabled } : module
); );
setHomeModules(updatedModules); setHomeModules(updatedModules);
@@ -1356,11 +1528,13 @@ export const UserMenu: React.FC = () => {
const handleResetSettings = () => { const handleResetSettings = () => {
const defaultDoubanProxyType = const defaultDoubanProxyType =
(window as any).RUNTIME_CONFIG?.DOUBAN_PROXY_TYPE || 'cmliussss-cdn-tencent'; (window as any).RUNTIME_CONFIG?.DOUBAN_PROXY_TYPE ||
'cmliussss-cdn-tencent';
const defaultDoubanProxy = const defaultDoubanProxy =
(window as any).RUNTIME_CONFIG?.DOUBAN_PROXY || ''; (window as any).RUNTIME_CONFIG?.DOUBAN_PROXY || '';
const defaultDoubanImageProxyType = const defaultDoubanImageProxyType =
(window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY_TYPE || 'cmliussss-cdn-tencent'; (window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY_TYPE ||
'cmliussss-cdn-tencent';
const defaultDoubanImageProxyUrl = const defaultDoubanImageProxyUrl =
(window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY || ''; (window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY || '';
const defaultFluidSearch = const defaultFluidSearch =
@@ -1390,6 +1564,7 @@ export const UserMenu: React.FC = () => {
false; false;
setDisableAutoLoadDanmaku(!defaultDanmakuAutoLoad); setDisableAutoLoadDanmaku(!defaultDanmakuAutoLoad);
setHomeBannerEnabled(true); setHomeBannerEnabled(true);
setHomeBannerHeightScale('1');
setHomeContinueWatchingEnabled(true); setHomeContinueWatchingEnabled(true);
setHomeModules(defaultHomeModules); setHomeModules(defaultHomeModules);
setSearchTraditionalToSimplified(false); setSearchTraditionalToSimplified(false);
@@ -1421,6 +1596,7 @@ export const UserMenu: React.FC = () => {
localStorage.setItem('danmakuMaxCount', '0'); localStorage.setItem('danmakuMaxCount', '0');
localStorage.setItem('danmaku_heatmap_disabled', 'false'); localStorage.setItem('danmaku_heatmap_disabled', 'false');
localStorage.setItem('homeBannerEnabled', 'true'); localStorage.setItem('homeBannerEnabled', 'true');
localStorage.setItem('homeBannerHeightScale', '1');
localStorage.setItem('homeContinueWatchingEnabled', 'true'); localStorage.setItem('homeContinueWatchingEnabled', 'true');
localStorage.setItem('homeModules', JSON.stringify(defaultHomeModules)); localStorage.setItem('homeModules', JSON.stringify(defaultHomeModules));
localStorage.setItem('searchTraditionalToSimplified', 'false'); localStorage.setItem('searchTraditionalToSimplified', 'false');
@@ -1495,8 +1671,8 @@ export const UserMenu: React.FC = () => {
currentRole === 'owner' currentRole === 'owner'
? 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300' ? 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'
: currentRole === 'admin' : currentRole === 'admin'
? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'
: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'; : 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
const handleOpenProfileCenter = () => { const handleOpenProfileCenter = () => {
setIsOpen(false); setIsOpen(false);
@@ -1542,7 +1718,9 @@ export const UserMenu: React.FC = () => {
<div className='text-[10px] text-gray-400 dark:text-gray-500'> <div className='text-[10px] text-gray-400 dark:text-gray-500'>
<div></div> <div></div>
<div className='mt-0.5'> <div className='mt-0.5'>
{displayStorageType === 'localstorage' ? '本地' : displayStorageType} {displayStorageType === 'localstorage'
? '本地'
: displayStorageType}
</div> </div>
</div> </div>
</div> </div>
@@ -1666,12 +1844,13 @@ export const UserMenu: React.FC = () => {
updateStatus && updateStatus &&
updateStatus !== UpdateStatus.FETCH_FAILED && ( updateStatus !== UpdateStatus.FETCH_FAILED && (
<div <div
className={`w-2 h-2 rounded-full -translate-y-2 ${updateStatus === UpdateStatus.HAS_UPDATE className={`w-2 h-2 rounded-full -translate-y-2 ${
? 'bg-yellow-500' updateStatus === UpdateStatus.HAS_UPDATE
: updateStatus === UpdateStatus.NO_UPDATE ? 'bg-yellow-500'
: updateStatus === UpdateStatus.NO_UPDATE
? 'bg-green-400' ? 'bg-green-400'
: '' : ''
}`} }`}
></div> ></div>
)} )}
</div> </div>
@@ -1702,9 +1881,7 @@ export const UserMenu: React.FC = () => {
/> />
{/* 设置面板 */} {/* 设置面板 */}
<div <div className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-xl max-h-[90vh] bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] flex flex-col'>
className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-xl max-h-[90vh] bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] flex flex-col'
>
{/* 内容容器 - 独立的滚动区域 */} {/* 内容容器 - 独立的滚动区域 */}
<div <div
className='flex-1 px-4 py-6 md:p-6 overflow-y-auto' className='flex-1 px-4 py-6 md:p-6 overflow-y-auto'
@@ -1773,7 +1950,9 @@ export const UserMenu: React.FC = () => {
{/* 自定义下拉选择框 */} {/* 自定义下拉选择框 */}
<button <button
type='button' type='button'
onClick={() => setIsDoubanDropdownOpen(!isDoubanDropdownOpen)} onClick={() =>
setIsDoubanDropdownOpen(!isDoubanDropdownOpen)
}
className='w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left' className='w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left'
> >
{ {
@@ -1786,8 +1965,9 @@ export const UserMenu: React.FC = () => {
{/* 下拉箭头 */} {/* 下拉箭头 */}
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'> <div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
<ChevronDown <ChevronDown
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${isDoubanDropdownOpen ? 'rotate-180' : '' className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${
}`} isDoubanDropdownOpen ? 'rotate-180' : ''
}`}
/> />
</div> </div>
@@ -1802,10 +1982,11 @@ export const UserMenu: React.FC = () => {
handleDoubanDataSourceChange(option.value); handleDoubanDataSourceChange(option.value);
setIsDoubanDropdownOpen(false); setIsDoubanDropdownOpen(false);
}} }}
className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${doubanDataSource === option.value className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400' doubanDataSource === option.value
: 'text-gray-900 dark:text-gray-100' ? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
}`} : 'text-gray-900 dark:text-gray-100'
}`}
> >
<span className='truncate'>{option.label}</span> <span className='truncate'>{option.label}</span>
{doubanDataSource === option.value && ( {doubanDataSource === option.value && (
@@ -1823,7 +2004,10 @@ export const UserMenu: React.FC = () => {
<button <button
type='button' type='button'
onClick={() => onClick={() =>
window.open(getThanksInfo(doubanDataSource)!.url, '_blank') window.open(
getThanksInfo(doubanDataSource)!.url,
'_blank'
)
} }
className='flex items-center justify-center gap-1.5 w-full px-3 text-xs text-gray-500 dark:text-gray-400 cursor-pointer' className='flex items-center justify-center gap-1.5 w-full px-3 text-xs text-gray-500 dark:text-gray-400 cursor-pointer'
> >
@@ -1852,7 +2036,9 @@ export const UserMenu: React.FC = () => {
className='w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500' className='w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500'
placeholder='例如: https://proxy.example.com/fetch?url=' placeholder='例如: https://proxy.example.com/fetch?url='
value={doubanProxyUrl} value={doubanProxyUrl}
onChange={(e) => handleDoubanProxyUrlChange(e.target.value)} onChange={(e) =>
handleDoubanProxyUrlChange(e.target.value)
}
/> />
{!doubanProxyUrl.trim() && ( {!doubanProxyUrl.trim() && (
<p className='text-xs text-amber-600 dark:text-amber-400 mt-1'> <p className='text-xs text-amber-600 dark:text-amber-400 mt-1'>
@@ -1878,7 +2064,9 @@ export const UserMenu: React.FC = () => {
<button <button
type='button' type='button'
onClick={() => onClick={() =>
setIsDoubanBackupDropdownOpen(!isDoubanBackupDropdownOpen) setIsDoubanBackupDropdownOpen(
!isDoubanBackupDropdownOpen
)
} }
className='w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left' className='w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left'
> >
@@ -1890,8 +2078,9 @@ export const UserMenu: React.FC = () => {
</button> </button>
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'> <div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
<ChevronDown <ChevronDown
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${isDoubanBackupDropdownOpen ? 'rotate-180' : '' className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${
}`} isDoubanBackupDropdownOpen ? 'rotate-180' : ''
}`}
/> />
</div> </div>
{isDoubanBackupDropdownOpen && ( {isDoubanBackupDropdownOpen && (
@@ -1901,13 +2090,16 @@ export const UserMenu: React.FC = () => {
key={option.value} key={option.value}
type='button' type='button'
onClick={() => { onClick={() => {
handleDoubanDataSourceBackupChange(option.value); handleDoubanDataSourceBackupChange(
option.value
);
setIsDoubanBackupDropdownOpen(false); setIsDoubanBackupDropdownOpen(false);
}} }}
className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${doubanDataSourceBackup === option.value className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400' doubanDataSourceBackup === option.value
: 'text-gray-900 dark:text-gray-100' ? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
}`} : 'text-gray-900 dark:text-gray-100'
}`}
> >
<span className='truncate'>{option.label}</span> <span className='truncate'>{option.label}</span>
{doubanDataSourceBackup === option.value && ( {doubanDataSourceBackup === option.value && (
@@ -1960,7 +2152,10 @@ export const UserMenu: React.FC = () => {
</p> </p>
</div> </div>
<div className='relative' data-dropdown='douban-image-proxy'> <div
className='relative'
data-dropdown='douban-image-proxy'
>
{/* 自定义下拉选择框 */} {/* 自定义下拉选择框 */}
<button <button
type='button' type='button'
@@ -1981,8 +2176,9 @@ export const UserMenu: React.FC = () => {
{/* 下拉箭头 */} {/* 下拉箭头 */}
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'> <div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
<ChevronDown <ChevronDown
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${isDoubanDropdownOpen ? 'rotate-180' : '' className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${
}`} isDoubanDropdownOpen ? 'rotate-180' : ''
}`}
/> />
</div> </div>
@@ -1997,10 +2193,11 @@ export const UserMenu: React.FC = () => {
handleDoubanImageProxyTypeChange(option.value); handleDoubanImageProxyTypeChange(option.value);
setIsDoubanImageProxyDropdownOpen(false); setIsDoubanImageProxyDropdownOpen(false);
}} }}
className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${doubanImageProxyType === option.value className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400' doubanImageProxyType === option.value
: 'text-gray-900 dark:text-gray-100' ? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
}`} : 'text-gray-900 dark:text-gray-100'
}`}
> >
<span className='truncate'>{option.label}</span> <span className='truncate'>{option.label}</span>
{doubanImageProxyType === option.value && ( {doubanImageProxyType === option.value && (
@@ -2086,14 +2283,18 @@ export const UserMenu: React.FC = () => {
> >
{ {
doubanImageProxyTypeOptions.find( doubanImageProxyTypeOptions.find(
(option) => option.value === doubanImageProxyTypeBackup (option) =>
option.value === doubanImageProxyTypeBackup
)?.label )?.label
} }
</button> </button>
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'> <div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
<ChevronDown <ChevronDown
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${isDoubanImageProxyBackupDropdownOpen ? 'rotate-180' : '' className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${
}`} isDoubanImageProxyBackupDropdownOpen
? 'rotate-180'
: ''
}`}
/> />
</div> </div>
{isDoubanImageProxyBackupDropdownOpen && ( {isDoubanImageProxyBackupDropdownOpen && (
@@ -2103,13 +2304,16 @@ export const UserMenu: React.FC = () => {
key={option.value} key={option.value}
type='button' type='button'
onClick={() => { onClick={() => {
handleDoubanImageProxyTypeBackupChange(option.value); handleDoubanImageProxyTypeBackupChange(
option.value
);
setIsDoubanImageProxyBackupDropdownOpen(false); setIsDoubanImageProxyBackupDropdownOpen(false);
}} }}
className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${doubanImageProxyTypeBackup === option.value className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400' doubanImageProxyTypeBackup === option.value
: 'text-gray-900 dark:text-gray-100' ? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
}`} : 'text-gray-900 dark:text-gray-100'
}`}
> >
<span className='truncate'>{option.label}</span> <span className='truncate'>{option.label}</span>
{doubanImageProxyTypeBackup === option.value && ( {doubanImageProxyTypeBackup === option.value && (
@@ -2211,7 +2415,9 @@ export const UserMenu: React.FC = () => {
type='checkbox' type='checkbox'
className='sr-only peer' className='sr-only peer'
checked={defaultAggregateSearch} checked={defaultAggregateSearch}
onChange={(e) => handleAggregateToggle(e.target.checked)} onChange={(e) =>
handleAggregateToggle(e.target.checked)
}
/> />
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div> <div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div> <div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
@@ -2235,7 +2441,9 @@ export const UserMenu: React.FC = () => {
type='checkbox' type='checkbox'
className='sr-only peer' className='sr-only peer'
checked={enableOptimization} checked={enableOptimization}
onChange={(e) => handleOptimizationToggle(e.target.checked)} onChange={(e) =>
handleOptimizationToggle(e.target.checked)
}
/> />
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div> <div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div> <div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
@@ -2293,35 +2501,57 @@ export const UserMenu: React.FC = () => {
max='30000' max='30000'
step='1000' step='1000'
value={speedTestTimeout} value={speedTestTimeout}
onChange={(e) => handleSpeedTestTimeoutChange(Number(e.target.value))} onChange={(e) =>
handleSpeedTestTimeoutChange(Number(e.target.value))
}
className='flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700' className='flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700'
style={{ style={{
background: `linear-gradient(to right, #10b981 0%, #10b981 ${((speedTestTimeout - 4000) / (30000 - 4000)) * 100}%, #e5e7eb ${((speedTestTimeout - 4000) / (30000 - 4000)) * 100}%, #e5e7eb 100%)` background: `linear-gradient(to right, #10b981 0%, #10b981 ${
((speedTestTimeout - 4000) / (30000 - 4000)) * 100
}%, #e5e7eb ${
((speedTestTimeout - 4000) / (30000 - 4000)) * 100
}%, #e5e7eb 100%)`,
}} }}
/> />
</div> </div>
<div className='flex justify-between text-xs text-gray-500 dark:text-gray-400'> <div className='flex justify-between text-xs text-gray-500 dark:text-gray-400'>
<button <button
onClick={() => handleSpeedTestTimeoutChange(4000)} onClick={() => handleSpeedTestTimeoutChange(4000)}
className={`px-2 py-0.5 rounded ${speedTestTimeout === 4000 ? 'bg-green-500 text-white' : 'hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`px-2 py-0.5 rounded ${
speedTestTimeout === 4000
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
> >
4 4
</button> </button>
<button <button
onClick={() => handleSpeedTestTimeoutChange(10000)} onClick={() => handleSpeedTestTimeoutChange(10000)}
className={`px-2 py-0.5 rounded ${speedTestTimeout === 10000 ? 'bg-green-500 text-white' : 'hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`px-2 py-0.5 rounded ${
speedTestTimeout === 10000
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
> >
10 10
</button> </button>
<button <button
onClick={() => handleSpeedTestTimeoutChange(20000)} onClick={() => handleSpeedTestTimeoutChange(20000)}
className={`px-2 py-0.5 rounded ${speedTestTimeout === 20000 ? 'bg-green-500 text-white' : 'hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`px-2 py-0.5 rounded ${
speedTestTimeout === 20000
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
> >
20 20
</button> </button>
<button <button
onClick={() => handleSpeedTestTimeoutChange(30000)} onClick={() => handleSpeedTestTimeoutChange(30000)}
className={`px-2 py-0.5 rounded ${speedTestTimeout === 30000 ? 'bg-green-500 text-white' : 'hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`px-2 py-0.5 rounded ${
speedTestTimeout === 30000
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
> >
30 30
</button> </button>
@@ -2348,7 +2578,9 @@ export const UserMenu: React.FC = () => {
type='checkbox' type='checkbox'
className='sr-only peer' className='sr-only peer'
checked={fluidSearch} checked={fluidSearch}
onChange={(e) => handleFluidSearchToggle(e.target.checked)} onChange={(e) =>
handleFluidSearchToggle(e.target.checked)
}
/> />
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div> <div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div> <div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
@@ -2372,7 +2604,9 @@ export const UserMenu: React.FC = () => {
type='checkbox' type='checkbox'
className='sr-only peer' className='sr-only peer'
checked={tmdbBackdropDisabled} checked={tmdbBackdropDisabled}
onChange={(e) => handleTmdbBackdropDisabledToggle(e.target.checked)} onChange={(e) =>
handleTmdbBackdropDisabledToggle(e.target.checked)
}
/> />
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div> <div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div> <div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
@@ -2396,7 +2630,9 @@ export const UserMenu: React.FC = () => {
type='checkbox' type='checkbox'
className='sr-only peer' className='sr-only peer'
checked={enableTrailers} checked={enableTrailers}
onChange={(e) => handleEnableTrailersToggle(e.target.checked)} onChange={(e) =>
handleEnableTrailersToggle(e.target.checked)
}
/> />
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div> <div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div> <div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
@@ -2420,7 +2656,11 @@ export const UserMenu: React.FC = () => {
type='checkbox' type='checkbox'
className='sr-only peer' className='sr-only peer'
checked={searchTraditionalToSimplified} checked={searchTraditionalToSimplified}
onChange={(e) => handleSearchTraditionalToSimplifiedToggle(e.target.checked)} onChange={(e) =>
handleSearchTraditionalToSimplifiedToggle(
e.target.checked
)
}
/> />
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div> <div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div> <div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
@@ -2444,7 +2684,9 @@ export const UserMenu: React.FC = () => {
type='checkbox' type='checkbox'
className='sr-only peer' className='sr-only peer'
checked={exactSearch} checked={exactSearch}
onChange={(e) => handleExactSearchToggle(e.target.checked)} onChange={(e) =>
handleExactSearchToggle(e.target.checked)
}
/> />
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div> <div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div> <div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
@@ -2500,23 +2742,39 @@ export const UserMenu: React.FC = () => {
max='10' max='10'
step='1' step='1'
value={maxConcurrentDownloads} value={maxConcurrentDownloads}
onChange={(e) => handleMaxConcurrentDownloadsChange(Number(e.target.value))} onChange={(e) =>
handleMaxConcurrentDownloadsChange(
Number(e.target.value)
)
}
className='flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700' className='flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700'
style={{ style={{
background: `linear-gradient(to right, #10b981 0%, #10b981 ${((maxConcurrentDownloads - 1) / (10 - 1)) * 100}%, #e5e7eb ${((maxConcurrentDownloads - 1) / (10 - 1)) * 100}%, #e5e7eb 100%)` background: `linear-gradient(to right, #10b981 0%, #10b981 ${
((maxConcurrentDownloads - 1) / (10 - 1)) * 100
}%, #e5e7eb ${
((maxConcurrentDownloads - 1) / (10 - 1)) * 100
}%, #e5e7eb 100%)`,
}} }}
/> />
</div> </div>
<div className='flex justify-between text-xs text-gray-500 dark:text-gray-400'> <div className='flex justify-between text-xs text-gray-500 dark:text-gray-400'>
<button <button
onClick={() => handleMaxConcurrentDownloadsChange(1)} onClick={() => handleMaxConcurrentDownloadsChange(1)}
className={`px-2 py-0.5 rounded ${maxConcurrentDownloads === 1 ? 'bg-green-500 text-white' : 'hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`px-2 py-0.5 rounded ${
maxConcurrentDownloads === 1
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
> >
1 1
</button> </button>
<button <button
onClick={() => handleMaxConcurrentDownloadsChange(10)} onClick={() => handleMaxConcurrentDownloadsChange(10)}
className={`px-2 py-0.5 rounded ${maxConcurrentDownloads === 10 ? 'bg-green-500 text-white' : 'hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`px-2 py-0.5 rounded ${
maxConcurrentDownloads === 10
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
> >
10 10
</button> </button>
@@ -2548,23 +2806,39 @@ export const UserMenu: React.FC = () => {
max='32' max='32'
step='1' step='1'
value={downloadThreadsPerTask} value={downloadThreadsPerTask}
onChange={(e) => handleDownloadThreadsPerTaskChange(Number(e.target.value))} onChange={(e) =>
handleDownloadThreadsPerTaskChange(
Number(e.target.value)
)
}
className='flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700' className='flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700'
style={{ style={{
background: `linear-gradient(to right, #10b981 0%, #10b981 ${((downloadThreadsPerTask - 1) / (32 - 1)) * 100}%, #e5e7eb ${((downloadThreadsPerTask - 1) / (32 - 1)) * 100}%, #e5e7eb 100%)` background: `linear-gradient(to right, #10b981 0%, #10b981 ${
((downloadThreadsPerTask - 1) / (32 - 1)) * 100
}%, #e5e7eb ${
((downloadThreadsPerTask - 1) / (32 - 1)) * 100
}%, #e5e7eb 100%)`,
}} }}
/> />
</div> </div>
<div className='flex justify-between text-xs text-gray-500 dark:text-gray-400'> <div className='flex justify-between text-xs text-gray-500 dark:text-gray-400'>
<button <button
onClick={() => handleDownloadThreadsPerTaskChange(1)} onClick={() => handleDownloadThreadsPerTaskChange(1)}
className={`px-2 py-0.5 rounded ${downloadThreadsPerTask === 1 ? 'bg-green-500 text-white' : 'hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`px-2 py-0.5 rounded ${
downloadThreadsPerTask === 1
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
> >
1 1
</button> </button>
<button <button
onClick={() => handleDownloadThreadsPerTaskChange(32)} onClick={() => handleDownloadThreadsPerTaskChange(32)}
className={`px-2 py-0.5 rounded ${downloadThreadsPerTask === 32 ? 'bg-green-500 text-white' : 'hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`px-2 py-0.5 rounded ${
downloadThreadsPerTask === 32
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
> >
32 32
</button> </button>
@@ -2598,7 +2872,9 @@ export const UserMenu: React.FC = () => {
name='downloadMode' name='downloadMode'
value='filesystem' value='filesystem'
checked={downloadMode === 'filesystem'} checked={downloadMode === 'filesystem'}
onChange={() => handleDownloadModeChange('filesystem')} onChange={() =>
handleDownloadModeChange('filesystem')
}
className='w-4 h-4 text-green-500' className='w-4 h-4 text-green-500'
/> />
<span className='text-sm text-gray-700 dark:text-gray-300'> <span className='text-sm text-gray-700 dark:text-gray-300'>
@@ -2696,27 +2972,58 @@ export const UserMenu: React.FC = () => {
value={getSliderValueFromStrategy(bufferStrategy)} value={getSliderValueFromStrategy(bufferStrategy)}
onChange={(e) => { onChange={(e) => {
const sliderValue = parseInt(e.target.value); const sliderValue = parseInt(e.target.value);
const strategy = getBufferStrategyFromSlider(sliderValue); const strategy =
getBufferStrategyFromSlider(sliderValue);
handleBufferStrategyChange(strategy); handleBufferStrategyChange(strategy);
}} }}
className='w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-green-500' className='w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-green-500'
style={{ style={{
background: `linear-gradient(to right, rgb(34 197 94) 0%, rgb(34 197 94) ${(getSliderValueFromStrategy(bufferStrategy) / 3) * 100}%, rgb(229 231 235) ${(getSliderValueFromStrategy(bufferStrategy) / 3) * 100}%, rgb(229 231 235) 100%)` background: `linear-gradient(to right, rgb(34 197 94) 0%, rgb(34 197 94) ${
(getSliderValueFromStrategy(bufferStrategy) / 3) *
100
}%, rgb(229 231 235) ${
(getSliderValueFromStrategy(bufferStrategy) / 3) *
100
}%, rgb(229 231 235) 100%)`,
}} }}
/> />
{/* 标签显示 */} {/* 标签显示 */}
<div className='flex justify-between text-xs text-gray-500 dark:text-gray-400 px-1'> <div className='flex justify-between text-xs text-gray-500 dark:text-gray-400 px-1'>
<span className={bufferStrategy === 'low' ? 'font-semibold text-green-600 dark:text-green-400' : ''}> <span
className={
bufferStrategy === 'low'
? 'font-semibold text-green-600 dark:text-green-400'
: ''
}
>
</span> </span>
<span className={bufferStrategy === 'medium' ? 'font-semibold text-green-600 dark:text-green-400' : ''}> <span
className={
bufferStrategy === 'medium'
? 'font-semibold text-green-600 dark:text-green-400'
: ''
}
>
</span> </span>
<span className={bufferStrategy === 'high' ? 'font-semibold text-green-600 dark:text-green-400' : ''}> <span
className={
bufferStrategy === 'high'
? 'font-semibold text-green-600 dark:text-green-400'
: ''
}
>
</span> </span>
<span className={bufferStrategy === 'ultra' ? 'font-semibold text-green-600 dark:text-green-400' : ''}> <span
className={
bufferStrategy === 'ultra'
? 'font-semibold text-green-600 dark:text-green-400'
: ''
}
>
</span> </span>
</div> </div>
@@ -2748,7 +3055,9 @@ export const UserMenu: React.FC = () => {
type='checkbox' type='checkbox'
className='sr-only peer' className='sr-only peer'
checked={nextEpisodePreCache} checked={nextEpisodePreCache}
onChange={(e) => handleNextEpisodePreCacheToggle(e.target.checked)} onChange={(e) =>
handleNextEpisodePreCacheToggle(e.target.checked)
}
/> />
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div> <div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div> <div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
@@ -2795,7 +3104,9 @@ export const UserMenu: React.FC = () => {
type='checkbox' type='checkbox'
className='sr-only peer' className='sr-only peer'
checked={disableAutoLoadDanmaku} checked={disableAutoLoadDanmaku}
onChange={(e) => handleDisableAutoLoadDanmakuToggle(e.target.checked)} onChange={(e) =>
handleDisableAutoLoadDanmakuToggle(e.target.checked)
}
/> />
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div> <div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div> <div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
@@ -2819,7 +3130,11 @@ export const UserMenu: React.FC = () => {
type='checkbox' type='checkbox'
className='sr-only peer' className='sr-only peer'
checked={nextEpisodeDanmakuPreload} checked={nextEpisodeDanmakuPreload}
onChange={(e) => handleNextEpisodeDanmakuPreloadToggle(e.target.checked)} onChange={(e) =>
handleNextEpisodeDanmakuPreloadToggle(
e.target.checked
)
}
/> />
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div> <div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div> <div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
@@ -2843,7 +3158,9 @@ export const UserMenu: React.FC = () => {
type='checkbox' type='checkbox'
className='sr-only peer' className='sr-only peer'
checked={danmakuHeatmapDisabled} checked={danmakuHeatmapDisabled}
onChange={(e) => handleDanmakuHeatmapDisabledToggle(e.target.checked)} onChange={(e) =>
handleDanmakuHeatmapDisabledToggle(e.target.checked)
}
/> />
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div> <div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div> <div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
@@ -2858,7 +3175,9 @@ export const UserMenu: React.FC = () => {
</span> </span>
<span className='text-xs font-medium text-gray-700 dark:text-gray-300'> <span className='text-xs font-medium text-gray-700 dark:text-gray-300'>
{danmakuMaxCount === 0 ? '无上限' : `${danmakuMaxCount}`} {danmakuMaxCount === 0
? '无上限'
: `${danmakuMaxCount}`}
</span> </span>
</div> </div>
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
@@ -2868,38 +3187,63 @@ export const UserMenu: React.FC = () => {
max='10000' max='10000'
step='100' step='100'
value={danmakuMaxCount} value={danmakuMaxCount}
onChange={(e) => handleDanmakuMaxCountChange(parseInt(e.target.value))} onChange={(e) =>
handleDanmakuMaxCountChange(parseInt(e.target.value))
}
className='flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700' className='flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700'
style={{ style={{
background: `linear-gradient(to right, #10b981 0%, #10b981 ${(danmakuMaxCount / 10000) * 100}%, #e5e7eb ${(danmakuMaxCount / 10000) * 100}%, #e5e7eb 100%)` background: `linear-gradient(to right, #10b981 0%, #10b981 ${
(danmakuMaxCount / 10000) * 100
}%, #e5e7eb ${
(danmakuMaxCount / 10000) * 100
}%, #e5e7eb 100%)`,
}} }}
/> />
</div> </div>
<div className='relative text-xs text-gray-500 dark:text-gray-400' style={{ height: '24px' }}> <div
className='relative text-xs text-gray-500 dark:text-gray-400'
style={{ height: '24px' }}
>
<button <button
onClick={() => handleDanmakuMaxCountChange(0)} onClick={() => handleDanmakuMaxCountChange(0)}
className={`absolute px-2 py-0.5 rounded ${danmakuMaxCount === 0 ? 'bg-green-500 text-white' : 'hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`absolute px-2 py-0.5 rounded ${
danmakuMaxCount === 0
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
style={{ left: '0%', transform: 'translateX(0%)' }} style={{ left: '0%', transform: 'translateX(0%)' }}
> >
</button> </button>
<button <button
onClick={() => handleDanmakuMaxCountChange(3000)} onClick={() => handleDanmakuMaxCountChange(3000)}
className={`absolute px-2 py-0.5 rounded ${danmakuMaxCount === 3000 ? 'bg-green-500 text-white' : 'hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`absolute px-2 py-0.5 rounded ${
danmakuMaxCount === 3000
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
style={{ left: '30%', transform: 'translateX(-50%)' }} style={{ left: '30%', transform: 'translateX(-50%)' }}
> >
3000 3000
</button> </button>
<button <button
onClick={() => handleDanmakuMaxCountChange(5000)} onClick={() => handleDanmakuMaxCountChange(5000)}
className={`absolute px-2 py-0.5 rounded ${danmakuMaxCount === 5000 ? 'bg-green-500 text-white' : 'hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`absolute px-2 py-0.5 rounded ${
danmakuMaxCount === 5000
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
style={{ left: '50%', transform: 'translateX(-50%)' }} style={{ left: '50%', transform: 'translateX(-50%)' }}
> >
5000 5000
</button> </button>
<button <button
onClick={() => handleDanmakuMaxCountChange(10000)} onClick={() => handleDanmakuMaxCountChange(10000)}
className={`absolute px-2 py-0.5 rounded ${danmakuMaxCount === 10000 ? 'bg-green-500 text-white' : 'hover:bg-gray-200 dark:hover:bg-gray-700'}`} className={`absolute px-2 py-0.5 rounded ${
danmakuMaxCount === 10000
? 'bg-green-500 text-white'
: 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
style={{ left: '100%', transform: 'translateX(-100%)' }} style={{ left: '100%', transform: 'translateX(-100%)' }}
> >
10000 10000
@@ -2935,8 +3279,18 @@ export const UserMenu: React.FC = () => {
</> </>
) : ( ) : (
<> <>
<svg className='w-4 h-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'> <svg
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16' /> className='w-4 h-4'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16'
/>
</svg> </svg>
<span></span> <span></span>
</> </>
@@ -2945,11 +3299,13 @@ export const UserMenu: React.FC = () => {
{/* 成功/失败提示 */} {/* 成功/失败提示 */}
{clearCacheMessage && ( {clearCacheMessage && (
<div className={`text-sm p-3 rounded-lg border ${ <div
clearCacheMessage.includes('成功') className={`text-sm p-3 rounded-lg border ${
? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 text-green-700 dark:text-green-300' clearCacheMessage.includes('成功')
: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-700 dark:text-red-300' ? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 text-green-700 dark:text-green-300'
}`}> : 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-700 dark:text-red-300'
}`}
>
{clearCacheMessage} {clearCacheMessage}
</div> </div>
)} )}
@@ -2980,15 +3336,71 @@ export const UserMenu: React.FC = () => {
<div className='p-3 md:p-4 space-y-4 md:space-y-6'> <div className='p-3 md:p-4 space-y-4 md:space-y-6'>
<div> <div>
<p className='text-xs text-gray-500 dark:text-gray-400 mb-3'> <p className='text-xs text-gray-500 dark:text-gray-400 mb-3'>
</p> </p>
</div> </div>
{/* 首页顶部组件显示 */} {/* 轮播图配置 */}
<div className='space-y-2'> <div className='space-y-2'>
<div>
<h4 className='text-sm font-semibold text-gray-800 dark:text-gray-200'>
</h4>
</div>
<div className='p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 space-y-3'>
<div>
<div className='text-sm font-medium text-gray-900 dark:text-gray-100'>
</div>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</p>
</div>
<div className='grid grid-cols-3 gap-2'>
{homeBannerHeightOptions.map((option) => (
<button
key={option.value}
onClick={() =>
handleHomeBannerHeightScaleChange(option.value)
}
className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
homeBannerHeightScale === option.value
? 'bg-blue-500 border-blue-500 text-white shadow-sm'
: 'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
title={`${option.label}${option.description}`}
>
<span>{option.label}</span>
<span
className={`ml-1 text-xs ${
homeBannerHeightScale === option.value
? 'text-blue-100'
: 'text-gray-500 dark:text-gray-400'
}`}
>
{option.description}
</span>
</button>
))}
</div>
</div>
</div>
{/* 模块显示与排序 */}
<div className='space-y-3 rounded-lg border border-gray-200 dark:border-gray-700 p-3'>
<div>
<h4 className='text-sm font-semibold text-gray-800 dark:text-gray-200'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
/
</p>
</div>
<div className='flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700'> <div className='flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700'>
<button <button
onClick={() => handleHomeBannerToggle(!homeBannerEnabled)} onClick={() =>
handleHomeBannerToggle(!homeBannerEnabled)
}
className='flex-shrink-0' className='flex-shrink-0'
title={homeBannerEnabled ? '点击隐藏' : '点击显示'} title={homeBannerEnabled ? '点击隐藏' : '点击显示'}
> >
@@ -2999,11 +3411,13 @@ export const UserMenu: React.FC = () => {
)} )}
</button> </button>
<div className='flex-1'> <div className='flex-1'>
<span className={`text-sm font-medium ${ <span
homeBannerEnabled className={`text-sm font-medium ${
? 'text-gray-900 dark:text-gray-100' homeBannerEnabled
: 'text-gray-400 dark:text-gray-500' ? 'text-gray-900 dark:text-gray-100'
}`}> : 'text-gray-400 dark:text-gray-500'
}`}
>
</span> </span>
</div> </div>
@@ -3011,9 +3425,15 @@ export const UserMenu: React.FC = () => {
<div className='flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700'> <div className='flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700'>
<button <button
onClick={() => handleHomeContinueWatchingToggle(!homeContinueWatchingEnabled)} onClick={() =>
handleHomeContinueWatchingToggle(
!homeContinueWatchingEnabled
)
}
className='flex-shrink-0' className='flex-shrink-0'
title={homeContinueWatchingEnabled ? '点击隐藏' : '点击显示'} title={
homeContinueWatchingEnabled ? '点击隐藏' : '点击显示'
}
> >
{homeContinueWatchingEnabled ? ( {homeContinueWatchingEnabled ? (
<Eye className='w-5 h-5 text-green-600 dark:text-green-400' /> <Eye className='w-5 h-5 text-green-600 dark:text-green-400' />
@@ -3022,69 +3442,75 @@ export const UserMenu: React.FC = () => {
)} )}
</button> </button>
<div className='flex-1'> <div className='flex-1'>
<span className={`text-sm font-medium ${ <span
homeContinueWatchingEnabled className={`text-sm font-medium ${
? 'text-gray-900 dark:text-gray-100' homeContinueWatchingEnabled
: 'text-gray-400 dark:text-gray-500' ? 'text-gray-900 dark:text-gray-100'
}`}> : 'text-gray-400 dark:text-gray-500'
}`}
>
</span> </span>
</div> </div>
</div> </div>
</div>
{/* 模块列表 */} {/* 模块列表 */}
<div className='space-y-2'> <div className='space-y-2'>
{homeModules.map((module, index) => ( {homeModules.map((module, index) => (
<div <div
key={module.id} key={module.id}
className='flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700' className='flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700'
>
{/* 左侧:显示/隐藏开关 */}
<button
onClick={() => handleHomeModuleToggle(module.id, !module.enabled)}
className='flex-shrink-0'
title={module.enabled ? '点击隐藏' : '点击显示'}
> >
{module.enabled ? ( {/* 左侧:显示/隐藏开关 */}
<Eye className='w-5 h-5 text-green-600 dark:text-green-400' />
) : (
<EyeOff className='w-5 h-5 text-gray-400 dark:text-gray-500' />
)}
</button>
{/* 中间:模块名称 */}
<div className='flex-1'>
<span className={`text-sm font-medium ${
module.enabled
? 'text-gray-900 dark:text-gray-100'
: 'text-gray-400 dark:text-gray-500'
}`}>
{module.name}
</span>
</div>
{/* 右侧:上下移动按钮 */}
<div className='flex gap-1'>
<button <button
onClick={() => handleHomeModuleMoveUp(index)} onClick={() =>
disabled={index === 0} handleHomeModuleToggle(module.id, !module.enabled)
className='p-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-30 disabled:cursor-not-allowed transition-colors' }
title='上移' className='flex-shrink-0'
title={module.enabled ? '点击隐藏' : '点击显示'}
> >
<MoveUp className='w-4 h-4 text-gray-600 dark:text-gray-400' /> {module.enabled ? (
</button> <Eye className='w-5 h-5 text-green-600 dark:text-green-400' />
<button ) : (
onClick={() => handleHomeModuleMoveDown(index)} <EyeOff className='w-5 h-5 text-gray-400 dark:text-gray-500' />
disabled={index === homeModules.length - 1} )}
className='p-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-30 disabled:cursor-not-allowed transition-colors'
title='下移'
>
<MoveDown className='w-4 h-4 text-gray-600 dark:text-gray-400' />
</button> </button>
{/* 中间:模块名称 */}
<div className='flex-1'>
<span
className={`text-sm font-medium ${
module.enabled
? 'text-gray-900 dark:text-gray-100'
: 'text-gray-400 dark:text-gray-500'
}`}
>
{module.name}
</span>
</div>
{/* 右侧:上下移动按钮 */}
<div className='flex gap-1'>
<button
onClick={() => handleHomeModuleMoveUp(index)}
disabled={index === 0}
className='p-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-30 disabled:cursor-not-allowed transition-colors'
title='上移'
>
<MoveUp className='w-4 h-4 text-gray-600 dark:text-gray-400' />
</button>
<button
onClick={() => handleHomeModuleMoveDown(index)}
disabled={index === homeModules.length - 1}
className='p-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-30 disabled:cursor-not-allowed transition-colors'
title='下移'
>
<MoveDown className='w-4 h-4 text-gray-600 dark:text-gray-400' />
</button>
</div>
</div> </div>
</div> ))}
))} </div>
</div> </div>
{/* 恢复默认按钮 */} {/* 恢复默认按钮 */}
@@ -3092,12 +3518,22 @@ export const UserMenu: React.FC = () => {
onClick={() => { onClick={() => {
setHomeModules(defaultHomeModules); setHomeModules(defaultHomeModules);
setHomeBannerEnabled(true); setHomeBannerEnabled(true);
setHomeBannerHeightScale('1');
setHomeContinueWatchingEnabled(true); setHomeContinueWatchingEnabled(true);
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
localStorage.setItem('homeModules', JSON.stringify(defaultHomeModules)); localStorage.setItem(
'homeModules',
JSON.stringify(defaultHomeModules)
);
localStorage.setItem('homeBannerEnabled', 'true'); localStorage.setItem('homeBannerEnabled', 'true');
localStorage.setItem('homeContinueWatchingEnabled', 'true'); localStorage.setItem('homeBannerHeightScale', '1');
window.dispatchEvent(new CustomEvent('homeModulesUpdated')); localStorage.setItem(
'homeContinueWatchingEnabled',
'true'
);
window.dispatchEvent(
new CustomEvent('homeModulesUpdated')
);
} }
}} }}
className='w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-lg transition-colors' className='w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-lg transition-colors'
@@ -3107,7 +3543,10 @@ export const UserMenu: React.FC = () => {
{/* 提示信息 */} {/* 提示信息 */}
<div className='text-xs text-gray-500 dark:text-gray-400 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg'> <div className='text-xs text-gray-500 dark:text-gray-400 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg'>
<p>💡 /使</p> <p>
💡
/使
</p>
</div> </div>
</div> </div>
)} )}
@@ -3144,9 +3583,7 @@ export const UserMenu: React.FC = () => {
/> />
{/* 订阅面板 */} {/* 订阅面板 */}
<div <div className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'>
className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'
>
<div <div
className='h-full p-6' className='h-full p-6'
data-panel-content data-panel-content
@@ -3220,14 +3657,28 @@ export const UserMenu: React.FC = () => {
</div> </div>
</div> </div>
<div className={`relative h-6 w-11 rounded-full transition-colors ${subscribeAdFilterEnabled ? 'bg-green-500' : 'bg-gray-300 dark:bg-gray-600'}`}> <div
<div className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition-transform ${subscribeAdFilterEnabled ? 'translate-x-5' : 'translate-x-0.5'}`} /> className={`relative h-6 w-11 rounded-full transition-colors ${
subscribeAdFilterEnabled
? 'bg-green-500'
: 'bg-gray-300 dark:bg-gray-600'
}`}
>
<div
className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition-transform ${
subscribeAdFilterEnabled
? 'translate-x-5'
: 'translate-x-0.5'
}`}
/>
</div> </div>
</button> </button>
<button <button
type='button' type='button'
onClick={() => setSubscribeYellowFilterEnabled((prev) => !prev)} onClick={() =>
setSubscribeYellowFilterEnabled((prev) => !prev)
}
className='w-full flex items-center justify-between rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-3 text-left bg-gray-50 dark:bg-gray-800/70' className='w-full flex items-center justify-between rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-3 text-left bg-gray-50 dark:bg-gray-800/70'
> >
<div> <div>
@@ -3238,8 +3689,20 @@ export const UserMenu: React.FC = () => {
</div> </div>
</div> </div>
<div className={`relative h-6 w-11 rounded-full transition-colors ${subscribeYellowFilterEnabled ? 'bg-yellow-500' : 'bg-gray-300 dark:bg-gray-600'}`}> <div
<div className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition-transform ${subscribeYellowFilterEnabled ? 'translate-x-5' : 'translate-x-0.5'}`} /> className={`relative h-6 w-11 rounded-full transition-colors ${
subscribeYellowFilterEnabled
? 'bg-yellow-500'
: 'bg-gray-300 dark:bg-gray-600'
}`}
>
<div
className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition-transform ${
subscribeYellowFilterEnabled
? 'translate-x-5'
: 'translate-x-0.5'
}`}
/>
</div> </div>
</button> </button>
</div> </div>
@@ -3263,7 +3726,8 @@ export const UserMenu: React.FC = () => {
{copySuccess ? '已复制' : '复制'} {copySuccess ? '已复制' : '复制'}
</button> </button>
</div> </div>
{(subscribeAdFilterEnabled || subscribeYellowFilterEnabled) && ( {(subscribeAdFilterEnabled ||
subscribeYellowFilterEnabled) && (
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'> <p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
💡 💡
</p> </p>
@@ -3283,7 +3747,10 @@ export const UserMenu: React.FC = () => {
</p> </p>
{/* 消息提示 */} {/* 消息提示 */}
<p id='tvbox-token-message' className='text-xs text-center hidden'></p> <p
id='tvbox-token-message'
className='text-xs text-center hidden'
></p>
</div> </div>
</> </>
)} )}
@@ -3321,9 +3788,7 @@ export const UserMenu: React.FC = () => {
/> />
{/* 修改密码面板 */} {/* 修改密码面板 */}
<div <div className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'>
className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'
>
{/* 内容容器 - 独立的滚动区域 */} {/* 内容容器 - 独立的滚动区域 */}
<div <div
className='h-full p-6' className='h-full p-6'
@@ -3438,9 +3903,7 @@ export const UserMenu: React.FC = () => {
/> />
{/* 举报信息面板 */} {/* 举报信息面板 */}
<div <div className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-lg bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1003] overflow-hidden'>
className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-lg bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1003] overflow-hidden'
>
<div <div
className='h-full max-h-[70vh] flex flex-col' className='h-full max-h-[70vh] flex flex-col'
data-panel-content data-panel-content
@@ -3469,13 +3932,20 @@ export const UserMenu: React.FC = () => {
<div className='flex-1 overflow-y-auto p-6'> <div className='flex-1 overflow-y-auto p-6'>
<div className='bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4'> <div className='bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4'>
<p className='text-gray-800 dark:text-gray-200 leading-relaxed'> <p className='text-gray-800 dark:text-gray-200 leading-relaxed'>
<span className='font-bold text-red-600 dark:text-red-400'>SzeMeng76</span>commit区
<span className='font-bold text-red-600 dark:text-red-400'>
SzeMeng76
</span>
commit区
</p> </p>
<p className='text-gray-800 dark:text-gray-200 leading-relaxed mt-3'> <p className='text-gray-800 dark:text-gray-200 leading-relaxed mt-3'>
</p> </p>
<p className='text-gray-800 dark:text-gray-200 leading-relaxed mt-3'> <p className='text-gray-800 dark:text-gray-200 leading-relaxed mt-3'>
<span className='font-semibold text-red-600 dark:text-red-400'>2026-02-25</span>emby功能 <span className='font-semibold text-red-600 dark:text-red-400'>
2026-02-25
</span>
emby功能
</p> </p>
</div> </div>
</div> </div>
@@ -3513,9 +3983,7 @@ export const UserMenu: React.FC = () => {
/> />
{/* 生态应用面板 */} {/* 生态应用面板 */}
<div <div className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-2xl bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'>
className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-2xl bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'
>
<div <div
className='h-full max-h-[85vh] flex flex-col' className='h-full max-h-[85vh] flex flex-col'
data-panel-content data-panel-content
@@ -3675,7 +4143,8 @@ export const UserMenu: React.FC = () => {
</h4> </h4>
<p className='text-sm text-gray-600 dark:text-gray-400 mb-3'> <p className='text-sm text-gray-600 dark:text-gray-400 mb-3'>
MKV MKV
</p> </p>
<a <a
href='https://github.com/mtvpls/moontvplus-transcoder/tags' href='https://github.com/mtvpls/moontvplus-transcoder/tags'
@@ -3843,14 +4312,10 @@ export const UserMenu: React.FC = () => {
/> />
{/* 使用 Portal 将生态应用面板渲染到 document.body */} {/* 使用 Portal 将生态应用面板渲染到 document.body */}
{isEcoAppsOpen && {isEcoAppsOpen && mounted && createPortal(ecoAppsPanel, document.body)}
mounted &&
createPortal(ecoAppsPanel, document.body)}
{/* 使用 Portal 将举报信息面板渲染到 document.body */} {/* 使用 Portal 将举报信息面板渲染到 document.body */}
{isReportOpen && {isReportOpen && mounted && createPortal(reportPanel, document.body)}
mounted &&
createPortal(reportPanel, document.body)}
{/* 确认对话框 */} {/* 确认对话框 */}
{confirmDialog.isOpen && {confirmDialog.isOpen &&
@@ -3875,7 +4340,9 @@ export const UserMenu: React.FC = () => {
{/* 按钮 */} {/* 按钮 */}
<div className='p-6 pt-0 flex gap-3 justify-end'> <div className='p-6 pt-0 flex gap-3 justify-end'>
<button <button
onClick={() => setConfirmDialog({ ...confirmDialog, isOpen: false })} onClick={() =>
setConfirmDialog({ ...confirmDialog, isOpen: false })
}
className='px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg transition-colors' className='px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-lg transition-colors'
> >