增加轮播图高度配置
This commit is contained in:
@@ -1,11 +1,21 @@
|
||||
'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 { 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 ProxyImage from '@/components/ProxyImage';
|
||||
@@ -15,6 +25,21 @@ interface BannerCarouselProps {
|
||||
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数据源的额外字段
|
||||
interface BannerItem extends TMDBItem {
|
||||
subtitle?: string; // TX数据源的子标题
|
||||
@@ -23,7 +48,10 @@ interface BannerItem extends TMDBItem {
|
||||
genres?: string[]; // 豆瓣数据源的类型标签
|
||||
}
|
||||
|
||||
export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = false }: BannerCarouselProps) {
|
||||
export default function BannerCarousel({
|
||||
autoPlayInterval = 5000,
|
||||
delayLoad = false,
|
||||
}: BannerCarouselProps) {
|
||||
const router = useRouter();
|
||||
const [items, setItems] = useState<BannerItem[]>([]);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
@@ -36,6 +64,8 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
|
||||
const [dataSource, setDataSource] = useState<string>(''); // 当前数据源
|
||||
const [trailersLoaded, setTrailersLoaded] = useState(false); // 预告片是否已加载
|
||||
const [isMuted, setIsMuted] = useState(true); // 视频是否静音(默认静音)
|
||||
const [bannerHeightScale, setBannerHeightScale] =
|
||||
useState<HomeBannerHeightScale>('1'); // 轮播图高度倍率
|
||||
const videoRef = useRef<HTMLVideoElement>(null); // 视频元素引用
|
||||
const videoRefs = useRef<Map<number, HTMLVideoElement>>(new Map()); // 所有视频元素的引用
|
||||
const touchStartX = useRef(0);
|
||||
@@ -95,6 +125,20 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
|
||||
if (setting !== null) {
|
||||
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)
|
||||
try {
|
||||
localStorage.setItem(cacheKey, JSON.stringify({
|
||||
data: result.list,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
localStorage.setItem(
|
||||
cacheKey,
|
||||
JSON.stringify({
|
||||
data: result.list,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
// localStorage 可能已满,忽略错误
|
||||
console.error('保存到 localStorage 失败:', e);
|
||||
@@ -227,7 +274,12 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
|
||||
// 前端获取豆瓣预告片
|
||||
useEffect(() => {
|
||||
// 只有在启用预告片、数据源是豆瓣、有数据且未加载预告片时才执行
|
||||
if (!enableTrailers || dataSource !== 'Douban' || items.length === 0 || trailersLoaded) {
|
||||
if (
|
||||
!enableTrailers ||
|
||||
dataSource !== 'Douban' ||
|
||||
items.length === 0 ||
|
||||
trailersLoaded
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -241,9 +293,10 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
|
||||
const detail = await getDoubanDetail(item.id.toString());
|
||||
|
||||
// 获取预告片链接(取第一个)
|
||||
const trailerUrl = detail.trailers && detail.trailers.length > 0
|
||||
? detail.trailers[0].video_url
|
||||
: null;
|
||||
const trailerUrl =
|
||||
detail.trailers && detail.trailers.length > 0
|
||||
? detail.trailers[0].video_url
|
||||
: null;
|
||||
|
||||
return {
|
||||
...item,
|
||||
@@ -298,7 +351,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
|
||||
setSkipNextAutoPlay(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setCurrentIndex((prev) => (prev + 1) % items.length);
|
||||
}, autoPlayInterval);
|
||||
|
||||
@@ -350,7 +403,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
|
||||
// 防止在手动切换过程中触发
|
||||
if (isManualChange.current) return;
|
||||
if (!touchStartX.current) return;
|
||||
|
||||
|
||||
// 如果有滑动,则执行滑动逻辑
|
||||
if (touchEndX.current !== 0) {
|
||||
const distance = touchStartX.current - touchEndX.current;
|
||||
@@ -374,13 +427,15 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
|
||||
|
||||
if (isLoading || !shouldLoad) {
|
||||
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
|
||||
src="/logo.png"
|
||||
alt="MoonTVPlus"
|
||||
src='/logo.png'
|
||||
alt='MoonTVPlus'
|
||||
width={120}
|
||||
height={120}
|
||||
className="opacity-50"
|
||||
className='opacity-50'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
@@ -395,7 +450,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
|
||||
|
||||
return (
|
||||
<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)}
|
||||
onMouseLeave={() => setIsPaused(false)}
|
||||
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) => (
|
||||
<div
|
||||
key={item.id}
|
||||
@@ -419,7 +474,7 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
|
||||
>
|
||||
{item.trailer_url && enableTrailers ? (
|
||||
/* 显示豆瓣直链视频 */
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div className='absolute inset-0 overflow-hidden'>
|
||||
<video
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
@@ -429,20 +484,20 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
|
||||
}
|
||||
}}
|
||||
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}
|
||||
loop
|
||||
playsInline
|
||||
preload="metadata"
|
||||
preload='metadata'
|
||||
/>
|
||||
</div>
|
||||
) : item.video_key && isYouTubeAccessible && enableTrailers ? (
|
||||
/* 显示YouTube视频 */
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div className='absolute inset-0 overflow-hidden'>
|
||||
<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`}
|
||||
className="absolute top-1/2 left-1/2 pointer-events-none"
|
||||
allow="autoplay; encrypted-media"
|
||||
className='absolute top-1/2 left-1/2 pointer-events-none'
|
||||
allow='autoplay; encrypted-media'
|
||||
style={{
|
||||
border: 'none',
|
||||
width: '100vw',
|
||||
@@ -456,54 +511,65 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
|
||||
) : (
|
||||
/* 显示图片 */
|
||||
<ProxyImage
|
||||
originalSrc={getImageUrl(item.backdrop_path || item.poster_path)}
|
||||
originalSrc={getImageUrl(
|
||||
item.backdrop_path || item.poster_path
|
||||
)}
|
||||
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'}
|
||||
/>
|
||||
)}
|
||||
{/* 渐变遮罩 */}
|
||||
<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-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>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 内容信息 */}
|
||||
<div className="absolute inset-0 flex items-end p-8 md:p-12 pointer-events-none">
|
||||
<div className="max-w-2xl space-y-4">
|
||||
<h2 className="text-3xl md:text-5xl font-bold text-white drop-shadow-lg">
|
||||
<div className='absolute inset-0 flex items-end p-8 md:p-12 pointer-events-none'>
|
||||
<div className='max-w-2xl space-y-4'>
|
||||
<h2 className='text-3xl md:text-5xl font-bold text-white drop-shadow-lg'>
|
||||
{currentItem.title}
|
||||
</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 && (
|
||||
<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)}
|
||||
</span>
|
||||
)}
|
||||
{/* 显示标签:优先TX的tags,其次豆瓣的genres,最后TMDB的genre_ids */}
|
||||
{currentItem.tags && currentItem.tags.length > 0 ? (
|
||||
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">
|
||||
{tag}
|
||||
</span>
|
||||
))
|
||||
) : currentItem.genres && Array.isArray(currentItem.genres) && currentItem.genres.length > 0 ? (
|
||||
/* 显示豆瓣数据源的标签 */
|
||||
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">
|
||||
{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.tags && currentItem.tags.length > 0
|
||||
? 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'
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))
|
||||
: currentItem.genres &&
|
||||
Array.isArray(currentItem.genres) &&
|
||||
currentItem.genres.length > 0
|
||||
? /* 显示豆瓣数据源的标签 */
|
||||
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'
|
||||
>
|
||||
{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 && (
|
||||
<span>{currentItem.release_date}</span>
|
||||
)}
|
||||
@@ -515,14 +581,14 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
|
||||
e.stopPropagation();
|
||||
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>
|
||||
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
@@ -532,36 +598,36 @@ export default function BannerCarousel({ autoPlayInterval = 5000, delayLoad = fa
|
||||
{/* 左右切换按钮 - 只在桌面端显示 */}
|
||||
<button
|
||||
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"
|
||||
aria-label="上一张"
|
||||
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='上一张'
|
||||
>
|
||||
<ChevronLeft className="w-8 h-8" />
|
||||
<ChevronLeft className='w-8 h-8' />
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
aria-label="下一张"
|
||||
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='下一张'
|
||||
>
|
||||
<ChevronRight className="w-8 h-8" />
|
||||
<ChevronRight className='w-8 h-8' />
|
||||
</button>
|
||||
|
||||
{/* 音量控制按钮 - 只在有豆瓣预告片时显示 */}
|
||||
{currentItem.trailer_url && enableTrailers && (
|
||||
<button
|
||||
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"
|
||||
aria-label={isMuted ? "开启声音" : "关闭声音"}
|
||||
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 ? '开启声音' : '关闭声音'}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* 指示器 */}
|
||||
<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) => (
|
||||
<button
|
||||
key={index}
|
||||
|
||||
+717
-250
@@ -65,17 +65,20 @@ export const UserMenu: React.FC = () => {
|
||||
const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
|
||||
const [isSubscribeOpen, setIsSubscribeOpen] = useState(false);
|
||||
const [isVersionPanelOpen, setIsVersionPanelOpen] = useState(false);
|
||||
const [isOfflineDownloadPanelOpen, setIsOfflineDownloadPanelOpen] = useState(false);
|
||||
const [isOfflineDownloadPanelOpen, setIsOfflineDownloadPanelOpen] =
|
||||
useState(false);
|
||||
const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false);
|
||||
const [isFavoritesPanelOpen, setIsFavoritesPanelOpen] = useState(false);
|
||||
const [isEmailSettingsOpen, setIsEmailSettingsOpen] = useState(false);
|
||||
const [isDeviceManagementOpen, setIsDeviceManagementOpen] = useState(false);
|
||||
const [isEcoAppsOpen, setIsEcoAppsOpen] = 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 [storageType, setStorageType] = useState<string>('localstorage');
|
||||
const [displayStorageType, setDisplayStorageType] = useState<string>('localstorage');
|
||||
const [displayStorageType, setDisplayStorageType] =
|
||||
useState<string>('localstorage');
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
|
||||
@@ -86,12 +89,25 @@ export const UserMenu: React.FC = () => {
|
||||
const [tvboxToken, setTvboxToken] = useState('');
|
||||
const [isResettingToken, setIsResettingToken] = useState(false);
|
||||
const [isLoadingSubscribeUrl, setIsLoadingSubscribeUrl] = useState(false);
|
||||
const [subscribeAdFilterEnabled, setSubscribeAdFilterEnabled] = useState(false);
|
||||
const [subscribeYellowFilterEnabled, setSubscribeYellowFilterEnabled] = useState(false);
|
||||
const [subscribeAdFilterEnabled, setSubscribeAdFilterEnabled] =
|
||||
useState(false);
|
||||
const [subscribeYellowFilterEnabled, setSubscribeYellowFilterEnabled] =
|
||||
useState(false);
|
||||
|
||||
// Body 滚动锁定 - 使用 overflow 方式避免布局问题
|
||||
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 html = document.documentElement;
|
||||
|
||||
@@ -104,13 +120,23 @@ export const UserMenu: React.FC = () => {
|
||||
html.style.overflow = 'hidden';
|
||||
|
||||
return () => {
|
||||
|
||||
// 恢复所有原始样式
|
||||
body.style.overflow = originalBodyOverflow;
|
||||
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);
|
||||
@@ -121,30 +147,44 @@ export const UserMenu: React.FC = () => {
|
||||
const [fluidSearch, setFluidSearch] = useState(true);
|
||||
const [tmdbBackdropDisabled, setTmdbBackdropDisabled] = useState(false);
|
||||
const [enableTrailers, setEnableTrailers] = useState(false);
|
||||
const [doubanDataSource, setDoubanDataSource] = useState('cmliussss-cdn-tencent');
|
||||
const [doubanDataSourceBackup, setDoubanDataSourceBackup] = useState('direct');
|
||||
const [doubanImageProxyType, setDoubanImageProxyType] = useState('cmliussss-cdn-tencent');
|
||||
const [doubanImageProxyTypeBackup, setDoubanImageProxyTypeBackup] = useState('server');
|
||||
const [doubanDataSource, setDoubanDataSource] = useState(
|
||||
'cmliussss-cdn-tencent'
|
||||
);
|
||||
const [doubanDataSourceBackup, setDoubanDataSourceBackup] =
|
||||
useState('direct');
|
||||
const [doubanImageProxyType, setDoubanImageProxyType] = useState(
|
||||
'cmliussss-cdn-tencent'
|
||||
);
|
||||
const [doubanImageProxyTypeBackup, setDoubanImageProxyTypeBackup] =
|
||||
useState('server');
|
||||
const [doubanImageProxyUrl, setDoubanImageProxyUrl] = useState('');
|
||||
const [doubanProxyUrlBackup, setDoubanProxyUrlBackup] = useState('');
|
||||
const [doubanImageProxyUrlBackup, setDoubanImageProxyUrlBackup] = useState('');
|
||||
const [doubanImageProxyUrlBackup, setDoubanImageProxyUrlBackup] =
|
||||
useState('');
|
||||
const [isDoubanDropdownOpen, setIsDoubanDropdownOpen] = useState(false);
|
||||
const [isDoubanBackupDropdownOpen, setIsDoubanBackupDropdownOpen] = useState(false);
|
||||
const [isDoubanBackupDropdownOpen, setIsDoubanBackupDropdownOpen] =
|
||||
useState(false);
|
||||
const [isDoubanImageProxyDropdownOpen, setIsDoubanImageProxyDropdownOpen] =
|
||||
useState(false);
|
||||
const [isDoubanImageProxyBackupDropdownOpen, setIsDoubanImageProxyBackupDropdownOpen] =
|
||||
useState(false);
|
||||
const [
|
||||
isDoubanImageProxyBackupDropdownOpen,
|
||||
setIsDoubanImageProxyBackupDropdownOpen,
|
||||
] = useState(false);
|
||||
const [bufferStrategy, setBufferStrategy] = useState('medium');
|
||||
const [nextEpisodePreCache, setNextEpisodePreCache] = useState(true);
|
||||
const [nextEpisodeDanmakuPreload, setNextEpisodeDanmakuPreload] = useState(true);
|
||||
const [nextEpisodeDanmakuPreload, setNextEpisodeDanmakuPreload] =
|
||||
useState(true);
|
||||
const [disableAutoLoadDanmaku, setDisableAutoLoadDanmaku] = useState(false);
|
||||
const [danmakuMaxCount, setDanmakuMaxCount] = useState(0);
|
||||
const [danmakuHeatmapDisabled, setDanmakuHeatmapDisabled] = useState(false);
|
||||
const [searchTraditionalToSimplified, setSearchTraditionalToSimplified] = useState(false);
|
||||
const [searchTraditionalToSimplified, setSearchTraditionalToSimplified] =
|
||||
useState(false);
|
||||
const [exactSearch, setExactSearch] = useState(true);
|
||||
const [maxConcurrentDownloads, setMaxConcurrentDownloads] = 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>('');
|
||||
|
||||
// 邮件通知设置
|
||||
@@ -179,7 +219,9 @@ export const UserMenu: React.FC = () => {
|
||||
const [isDoubanSectionOpen, setIsDoubanSectionOpen] = useState(false);
|
||||
|
||||
// TMDB 图片设置
|
||||
const [tmdbImageBaseUrl, setTmdbImageBaseUrl] = useState('https://image.tmdb.org');
|
||||
const [tmdbImageBaseUrl, setTmdbImageBaseUrl] = useState(
|
||||
'https://image.tmdb.org'
|
||||
);
|
||||
const [isUsageSectionOpen, setIsUsageSectionOpen] = useState(false);
|
||||
const [isDownloadSectionOpen, setIsDownloadSectionOpen] = useState(false);
|
||||
const [isBufferSectionOpen, setIsBufferSectionOpen] = useState(false);
|
||||
@@ -194,6 +236,8 @@ export const UserMenu: React.FC = () => {
|
||||
order: number;
|
||||
}
|
||||
|
||||
type HomeBannerHeightScale = '1' | '1.5' | '2';
|
||||
|
||||
const defaultHomeModules: HomeModule[] = [
|
||||
{ id: 'hotMovies', name: '热门电影', enabled: true, order: 0 },
|
||||
{ id: 'hotDuanju', name: '热播短剧', enabled: true, order: 1 },
|
||||
@@ -203,9 +247,23 @@ export const UserMenu: React.FC = () => {
|
||||
{ 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 [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 = [
|
||||
@@ -229,8 +287,14 @@ export const UserMenu: React.FC = () => {
|
||||
{ value: 'cmliussss-cdn-ali', label: '豆瓣 CDN By CMLiussss(阿里云)' },
|
||||
{ value: 'baidu', 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 [clearCacheMessage, setClearCacheMessage] = useState<string | null>(null);
|
||||
const [clearCacheMessage, setClearCacheMessage] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [danmakuCacheUsage, setDanmakuCacheUsage] = useState('计算中...');
|
||||
|
||||
// 确保组件已挂载
|
||||
@@ -300,7 +366,10 @@ export const UserMenu: React.FC = () => {
|
||||
if (globalWindow.__loadingNotifications) {
|
||||
// 如果正在加载,等待加载完成后获取结果
|
||||
const checkInterval = setInterval(() => {
|
||||
if (!globalWindow.__loadingNotifications && globalWindow.__unreadNotificationCount !== undefined) {
|
||||
if (
|
||||
!globalWindow.__loadingNotifications &&
|
||||
globalWindow.__unreadNotificationCount !== undefined
|
||||
) {
|
||||
setUnreadCount(globalWindow.__unreadNotificationCount);
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
@@ -340,14 +409,18 @@ export const UserMenu: React.FC = () => {
|
||||
|
||||
window.addEventListener('notificationsUpdated', handleNotificationsUpdated);
|
||||
return () => {
|
||||
window.removeEventListener('notificationsUpdated', handleNotificationsUpdated);
|
||||
window.removeEventListener(
|
||||
'notificationsUpdated',
|
||||
handleNotificationsUpdated
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 从运行时配置读取订阅是否启用
|
||||
useEffect(() => {
|
||||
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);
|
||||
}
|
||||
}, []);
|
||||
@@ -363,7 +436,13 @@ export const UserMenu: React.FC = () => {
|
||||
const token = data.token;
|
||||
setTvboxToken(token);
|
||||
|
||||
setSubscribeUrl(buildSubscribeUrl(token, subscribeAdFilterEnabled, subscribeYellowFilterEnabled));
|
||||
setSubscribeUrl(
|
||||
buildSubscribeUrl(
|
||||
token,
|
||||
subscribeAdFilterEnabled,
|
||||
subscribeYellowFilterEnabled
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取订阅URL失败:', error);
|
||||
@@ -393,11 +472,18 @@ export const UserMenu: React.FC = () => {
|
||||
const token = data.token;
|
||||
setTvboxToken(token);
|
||||
|
||||
setSubscribeUrl(buildSubscribeUrl(token, subscribeAdFilterEnabled, subscribeYellowFilterEnabled));
|
||||
setSubscribeUrl(
|
||||
buildSubscribeUrl(
|
||||
token,
|
||||
subscribeAdFilterEnabled,
|
||||
subscribeYellowFilterEnabled
|
||||
)
|
||||
);
|
||||
|
||||
if (messageEl) {
|
||||
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');
|
||||
setTimeout(() => {
|
||||
messageEl.classList.add('hidden');
|
||||
@@ -407,7 +493,8 @@ export const UserMenu: React.FC = () => {
|
||||
const data = await response.json();
|
||||
if (messageEl) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -416,7 +503,8 @@ export const UserMenu: React.FC = () => {
|
||||
const messageEl = document.getElementById('tvbox-token-message');
|
||||
if (messageEl) {
|
||||
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');
|
||||
}
|
||||
} 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 url = new URL('/api/tvbox/subscribe', currentOrigin);
|
||||
url.searchParams.set('token', token);
|
||||
@@ -465,7 +557,8 @@ export const UserMenu: React.FC = () => {
|
||||
|
||||
const savedDoubanDataSource = localStorage.getItem('doubanDataSource');
|
||||
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) {
|
||||
setDoubanDataSource(savedDoubanDataSource);
|
||||
} else if (defaultDoubanProxyType) {
|
||||
@@ -495,7 +588,8 @@ export const UserMenu: React.FC = () => {
|
||||
'doubanImageProxyType'
|
||||
);
|
||||
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) {
|
||||
setDoubanImageProxyType(savedDoubanImageProxyType);
|
||||
} else if (defaultDoubanImageProxyType) {
|
||||
@@ -516,7 +610,9 @@ export const UserMenu: React.FC = () => {
|
||||
const savedDoubanImageProxyTypeBackup = localStorage.getItem(
|
||||
'doubanImageProxyTypeBackup'
|
||||
);
|
||||
setDoubanImageProxyTypeBackup(savedDoubanImageProxyTypeBackup || 'server');
|
||||
setDoubanImageProxyTypeBackup(
|
||||
savedDoubanImageProxyTypeBackup || 'server'
|
||||
);
|
||||
|
||||
const savedDoubanImageProxyUrlBackup = localStorage.getItem(
|
||||
'doubanImageProxyUrlBackup'
|
||||
@@ -553,7 +649,9 @@ export const UserMenu: React.FC = () => {
|
||||
setFluidSearch(defaultFluidSearch);
|
||||
}
|
||||
|
||||
const savedTmdbBackdropDisabled = localStorage.getItem('tmdb_backdrop_disabled');
|
||||
const savedTmdbBackdropDisabled = localStorage.getItem(
|
||||
'tmdb_backdrop_disabled'
|
||||
);
|
||||
if (savedTmdbBackdropDisabled !== null) {
|
||||
setTmdbBackdropDisabled(savedTmdbBackdropDisabled === 'true');
|
||||
}
|
||||
@@ -568,17 +666,23 @@ export const UserMenu: React.FC = () => {
|
||||
setBufferStrategy(savedBufferStrategy);
|
||||
}
|
||||
|
||||
const savedNextEpisodePreCache = localStorage.getItem('nextEpisodePreCache');
|
||||
const savedNextEpisodePreCache = localStorage.getItem(
|
||||
'nextEpisodePreCache'
|
||||
);
|
||||
if (savedNextEpisodePreCache !== null) {
|
||||
setNextEpisodePreCache(savedNextEpisodePreCache === 'true');
|
||||
}
|
||||
|
||||
const savedNextEpisodeDanmakuPreload = localStorage.getItem('nextEpisodeDanmakuPreload');
|
||||
const savedNextEpisodeDanmakuPreload = localStorage.getItem(
|
||||
'nextEpisodeDanmakuPreload'
|
||||
);
|
||||
if (savedNextEpisodeDanmakuPreload !== null) {
|
||||
setNextEpisodeDanmakuPreload(savedNextEpisodeDanmakuPreload === 'true');
|
||||
}
|
||||
|
||||
const savedDisableAutoLoadDanmaku = localStorage.getItem('disableAutoLoadDanmaku');
|
||||
const savedDisableAutoLoadDanmaku = localStorage.getItem(
|
||||
'disableAutoLoadDanmaku'
|
||||
);
|
||||
if (savedDisableAutoLoadDanmaku !== null) {
|
||||
setDisableAutoLoadDanmaku(savedDisableAutoLoadDanmaku === 'true');
|
||||
} else {
|
||||
@@ -592,7 +696,9 @@ export const UserMenu: React.FC = () => {
|
||||
setDanmakuMaxCount(parseInt(savedDanmakuMaxCount, 10));
|
||||
}
|
||||
|
||||
const savedDanmakuHeatmapDisabled = localStorage.getItem('danmaku_heatmap_disabled');
|
||||
const savedDanmakuHeatmapDisabled = localStorage.getItem(
|
||||
'danmaku_heatmap_disabled'
|
||||
);
|
||||
if (savedDanmakuHeatmapDisabled !== null) {
|
||||
setDanmakuHeatmapDisabled(savedDanmakuHeatmapDisabled === 'true');
|
||||
}
|
||||
@@ -602,9 +708,24 @@ export const UserMenu: React.FC = () => {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
setMaxConcurrentDownloads(Number(savedMaxConcurrentDownloads));
|
||||
}
|
||||
|
||||
// 加载单任务线程数设置
|
||||
const savedDownloadThreadsPerTask = localStorage.getItem('downloadThreadsPerTask');
|
||||
const savedDownloadThreadsPerTask = localStorage.getItem(
|
||||
'downloadThreadsPerTask'
|
||||
);
|
||||
if (savedDownloadThreadsPerTask !== null) {
|
||||
setDownloadThreadsPerTask(Number(savedDownloadThreadsPerTask));
|
||||
}
|
||||
|
||||
// 加载下载模式设置
|
||||
const savedDownloadMode = localStorage.getItem('downloadMode');
|
||||
if (savedDownloadMode === 'browser' || savedDownloadMode === 'filesystem') {
|
||||
if (
|
||||
savedDownloadMode === 'browser' ||
|
||||
savedDownloadMode === 'filesystem'
|
||||
) {
|
||||
setDownloadMode(savedDownloadMode);
|
||||
}
|
||||
|
||||
// 加载保存路径设置
|
||||
const savedFilesystemSavePath = localStorage.getItem('filesystemSavePath');
|
||||
const savedFilesystemSavePath =
|
||||
localStorage.getItem('filesystemSavePath');
|
||||
if (savedFilesystemSavePath !== null) {
|
||||
setFilesystemSavePath(savedFilesystemSavePath);
|
||||
}
|
||||
@@ -763,7 +896,8 @@ export const UserMenu: React.FC = () => {
|
||||
setConfirmDialog({
|
||||
isOpen: true,
|
||||
title: '登出所有设备',
|
||||
message: '确定要登出所有设备吗?这将清除所有设备的登录状态(包括当前设备)。',
|
||||
message:
|
||||
'确定要登出所有设备吗?这将清除所有设备的登录状态(包括当前设备)。',
|
||||
onConfirm: async () => {
|
||||
setConfirmDialog({ ...confirmDialog, isOpen: false });
|
||||
try {
|
||||
@@ -789,7 +923,11 @@ export const UserMenu: React.FC = () => {
|
||||
const getDeviceIcon = (deviceInfo: string) => {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -932,11 +1070,22 @@ export const UserMenu: React.FC = () => {
|
||||
console.error('复制失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!tvboxToken || !isSubscribeOpen) return;
|
||||
setSubscribeUrl(buildSubscribeUrl(tvboxToken, subscribeAdFilterEnabled, subscribeYellowFilterEnabled));
|
||||
}, [tvboxToken, subscribeAdFilterEnabled, subscribeYellowFilterEnabled, isSubscribeOpen]);
|
||||
setSubscribeUrl(
|
||||
buildSubscribeUrl(
|
||||
tvboxToken,
|
||||
subscribeAdFilterEnabled,
|
||||
subscribeYellowFilterEnabled
|
||||
)
|
||||
);
|
||||
}, [
|
||||
tvboxToken,
|
||||
subscribeAdFilterEnabled,
|
||||
subscribeYellowFilterEnabled,
|
||||
isSubscribeOpen,
|
||||
]);
|
||||
|
||||
const handleSubmitChangePassword = async () => {
|
||||
setPasswordError('');
|
||||
@@ -1043,11 +1192,16 @@ export const UserMenu: React.FC = () => {
|
||||
|
||||
const handleDownloadModeChange = (mode: 'browser' | 'filesystem') => {
|
||||
// 如果选择 filesystem 模式,先检测浏览器是否支持
|
||||
if (mode === 'filesystem' && typeof window !== 'undefined' && !('showDirectoryPicker' in window)) {
|
||||
if (
|
||||
mode === 'filesystem' &&
|
||||
typeof window !== 'undefined' &&
|
||||
!('showDirectoryPicker' in window)
|
||||
) {
|
||||
setConfirmDialog({
|
||||
isOpen: true,
|
||||
title: '浏览器不支持',
|
||||
message: '您的浏览器不支持 File System Access API,请使用 Chrome 86+ 或 Edge 86+',
|
||||
message:
|
||||
'您的浏览器不支持 File System Access API,请使用 Chrome 86+ 或 Edge 86+',
|
||||
onConfirm: () => {
|
||||
setConfirmDialog({ ...confirmDialog, isOpen: false });
|
||||
},
|
||||
@@ -1085,18 +1239,28 @@ export const UserMenu: React.FC = () => {
|
||||
|
||||
// 创建 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('createdAt', 'createdAt', { unique: false });
|
||||
activeStore.createIndex('createdAt', 'createdAt', {
|
||||
unique: false,
|
||||
});
|
||||
}
|
||||
|
||||
// 创建 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('videoId', 'videoId', { unique: false });
|
||||
completedStore.createIndex('completedAt', 'completedAt', { unique: false });
|
||||
completedStore.createIndex('sourceVideoId', ['source', 'videoId'], { unique: false });
|
||||
completedStore.createIndex('completedAt', 'completedAt', {
|
||||
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) => {
|
||||
setHomeContinueWatchingEnabled(value);
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -1290,7 +1462,7 @@ export const UserMenu: React.FC = () => {
|
||||
|
||||
// 首页模块配置处理函数
|
||||
const handleHomeModuleToggle = (id: string, enabled: boolean) => {
|
||||
const updatedModules = homeModules.map(module =>
|
||||
const updatedModules = homeModules.map((module) =>
|
||||
module.id === id ? { ...module, enabled } : module
|
||||
);
|
||||
setHomeModules(updatedModules);
|
||||
@@ -1356,11 +1528,13 @@ export const UserMenu: React.FC = () => {
|
||||
|
||||
const handleResetSettings = () => {
|
||||
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 =
|
||||
(window as any).RUNTIME_CONFIG?.DOUBAN_PROXY || '';
|
||||
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 =
|
||||
(window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY || '';
|
||||
const defaultFluidSearch =
|
||||
@@ -1390,6 +1564,7 @@ export const UserMenu: React.FC = () => {
|
||||
false;
|
||||
setDisableAutoLoadDanmaku(!defaultDanmakuAutoLoad);
|
||||
setHomeBannerEnabled(true);
|
||||
setHomeBannerHeightScale('1');
|
||||
setHomeContinueWatchingEnabled(true);
|
||||
setHomeModules(defaultHomeModules);
|
||||
setSearchTraditionalToSimplified(false);
|
||||
@@ -1421,6 +1596,7 @@ export const UserMenu: React.FC = () => {
|
||||
localStorage.setItem('danmakuMaxCount', '0');
|
||||
localStorage.setItem('danmaku_heatmap_disabled', 'false');
|
||||
localStorage.setItem('homeBannerEnabled', 'true');
|
||||
localStorage.setItem('homeBannerHeightScale', '1');
|
||||
localStorage.setItem('homeContinueWatchingEnabled', 'true');
|
||||
localStorage.setItem('homeModules', JSON.stringify(defaultHomeModules));
|
||||
localStorage.setItem('searchTraditionalToSimplified', 'false');
|
||||
@@ -1495,8 +1671,8 @@ export const UserMenu: React.FC = () => {
|
||||
currentRole === 'owner'
|
||||
? 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'
|
||||
: currentRole === 'admin'
|
||||
? '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-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';
|
||||
|
||||
const handleOpenProfileCenter = () => {
|
||||
setIsOpen(false);
|
||||
@@ -1542,7 +1718,9 @@ export const UserMenu: React.FC = () => {
|
||||
<div className='text-[10px] text-gray-400 dark:text-gray-500'>
|
||||
<div>数据存储</div>
|
||||
<div className='mt-0.5'>
|
||||
{displayStorageType === 'localstorage' ? '本地' : displayStorageType}
|
||||
{displayStorageType === 'localstorage'
|
||||
? '本地'
|
||||
: displayStorageType}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1666,12 +1844,13 @@ export const UserMenu: React.FC = () => {
|
||||
updateStatus &&
|
||||
updateStatus !== UpdateStatus.FETCH_FAILED && (
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full -translate-y-2 ${updateStatus === UpdateStatus.HAS_UPDATE
|
||||
? 'bg-yellow-500'
|
||||
: updateStatus === UpdateStatus.NO_UPDATE
|
||||
className={`w-2 h-2 rounded-full -translate-y-2 ${
|
||||
updateStatus === UpdateStatus.HAS_UPDATE
|
||||
? 'bg-yellow-500'
|
||||
: updateStatus === UpdateStatus.NO_UPDATE
|
||||
? 'bg-green-400'
|
||||
: ''
|
||||
}`}
|
||||
}`}
|
||||
></div>
|
||||
)}
|
||||
</div>
|
||||
@@ -1702,9 +1881,7 @@ export const UserMenu: React.FC = () => {
|
||||
/>
|
||||
|
||||
{/* 设置面板 */}
|
||||
<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'
|
||||
>
|
||||
<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'>
|
||||
{/* 内容容器 - 独立的滚动区域 */}
|
||||
<div
|
||||
className='flex-1 px-4 py-6 md:p-6 overflow-y-auto'
|
||||
@@ -1773,7 +1950,9 @@ export const UserMenu: React.FC = () => {
|
||||
{/* 自定义下拉选择框 */}
|
||||
<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'
|
||||
>
|
||||
{
|
||||
@@ -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'>
|
||||
<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>
|
||||
|
||||
@@ -1802,10 +1982,11 @@ export const UserMenu: React.FC = () => {
|
||||
handleDoubanDataSourceChange(option.value);
|
||||
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
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
||||
: 'text-gray-900 dark:text-gray-100'
|
||||
}`}
|
||||
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
|
||||
? '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>
|
||||
{doubanDataSource === option.value && (
|
||||
@@ -1823,7 +2004,10 @@ export const UserMenu: React.FC = () => {
|
||||
<button
|
||||
type='button'
|
||||
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'
|
||||
>
|
||||
@@ -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'
|
||||
placeholder='例如: https://proxy.example.com/fetch?url='
|
||||
value={doubanProxyUrl}
|
||||
onChange={(e) => handleDoubanProxyUrlChange(e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleDoubanProxyUrlChange(e.target.value)
|
||||
}
|
||||
/>
|
||||
{!doubanProxyUrl.trim() && (
|
||||
<p className='text-xs text-amber-600 dark:text-amber-400 mt-1'>
|
||||
@@ -1878,7 +2064,9 @@ export const UserMenu: React.FC = () => {
|
||||
<button
|
||||
type='button'
|
||||
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'
|
||||
>
|
||||
@@ -1890,8 +2078,9 @@ export const UserMenu: React.FC = () => {
|
||||
</button>
|
||||
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
|
||||
<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>
|
||||
{isDoubanBackupDropdownOpen && (
|
||||
@@ -1901,13 +2090,16 @@ export const UserMenu: React.FC = () => {
|
||||
key={option.value}
|
||||
type='button'
|
||||
onClick={() => {
|
||||
handleDoubanDataSourceBackupChange(option.value);
|
||||
handleDoubanDataSourceBackupChange(
|
||||
option.value
|
||||
);
|
||||
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
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
||||
: 'text-gray-900 dark:text-gray-100'
|
||||
}`}
|
||||
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
|
||||
? '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>
|
||||
{doubanDataSourceBackup === option.value && (
|
||||
@@ -1960,7 +2152,10 @@ export const UserMenu: React.FC = () => {
|
||||
选择获取豆瓣图片的方式
|
||||
</p>
|
||||
</div>
|
||||
<div className='relative' data-dropdown='douban-image-proxy'>
|
||||
<div
|
||||
className='relative'
|
||||
data-dropdown='douban-image-proxy'
|
||||
>
|
||||
{/* 自定义下拉选择框 */}
|
||||
<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'>
|
||||
<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>
|
||||
|
||||
@@ -1997,10 +2193,11 @@ export const UserMenu: React.FC = () => {
|
||||
handleDoubanImageProxyTypeChange(option.value);
|
||||
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
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
||||
: 'text-gray-900 dark:text-gray-100'
|
||||
}`}
|
||||
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
|
||||
? '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>
|
||||
{doubanImageProxyType === option.value && (
|
||||
@@ -2086,14 +2283,18 @@ export const UserMenu: React.FC = () => {
|
||||
>
|
||||
{
|
||||
doubanImageProxyTypeOptions.find(
|
||||
(option) => option.value === doubanImageProxyTypeBackup
|
||||
(option) =>
|
||||
option.value === doubanImageProxyTypeBackup
|
||||
)?.label
|
||||
}
|
||||
</button>
|
||||
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
|
||||
<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>
|
||||
{isDoubanImageProxyBackupDropdownOpen && (
|
||||
@@ -2103,13 +2304,16 @@ export const UserMenu: React.FC = () => {
|
||||
key={option.value}
|
||||
type='button'
|
||||
onClick={() => {
|
||||
handleDoubanImageProxyTypeBackupChange(option.value);
|
||||
handleDoubanImageProxyTypeBackupChange(
|
||||
option.value
|
||||
);
|
||||
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
|
||||
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
||||
: 'text-gray-900 dark:text-gray-100'
|
||||
}`}
|
||||
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
|
||||
? '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>
|
||||
{doubanImageProxyTypeBackup === option.value && (
|
||||
@@ -2211,7 +2415,9 @@ export const UserMenu: React.FC = () => {
|
||||
type='checkbox'
|
||||
className='sr-only peer'
|
||||
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='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'
|
||||
className='sr-only peer'
|
||||
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='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'
|
||||
step='1000'
|
||||
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'
|
||||
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 className='flex justify-between text-xs text-gray-500 dark:text-gray-400'>
|
||||
<button
|
||||
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秒
|
||||
</button>
|
||||
<button
|
||||
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秒
|
||||
</button>
|
||||
<button
|
||||
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秒
|
||||
</button>
|
||||
<button
|
||||
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秒
|
||||
</button>
|
||||
@@ -2348,7 +2578,9 @@ export const UserMenu: React.FC = () => {
|
||||
type='checkbox'
|
||||
className='sr-only peer'
|
||||
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='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'
|
||||
className='sr-only peer'
|
||||
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='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'
|
||||
className='sr-only peer'
|
||||
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='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'
|
||||
className='sr-only peer'
|
||||
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='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'
|
||||
className='sr-only peer'
|
||||
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='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'
|
||||
step='1'
|
||||
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'
|
||||
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 className='flex justify-between text-xs text-gray-500 dark:text-gray-400'>
|
||||
<button
|
||||
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个
|
||||
</button>
|
||||
<button
|
||||
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个
|
||||
</button>
|
||||
@@ -2548,23 +2806,39 @@ export const UserMenu: React.FC = () => {
|
||||
max='32'
|
||||
step='1'
|
||||
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'
|
||||
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 className='flex justify-between text-xs text-gray-500 dark:text-gray-400'>
|
||||
<button
|
||||
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个
|
||||
</button>
|
||||
<button
|
||||
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个
|
||||
</button>
|
||||
@@ -2598,7 +2872,9 @@ export const UserMenu: React.FC = () => {
|
||||
name='downloadMode'
|
||||
value='filesystem'
|
||||
checked={downloadMode === 'filesystem'}
|
||||
onChange={() => handleDownloadModeChange('filesystem')}
|
||||
onChange={() =>
|
||||
handleDownloadModeChange('filesystem')
|
||||
}
|
||||
className='w-4 h-4 text-green-500'
|
||||
/>
|
||||
<span className='text-sm text-gray-700 dark:text-gray-300'>
|
||||
@@ -2696,27 +2972,58 @@ export const UserMenu: React.FC = () => {
|
||||
value={getSliderValueFromStrategy(bufferStrategy)}
|
||||
onChange={(e) => {
|
||||
const sliderValue = parseInt(e.target.value);
|
||||
const strategy = getBufferStrategyFromSlider(sliderValue);
|
||||
const strategy =
|
||||
getBufferStrategyFromSlider(sliderValue);
|
||||
handleBufferStrategyChange(strategy);
|
||||
}}
|
||||
className='w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-green-500'
|
||||
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'>
|
||||
<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 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 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 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>
|
||||
</div>
|
||||
@@ -2748,7 +3055,9 @@ export const UserMenu: React.FC = () => {
|
||||
type='checkbox'
|
||||
className='sr-only peer'
|
||||
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='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'
|
||||
className='sr-only peer'
|
||||
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='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'
|
||||
className='sr-only peer'
|
||||
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='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'
|
||||
className='sr-only peer'
|
||||
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='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 className='text-xs font-medium text-gray-700 dark:text-gray-300'>
|
||||
{danmakuMaxCount === 0 ? '无上限' : `${danmakuMaxCount} 条`}
|
||||
{danmakuMaxCount === 0
|
||||
? '无上限'
|
||||
: `${danmakuMaxCount} 条`}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
@@ -2868,38 +3187,63 @@ export const UserMenu: React.FC = () => {
|
||||
max='10000'
|
||||
step='100'
|
||||
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'
|
||||
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 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
|
||||
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%)' }}
|
||||
>
|
||||
无上限
|
||||
</button>
|
||||
<button
|
||||
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%)' }}
|
||||
>
|
||||
3000
|
||||
</button>
|
||||
<button
|
||||
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%)' }}
|
||||
>
|
||||
5000
|
||||
</button>
|
||||
<button
|
||||
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%)' }}
|
||||
>
|
||||
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'>
|
||||
<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
|
||||
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>
|
||||
<span>清除弹幕缓存</span>
|
||||
</>
|
||||
@@ -2945,11 +3299,13 @@ export const UserMenu: React.FC = () => {
|
||||
|
||||
{/* 成功/失败提示 */}
|
||||
{clearCacheMessage && (
|
||||
<div className={`text-sm p-3 rounded-lg border ${
|
||||
clearCacheMessage.includes('成功')
|
||||
? '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'
|
||||
}`}>
|
||||
<div
|
||||
className={`text-sm p-3 rounded-lg border ${
|
||||
clearCacheMessage.includes('成功')
|
||||
? '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}
|
||||
</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>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mb-3'>
|
||||
配置首页模块的显示顺序和可见性
|
||||
配置首页轮播图显示效果,以及首页模块布局
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 首页顶部组件显示 */}
|
||||
{/* 轮播图配置 */}
|
||||
<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'>
|
||||
<button
|
||||
onClick={() => handleHomeBannerToggle(!homeBannerEnabled)}
|
||||
onClick={() =>
|
||||
handleHomeBannerToggle(!homeBannerEnabled)
|
||||
}
|
||||
className='flex-shrink-0'
|
||||
title={homeBannerEnabled ? '点击隐藏' : '点击显示'}
|
||||
>
|
||||
@@ -2999,11 +3411,13 @@ export const UserMenu: React.FC = () => {
|
||||
)}
|
||||
</button>
|
||||
<div className='flex-1'>
|
||||
<span className={`text-sm font-medium ${
|
||||
homeBannerEnabled
|
||||
? 'text-gray-900 dark:text-gray-100'
|
||||
: 'text-gray-400 dark:text-gray-500'
|
||||
}`}>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
homeBannerEnabled
|
||||
? 'text-gray-900 dark:text-gray-100'
|
||||
: 'text-gray-400 dark:text-gray-500'
|
||||
}`}
|
||||
>
|
||||
首页轮播图
|
||||
</span>
|
||||
</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'>
|
||||
<button
|
||||
onClick={() => handleHomeContinueWatchingToggle(!homeContinueWatchingEnabled)}
|
||||
onClick={() =>
|
||||
handleHomeContinueWatchingToggle(
|
||||
!homeContinueWatchingEnabled
|
||||
)
|
||||
}
|
||||
className='flex-shrink-0'
|
||||
title={homeContinueWatchingEnabled ? '点击隐藏' : '点击显示'}
|
||||
title={
|
||||
homeContinueWatchingEnabled ? '点击隐藏' : '点击显示'
|
||||
}
|
||||
>
|
||||
{homeContinueWatchingEnabled ? (
|
||||
<Eye className='w-5 h-5 text-green-600 dark:text-green-400' />
|
||||
@@ -3022,69 +3442,75 @@ export const UserMenu: React.FC = () => {
|
||||
)}
|
||||
</button>
|
||||
<div className='flex-1'>
|
||||
<span className={`text-sm font-medium ${
|
||||
homeContinueWatchingEnabled
|
||||
? 'text-gray-900 dark:text-gray-100'
|
||||
: 'text-gray-400 dark:text-gray-500'
|
||||
}`}>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
homeContinueWatchingEnabled
|
||||
? 'text-gray-900 dark:text-gray-100'
|
||||
: 'text-gray-400 dark:text-gray-500'
|
||||
}`}
|
||||
>
|
||||
继续观看
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模块列表 */}
|
||||
<div className='space-y-2'>
|
||||
{homeModules.map((module, index) => (
|
||||
<div
|
||||
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'
|
||||
>
|
||||
{/* 左侧:显示/隐藏开关 */}
|
||||
<button
|
||||
onClick={() => handleHomeModuleToggle(module.id, !module.enabled)}
|
||||
className='flex-shrink-0'
|
||||
title={module.enabled ? '点击隐藏' : '点击显示'}
|
||||
{/* 模块列表 */}
|
||||
<div className='space-y-2'>
|
||||
{homeModules.map((module, index) => (
|
||||
<div
|
||||
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'
|
||||
>
|
||||
{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
|
||||
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='上移'
|
||||
onClick={() =>
|
||||
handleHomeModuleToggle(module.id, !module.enabled)
|
||||
}
|
||||
className='flex-shrink-0'
|
||||
title={module.enabled ? '点击隐藏' : '点击显示'}
|
||||
>
|
||||
<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' />
|
||||
{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
|
||||
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>
|
||||
|
||||
{/* 恢复默认按钮 */}
|
||||
@@ -3092,12 +3518,22 @@ export const UserMenu: React.FC = () => {
|
||||
onClick={() => {
|
||||
setHomeModules(defaultHomeModules);
|
||||
setHomeBannerEnabled(true);
|
||||
setHomeBannerHeightScale('1');
|
||||
setHomeContinueWatchingEnabled(true);
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('homeModules', JSON.stringify(defaultHomeModules));
|
||||
localStorage.setItem(
|
||||
'homeModules',
|
||||
JSON.stringify(defaultHomeModules)
|
||||
);
|
||||
localStorage.setItem('homeBannerEnabled', 'true');
|
||||
localStorage.setItem('homeContinueWatchingEnabled', 'true');
|
||||
window.dispatchEvent(new CustomEvent('homeModulesUpdated'));
|
||||
localStorage.setItem('homeBannerHeightScale', '1');
|
||||
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'
|
||||
@@ -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'>
|
||||
<p>💡 提示:点击眼睛图标可显示/隐藏模块,使用箭头按钮调整模块顺序</p>
|
||||
<p>
|
||||
💡
|
||||
提示:点击眼睛图标可显示/隐藏模块,使用箭头按钮调整模块顺序
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -3144,9 +3583,7 @@ export const UserMenu: React.FC = () => {
|
||||
/>
|
||||
|
||||
{/* 订阅面板 */}
|
||||
<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'
|
||||
>
|
||||
<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'>
|
||||
<div
|
||||
className='h-full p-6'
|
||||
data-panel-content
|
||||
@@ -3220,14 +3657,28 @@ export const UserMenu: React.FC = () => {
|
||||
开启后通过代理处理播放链接,兼容性可能略低
|
||||
</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 className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition-transform ${subscribeAdFilterEnabled ? 'translate-x-5' : 'translate-x-0.5'}`} />
|
||||
<div
|
||||
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>
|
||||
</button>
|
||||
|
||||
<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'
|
||||
>
|
||||
<div>
|
||||
@@ -3238,8 +3689,20 @@ export const UserMenu: React.FC = () => {
|
||||
开启后同样走代理,并在代理搜索时过滤黄色内容
|
||||
</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 className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition-transform ${subscribeYellowFilterEnabled ? 'translate-x-5' : 'translate-x-0.5'}`} />
|
||||
<div
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
@@ -3263,7 +3726,8 @@ export const UserMenu: React.FC = () => {
|
||||
{copySuccess ? '已复制' : '复制'}
|
||||
</button>
|
||||
</div>
|
||||
{(subscribeAdFilterEnabled || subscribeYellowFilterEnabled) && (
|
||||
{(subscribeAdFilterEnabled ||
|
||||
subscribeYellowFilterEnabled) && (
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||
💡 代理模式已开启,某些源可能因为区域或兼容问题无法播放
|
||||
</p>
|
||||
@@ -3283,7 +3747,10 @@ export const UserMenu: React.FC = () => {
|
||||
⚠️ 重置后旧链接将失效
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
@@ -3321,9 +3788,7 @@ export const UserMenu: React.FC = () => {
|
||||
/>
|
||||
|
||||
{/* 修改密码面板 */}
|
||||
<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'
|
||||
>
|
||||
<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'>
|
||||
{/* 内容容器 - 独立的滚动区域 */}
|
||||
<div
|
||||
className='h-full p-6'
|
||||
@@ -3438,9 +3903,7 @@ export const UserMenu: React.FC = () => {
|
||||
/>
|
||||
|
||||
{/* 举报信息面板 */}
|
||||
<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'
|
||||
>
|
||||
<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'>
|
||||
<div
|
||||
className='h-full max-h-[70vh] flex flex-col'
|
||||
data-panel-content
|
||||
@@ -3469,13 +3932,20 @@ export const UserMenu: React.FC = () => {
|
||||
<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'>
|
||||
<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 className='text-gray-800 dark:text-gray-200 leading-relaxed mt-3'>
|
||||
超分,观影室,豆瓣反爬,精确搜索等等等等,直接抄袭,最不要脸的就是,刚更新一版,几小时后直接抄走。
|
||||
</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3513,9 +3983,7 @@ export const UserMenu: React.FC = () => {
|
||||
/>
|
||||
|
||||
{/* 生态应用面板 */}
|
||||
<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'
|
||||
>
|
||||
<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'>
|
||||
<div
|
||||
className='h-full max-h-[85vh] flex flex-col'
|
||||
data-panel-content
|
||||
@@ -3675,7 +4143,8 @@ export const UserMenu: React.FC = () => {
|
||||
私人影库转码器
|
||||
</h4>
|
||||
<p className='text-sm text-gray-600 dark:text-gray-400 mb-3'>
|
||||
为私人影库中的 MKV 视频提供转码播放能力,可解析内封字幕并解决部分视频无音频问题,但通常需要较高的本机性能配置。
|
||||
为私人影库中的 MKV
|
||||
视频提供转码播放能力,可解析内封字幕并解决部分视频无音频问题,但通常需要较高的本机性能配置。
|
||||
</p>
|
||||
<a
|
||||
href='https://github.com/mtvpls/moontvplus-transcoder/tags'
|
||||
@@ -3843,14 +4312,10 @@ export const UserMenu: React.FC = () => {
|
||||
/>
|
||||
|
||||
{/* 使用 Portal 将生态应用面板渲染到 document.body */}
|
||||
{isEcoAppsOpen &&
|
||||
mounted &&
|
||||
createPortal(ecoAppsPanel, document.body)}
|
||||
{isEcoAppsOpen && mounted && createPortal(ecoAppsPanel, document.body)}
|
||||
|
||||
{/* 使用 Portal 将举报信息面板渲染到 document.body */}
|
||||
{isReportOpen &&
|
||||
mounted &&
|
||||
createPortal(reportPanel, document.body)}
|
||||
{isReportOpen && mounted && createPortal(reportPanel, document.body)}
|
||||
|
||||
{/* 确认对话框 */}
|
||||
{confirmDialog.isOpen &&
|
||||
@@ -3875,7 +4340,9 @@ export const UserMenu: React.FC = () => {
|
||||
{/* 按钮 */}
|
||||
<div className='p-6 pt-0 flex gap-3 justify-end'>
|
||||
<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'
|
||||
>
|
||||
取消
|
||||
|
||||
Reference in New Issue
Block a user