/* eslint-disable no-console,@typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ 'use client'; import { Bell, Check, ChevronDown, ChevronUp, Copy, Download, ExternalLink, Eye, EyeOff, Gauge, Globe, Home, KeyRound, LogOut, Mail, MessageSquare, Monitor, MoveDown, MoveUp, Package, Rss, Settings, Shield, Sliders, Smartphone, Star, Tablet, User, X, } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; import { clearAllDanmakuCache } from '@/lib/danmaku/api'; import { CURRENT_VERSION } from '@/lib/version'; import { UpdateStatus } from '@/lib/version_check'; import { FavoritesPanel } from './FavoritesPanel'; import { NotificationPanel } from './NotificationPanel'; import { OfflineDownloadPanel } from './OfflineDownloadPanel'; import { useVersionCheck } from './VersionCheckProvider'; import { VersionPanel } from './VersionPanel'; import { DownloadManagementPanel } from './DownloadManagementPanel'; interface AuthInfo { username?: string; role?: 'owner' | 'admin' | 'user'; } export const UserMenu: React.FC = () => { const router = useRouter(); const { updateStatus, isChecking } = useVersionCheck(); const [isOpen, setIsOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false); const [isSubscribeOpen, setIsSubscribeOpen] = useState(false); const [isVersionPanelOpen, setIsVersionPanelOpen] = 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 [authInfo, setAuthInfo] = useState(null); const [storageType, setStorageType] = useState('localstorage'); const [mounted, setMounted] = useState(false); const [unreadCount, setUnreadCount] = useState(0); // 订阅相关状态 const [subscribeEnabled, setSubscribeEnabled] = useState(false); const [subscribeUrl, setSubscribeUrl] = useState(''); const [subscribeUrlWithAdFilter, setSubscribeUrlWithAdFilter] = useState(''); const [copySuccess, setCopySuccess] = useState(false); const [copySuccessAdFilter, setCopySuccessAdFilter] = useState(false); const [tvboxToken, setTvboxToken] = useState(''); const [isResettingToken, setIsResettingToken] = useState(false); const [isLoadingSubscribeUrl, setIsLoadingSubscribeUrl] = useState(false); // Body 滚动锁定 - 使用 overflow 方式避免布局问题 useEffect(() => { if (isSettingsOpen || isChangePasswordOpen || isSubscribeOpen || isOfflineDownloadPanelOpen || isEmailSettingsOpen || isDeviceManagementOpen || isEcoAppsOpen || isReportOpen || isDownloadManagementOpen) { const body = document.body; const html = document.documentElement; // 保存原始样式 const originalBodyOverflow = body.style.overflow; const originalHtmlOverflow = html.style.overflow; // 只设置 overflow 来阻止滚动 body.style.overflow = 'hidden'; html.style.overflow = 'hidden'; return () => { // 恢复所有原始样式 body.style.overflow = originalBodyOverflow; html.style.overflow = originalHtmlOverflow; }; } }, [isSettingsOpen, isChangePasswordOpen, isSubscribeOpen, isOfflineDownloadPanelOpen, isEmailSettingsOpen, isDeviceManagementOpen, isEcoAppsOpen]); // 设置相关状态 const [defaultAggregateSearch, setDefaultAggregateSearch] = useState(true); const [doubanProxyUrl, setDoubanProxyUrl] = useState(''); const [enableOptimization, setEnableOptimization] = useState(true); const [speedTestTimeout, setSpeedTestTimeout] = useState(4000); // 测速超时时间(毫秒) const [fluidSearch, setFluidSearch] = useState(true); const [liveDirectConnect, setLiveDirectConnect] = useState(false); const [tmdbBackdropDisabled, setTmdbBackdropDisabled] = useState(false); const [enableTrailers, setEnableTrailers] = useState(false); const [doubanDataSource, setDoubanDataSource] = useState('cmliussss-cdn-tencent'); const [doubanImageProxyType, setDoubanImageProxyType] = useState('cmliussss-cdn-tencent'); const [doubanImageProxyUrl, setDoubanImageProxyUrl] = useState(''); const [isDoubanDropdownOpen, setIsDoubanDropdownOpen] = useState(false); const [isDoubanImageProxyDropdownOpen, setIsDoubanImageProxyDropdownOpen] = useState(false); const [bufferStrategy, setBufferStrategy] = useState('medium'); const [nextEpisodePreCache, setNextEpisodePreCache] = 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 [exactSearch, setExactSearch] = useState(true); const [maxConcurrentDownloads, setMaxConcurrentDownloads] = useState(6); const [downloadThreadsPerTask, setDownloadThreadsPerTask] = useState(6); const [downloadMode, setDownloadMode] = useState<'browser' | 'filesystem'>('browser'); const [filesystemSavePath, setFilesystemSavePath] = useState(''); // 邮件通知设置 const [userEmail, setUserEmail] = useState(''); const [emailNotifications, setEmailNotifications] = useState(false); const [emailSettingsLoading, setEmailSettingsLoading] = useState(false); const [emailSettingsSaving, setEmailSettingsSaving] = useState(false); // 设备管理状态 const [devices, setDevices] = useState([]); const [devicesLoading, setDevicesLoading] = useState(false); const [revoking, setRevoking] = useState(null); // 确认对话框状态 const [confirmDialog, setConfirmDialog] = useState<{ isOpen: boolean; title: string; message: string; onConfirm: () => void; }>({ isOpen: false, title: '', message: '', onConfirm: () => {}, }); // 折叠面板状态 const [isDoubanSectionOpen, setIsDoubanSectionOpen] = useState(true); // TMDB 图片设置 const [tmdbImageBaseUrl, setTmdbImageBaseUrl] = useState('https://image.tmdb.org'); const [isUsageSectionOpen, setIsUsageSectionOpen] = useState(false); const [isDownloadSectionOpen, setIsDownloadSectionOpen] = useState(false); const [isBufferSectionOpen, setIsBufferSectionOpen] = useState(false); const [isDanmakuSectionOpen, setIsDanmakuSectionOpen] = useState(false); const [isHomepageSectionOpen, setIsHomepageSectionOpen] = useState(false); // 首页模块配置 interface HomeModule { id: string; name: string; enabled: boolean; order: number; } const defaultHomeModules: HomeModule[] = [ { id: 'hotMovies', name: '热门电影', enabled: true, order: 0 }, { id: 'hotDuanju', name: '热播短剧', enabled: true, order: 1 }, { id: 'bangumiCalendar', name: '新番放送', enabled: true, order: 2 }, { id: 'hotTvShows', name: '热门剧集', enabled: true, order: 3 }, { id: 'hotVarietyShows', name: '热门综艺', enabled: true, order: 4 }, { id: 'upcomingContent', name: '即将上映', enabled: true, order: 5 }, ]; const [homeModules, setHomeModules] = useState(defaultHomeModules); const [homeBannerEnabled, setHomeBannerEnabled] = useState(true); const [homeContinueWatchingEnabled, setHomeContinueWatchingEnabled] = useState(true); // 豆瓣数据源选项 const doubanDataSourceOptions = [ { value: 'direct', label: '直连(服务器直接请求豆瓣)' }, { value: 'cors-proxy-zwei', label: 'Cors Proxy By Zwei' }, { value: 'cmliussss-cdn-tencent', label: '豆瓣 CDN By CMLiussss(腾讯云)', }, { value: 'cmliussss-cdn-ali', label: '豆瓣 CDN By CMLiussss(阿里云)' }, { value: 'custom', label: '自定义代理' }, ]; // 豆瓣图片代理选项 const doubanImageProxyTypeOptions = [ { value: 'server', label: '服务器代理(由服务器代理请求豆瓣)' }, { value: 'cmliussss-cdn-tencent', label: '豆瓣 CDN By CMLiussss(腾讯云)', }, { value: 'cmliussss-cdn-ali', label: '豆瓣 CDN By CMLiussss(阿里云)' }, { value: 'baidu', label: '百度图片代理' }, { value: 'custom', label: '自定义代理' }, { value: 'direct', label: '直连(浏览器直接请求豆瓣,可能需要浏览器插件才能正常显示)' }, { value: 'img3', label: '豆瓣官方精品 CDN(阿里云,可能需要浏览器插件才能正常显示)' }, ]; // 缓冲策略选项 const bufferStrategyOptions = [ { value: 'low', label: '低缓冲(省流量)' }, { value: 'medium', label: '中缓冲(推荐)' }, { value: 'high', label: '高缓冲(流畅播放)' }, { value: 'ultra', label: '超高缓冲(极速体验)' }, ]; // 修改密码相关状态 const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [passwordLoading, setPasswordLoading] = useState(false); const [passwordError, setPasswordError] = useState(''); // 清除弹幕缓存相关状态 const [isClearingCache, setIsClearingCache] = useState(false); const [clearCacheMessage, setClearCacheMessage] = useState(null); // 确保组件已挂载 useEffect(() => { setMounted(true); }, []); // 加载未读通知数量 const loadUnreadCount = async () => { try { const response = await fetch('/api/notifications'); if (response.ok) { const data = await response.json(); const count = data.unreadCount || 0; setUnreadCount(count); // 同步到全局,让其他 UserMenu 实例也能获取 if (typeof window !== 'undefined') { (window as any).__unreadNotificationCount = count; } } } catch (error) { console.error('加载未读通知数量失败:', error); } }; // 首次加载时检查未读通知数量(使用全局标记避免多个实例重复请求) useEffect(() => { if (typeof window === 'undefined') return; // 检查是否已经有其他实例在加载 const globalWindow = window as any; if (globalWindow.__loadingNotifications) { // 如果正在加载,等待加载完成后获取结果 const checkInterval = setInterval(() => { if (!globalWindow.__loadingNotifications && globalWindow.__unreadNotificationCount !== undefined) { setUnreadCount(globalWindow.__unreadNotificationCount); clearInterval(checkInterval); } }, 100); return () => clearInterval(checkInterval); } // 检查是否已经加载过 if (globalWindow.__unreadNotificationCount !== undefined) { setUnreadCount(globalWindow.__unreadNotificationCount); return; } // 标记正在加载 globalWindow.__loadingNotifications = true; loadUnreadCount().finally(() => { globalWindow.__loadingNotifications = false; }); }, []); // 监听通知更新事件 useEffect(() => { const handleNotificationsUpdated = () => { // 清除缓存,强制重新加载 if (typeof window !== 'undefined') { delete (window as any).__unreadNotificationCount; } loadUnreadCount(); }; window.addEventListener('notificationsUpdated', handleNotificationsUpdated); return () => { window.removeEventListener('notificationsUpdated', handleNotificationsUpdated); }; }, []); // 从运行时配置读取订阅是否启用 useEffect(() => { if (typeof window !== 'undefined') { const enabled = (window as any).RUNTIME_CONFIG?.ENABLE_TVBOX_SUBSCRIBE || false; setSubscribeEnabled(enabled); } }, []); // 懒加载订阅 URL - 只在打开订阅面板时请求 const fetchSubscribeUrl = async () => { setIsLoadingSubscribeUrl(true); try { // 获取用户的 TVBox token const response = await fetch('/api/user/tvbox-token'); if (response.ok) { const data = await response.json(); const token = data.token; setTvboxToken(token); // 前端拼接订阅链接 const currentOrigin = window.location.origin; const standardUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}`; const adFilterUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}&adFilter=true`; setSubscribeUrl(standardUrl); setSubscribeUrlWithAdFilter(adFilterUrl); } } catch (error) { console.error('获取订阅URL失败:', error); } finally { setIsLoadingSubscribeUrl(false); } }; // 重置 TVBox token const handleResetToken = async () => { setConfirmDialog({ isOpen: true, title: '重置订阅Token', message: '确定要重置订阅token吗?重置后旧的订阅链接将失效。', onConfirm: async () => { setConfirmDialog({ ...confirmDialog, isOpen: false }); setIsResettingToken(true); try { const response = await fetch('/api/user/tvbox-token/reset', { method: 'POST', }); const messageEl = document.getElementById('tvbox-token-message'); if (response.ok) { const data = await response.json(); const token = data.token; setTvboxToken(token); // 更新订阅链接 const currentOrigin = window.location.origin; const standardUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}`; const adFilterUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}&adFilter=true`; setSubscribeUrl(standardUrl); setSubscribeUrlWithAdFilter(adFilterUrl); if (messageEl) { messageEl.textContent = '订阅token已重置!'; messageEl.className = 'text-xs text-center text-green-600 dark:text-green-400 mt-2'; messageEl.classList.remove('hidden'); setTimeout(() => { messageEl.classList.add('hidden'); }, 3000); } } else { 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.classList.remove('hidden'); } } } catch (error) { console.error('重置token失败:', error); 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.classList.remove('hidden'); } } finally { setIsResettingToken(false); } }, }); }; // 获取认证信息和存储类型 useEffect(() => { if (typeof window !== 'undefined') { const auth = getAuthInfoFromBrowserCookie(); setAuthInfo(auth); const type = (window as any).RUNTIME_CONFIG?.STORAGE_TYPE || 'localstorage'; setStorageType(type); } }, []); // 从 localStorage 读取设置 useEffect(() => { if (typeof window !== 'undefined') { const savedAggregateSearch = localStorage.getItem( 'defaultAggregateSearch' ); if (savedAggregateSearch !== null) { setDefaultAggregateSearch(JSON.parse(savedAggregateSearch)); } const savedDoubanDataSource = localStorage.getItem('doubanDataSource'); const defaultDoubanProxyType = (window as any).RUNTIME_CONFIG?.DOUBAN_PROXY_TYPE || 'cmliussss-cdn-tencent'; if (savedDoubanDataSource !== null) { setDoubanDataSource(savedDoubanDataSource); } else if (defaultDoubanProxyType) { setDoubanDataSource(defaultDoubanProxyType); } const savedDoubanProxyUrl = localStorage.getItem('doubanProxyUrl'); const defaultDoubanProxy = (window as any).RUNTIME_CONFIG?.DOUBAN_PROXY || ''; if (savedDoubanProxyUrl !== null) { setDoubanProxyUrl(savedDoubanProxyUrl); } else if (defaultDoubanProxy) { setDoubanProxyUrl(defaultDoubanProxy); } const savedDoubanImageProxyType = localStorage.getItem( 'doubanImageProxyType' ); const defaultDoubanImageProxyType = (window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY_TYPE || 'cmliussss-cdn-tencent'; if (savedDoubanImageProxyType !== null) { setDoubanImageProxyType(savedDoubanImageProxyType); } else if (defaultDoubanImageProxyType) { setDoubanImageProxyType(defaultDoubanImageProxyType); } const savedDoubanImageProxyUrl = localStorage.getItem( 'doubanImageProxyUrl' ); const defaultDoubanImageProxyUrl = (window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY || ''; if (savedDoubanImageProxyUrl !== null) { setDoubanImageProxyUrl(savedDoubanImageProxyUrl); } else if (defaultDoubanImageProxyUrl) { setDoubanImageProxyUrl(defaultDoubanImageProxyUrl); } const savedTmdbImageBaseUrl = localStorage.getItem('tmdbImageBaseUrl'); if (savedTmdbImageBaseUrl !== null) { setTmdbImageBaseUrl(savedTmdbImageBaseUrl); } const savedEnableOptimization = localStorage.getItem('enableOptimization'); if (savedEnableOptimization !== null) { setEnableOptimization(JSON.parse(savedEnableOptimization)); } const savedSpeedTestTimeout = localStorage.getItem('speedTestTimeout'); if (savedSpeedTestTimeout !== null) { setSpeedTestTimeout(Number(savedSpeedTestTimeout)); } const savedFluidSearch = localStorage.getItem('fluidSearch'); const defaultFluidSearch = (window as any).RUNTIME_CONFIG?.FLUID_SEARCH !== false; if (savedFluidSearch !== null) { setFluidSearch(JSON.parse(savedFluidSearch)); } else if (defaultFluidSearch !== undefined) { setFluidSearch(defaultFluidSearch); } const savedLiveDirectConnect = localStorage.getItem('liveDirectConnect'); if (savedLiveDirectConnect !== null) { setLiveDirectConnect(JSON.parse(savedLiveDirectConnect)); } const savedTmdbBackdropDisabled = localStorage.getItem('tmdb_backdrop_disabled'); if (savedTmdbBackdropDisabled !== null) { setTmdbBackdropDisabled(savedTmdbBackdropDisabled === 'true'); } const savedEnableTrailers = localStorage.getItem('enableTrailers'); if (savedEnableTrailers !== null) { setEnableTrailers(savedEnableTrailers === 'true'); } const savedBufferStrategy = localStorage.getItem('bufferStrategy'); if (savedBufferStrategy !== null) { setBufferStrategy(savedBufferStrategy); } const savedNextEpisodePreCache = localStorage.getItem('nextEpisodePreCache'); if (savedNextEpisodePreCache !== null) { setNextEpisodePreCache(savedNextEpisodePreCache === 'true'); } const savedNextEpisodeDanmakuPreload = localStorage.getItem('nextEpisodeDanmakuPreload'); if (savedNextEpisodeDanmakuPreload !== null) { setNextEpisodeDanmakuPreload(savedNextEpisodeDanmakuPreload === 'true'); } const savedDisableAutoLoadDanmaku = localStorage.getItem('disableAutoLoadDanmaku'); if (savedDisableAutoLoadDanmaku !== null) { setDisableAutoLoadDanmaku(savedDisableAutoLoadDanmaku === 'true'); } const savedDanmakuMaxCount = localStorage.getItem('danmakuMaxCount'); if (savedDanmakuMaxCount !== null) { setDanmakuMaxCount(parseInt(savedDanmakuMaxCount, 10)); } const savedDanmakuHeatmapDisabled = localStorage.getItem('danmaku_heatmap_disabled'); if (savedDanmakuHeatmapDisabled !== null) { setDanmakuHeatmapDisabled(savedDanmakuHeatmapDisabled === 'true'); } const savedHomeBannerEnabled = localStorage.getItem('homeBannerEnabled'); if (savedHomeBannerEnabled !== null) { setHomeBannerEnabled(savedHomeBannerEnabled === 'true'); } const savedHomeContinueWatchingEnabled = localStorage.getItem('homeContinueWatchingEnabled'); if (savedHomeContinueWatchingEnabled !== null) { setHomeContinueWatchingEnabled(savedHomeContinueWatchingEnabled === 'true'); } // 加载首页模块配置 const savedHomeModules = localStorage.getItem('homeModules'); if (savedHomeModules !== null) { try { setHomeModules(JSON.parse(savedHomeModules)); } catch (error) { console.error('解析首页模块配置失败:', error); } } // 加载搜索繁体转简体设置 const savedSearchTraditionalToSimplified = localStorage.getItem('searchTraditionalToSimplified'); if (savedSearchTraditionalToSimplified !== null) { setSearchTraditionalToSimplified(savedSearchTraditionalToSimplified === 'true'); } // 加载精确搜索设置 const savedExactSearch = localStorage.getItem('exactSearch'); if (savedExactSearch !== null) { setExactSearch(savedExactSearch === 'true'); } // 加载最大同时下载限制设置 const savedMaxConcurrentDownloads = localStorage.getItem('maxConcurrentDownloads'); if (savedMaxConcurrentDownloads !== null) { setMaxConcurrentDownloads(Number(savedMaxConcurrentDownloads)); } // 加载单任务线程数设置 const savedDownloadThreadsPerTask = localStorage.getItem('downloadThreadsPerTask'); if (savedDownloadThreadsPerTask !== null) { setDownloadThreadsPerTask(Number(savedDownloadThreadsPerTask)); } // 加载下载模式设置 const savedDownloadMode = localStorage.getItem('downloadMode'); if (savedDownloadMode === 'browser' || savedDownloadMode === 'filesystem') { setDownloadMode(savedDownloadMode); } // 加载保存路径设置 const savedFilesystemSavePath = localStorage.getItem('filesystemSavePath'); if (savedFilesystemSavePath !== null) { setFilesystemSavePath(savedFilesystemSavePath); } } }, []); // 加载邮件通知设置 const loadEmailSettings = async () => { setEmailSettingsLoading(true); try { const response = await fetch('/api/user/email-settings'); if (response.ok) { const data = await response.json(); setUserEmail(data.email || ''); setEmailNotifications(data.emailNotifications || false); } } catch (error) { console.error('加载邮件设置失败:', error); } finally { setEmailSettingsLoading(false); } }; // 保存邮件通知设置 const handleSaveEmailSettings = async () => { setEmailSettingsSaving(true); try { const response = await fetch('/api/user/email-settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: userEmail, emailNotifications, }), }); const messageEl = document.getElementById('email-settings-message'); if (response.ok) { if (messageEl) { messageEl.textContent = '保存成功!'; messageEl.className = 'text-xs text-center text-green-600 dark:text-green-400'; messageEl.classList.remove('hidden'); setTimeout(() => { messageEl.classList.add('hidden'); }, 3000); } } else { const data = await response.json(); if (messageEl) { messageEl.textContent = data.error || '保存失败'; messageEl.className = 'text-xs text-center text-red-600 dark:text-red-400'; messageEl.classList.remove('hidden'); } } } catch (error) { console.error('保存邮件设置失败:', error); const messageEl = document.getElementById('email-settings-message'); if (messageEl) { messageEl.textContent = '保存失败,请重试'; messageEl.className = 'text-xs text-center text-red-600 dark:text-red-400'; messageEl.classList.remove('hidden'); } } finally { setEmailSettingsSaving(false); } }; // 加载设备列表 const loadDevices = async () => { setDevicesLoading(true); try { const response = await fetch('/api/auth/devices'); if (response.ok) { const data = await response.json(); setDevices(data.devices || []); } } catch (error) { console.error('加载设备列表失败:', error); } finally { setDevicesLoading(false); } }; // 撤销单个设备 const handleRevokeDevice = async (tokenId: string) => { setConfirmDialog({ isOpen: true, title: '撤销设备登录', message: '确定要撤销该设备的登录吗?', onConfirm: async () => { setConfirmDialog({ ...confirmDialog, isOpen: false }); setRevoking(tokenId); try { const response = await fetch('/api/auth/devices', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tokenId }), }); if (response.ok) { // 重新加载设备列表 await loadDevices(); } else { alert('撤销失败,请重试'); } } catch (error) { console.error('撤销设备失败:', error); alert('撤销失败,请重试'); } finally { setRevoking(null); } }, }); }; // 撤销所有设备 const handleRevokeAllDevices = async () => { setConfirmDialog({ isOpen: true, title: '登出所有设备', message: '确定要登出所有设备吗?这将清除所有设备的登录状态(包括当前设备)。', onConfirm: async () => { setConfirmDialog({ ...confirmDialog, isOpen: false }); try { const response = await fetch('/api/auth/devices', { method: 'POST', }); if (response.ok) { // 登出所有设备后,重定向到首页 window.location.href = '/'; } else { alert('操作失败,请重试'); } } catch (error) { console.error('登出所有设备失败:', error); alert('操作失败,请重试'); } }, }); }; // 根据设备类型返回对应的图标 const getDeviceIcon = (deviceInfo: string) => { const info = deviceInfo.toLowerCase(); if (info.includes('mobile') || info.includes('iphone') || info.includes('android')) { return Smartphone; } if (info.includes('tablet') || info.includes('ipad')) { return Tablet; } return Monitor; }; // 点击外部区域关闭下拉框 useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (isDoubanDropdownOpen) { const target = event.target as Element; if (!target.closest('[data-dropdown="douban-datasource"]')) { setIsDoubanDropdownOpen(false); } } }; if (isDoubanDropdownOpen) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } }, [isDoubanDropdownOpen]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (isDoubanImageProxyDropdownOpen) { const target = event.target as Element; if (!target.closest('[data-dropdown="douban-image-proxy"]')) { setIsDoubanImageProxyDropdownOpen(false); } } }; if (isDoubanImageProxyDropdownOpen) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } }, [isDoubanImageProxyDropdownOpen]); const handleMenuClick = () => { setIsOpen(!isOpen); }; const handleCloseMenu = () => { setIsOpen(false); }; const handleLogout = async () => { try { await fetch('/api/logout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, }); } catch (error) { console.error('注销请求失败:', error); } window.location.href = '/'; }; const handleAdminPanel = () => { router.push('/admin'); }; const handleChangePassword = () => { setIsOpen(false); setIsChangePasswordOpen(true); setNewPassword(''); setConfirmPassword(''); setPasswordError(''); }; const handleCloseChangePassword = () => { setIsChangePasswordOpen(false); setNewPassword(''); setConfirmPassword(''); setPasswordError(''); }; const handleSubscribe = async () => { setIsOpen(false); setIsSubscribeOpen(true); setCopySuccess(false); // 懒加载:打开面板时才请求订阅URL await fetchSubscribeUrl(); }; const handleCloseSubscribe = () => { setIsSubscribeOpen(false); setCopySuccess(false); setCopySuccessAdFilter(false); }; const handleCopySubscribeUrl = async () => { try { await navigator.clipboard.writeText(subscribeUrl); setCopySuccess(true); setTimeout(() => { setCopySuccess(false); }, 2000); } catch (error) { console.error('复制失败:', error); } }; const handleCopySubscribeUrlWithAdFilter = async () => { try { await navigator.clipboard.writeText(subscribeUrlWithAdFilter); setCopySuccessAdFilter(true); setTimeout(() => { setCopySuccessAdFilter(false); }, 2000); } catch (error) { console.error('复制失败:', error); } }; const handleSubmitChangePassword = async () => { setPasswordError(''); // 验证密码 if (!newPassword) { setPasswordError('新密码不得为空'); return; } if (newPassword !== confirmPassword) { setPasswordError('两次输入的密码不一致'); return; } setPasswordLoading(true); try { const response = await fetch('/api/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ newPassword, }), }); const data = await response.json(); if (!response.ok) { setPasswordError(data.error || '修改密码失败'); return; } // 修改成功,关闭弹窗并登出 setIsChangePasswordOpen(false); await handleLogout(); } catch (error) { setPasswordError('网络错误,请稍后重试'); } finally { setPasswordLoading(false); } }; const handleSettings = () => { setIsOpen(false); setIsSettingsOpen(true); }; const handleCloseSettings = () => { setIsSettingsOpen(false); }; // 设置相关的处理函数 const handleAggregateToggle = (value: boolean) => { setDefaultAggregateSearch(value); if (typeof window !== 'undefined') { localStorage.setItem('defaultAggregateSearch', JSON.stringify(value)); } }; const handleDoubanProxyUrlChange = (value: string) => { setDoubanProxyUrl(value); if (typeof window !== 'undefined') { localStorage.setItem('doubanProxyUrl', value); } }; const handleOptimizationToggle = (value: boolean) => { setEnableOptimization(value); if (typeof window !== 'undefined') { localStorage.setItem('enableOptimization', JSON.stringify(value)); } }; const handleSpeedTestTimeoutChange = (value: number) => { setSpeedTestTimeout(value); if (typeof window !== 'undefined') { localStorage.setItem('speedTestTimeout', String(value)); } }; const handleMaxConcurrentDownloadsChange = (value: number) => { setMaxConcurrentDownloads(value); if (typeof window !== 'undefined') { localStorage.setItem('maxConcurrentDownloads', String(value)); } }; const handleDownloadThreadsPerTaskChange = (value: number) => { setDownloadThreadsPerTask(value); if (typeof window !== 'undefined') { localStorage.setItem('downloadThreadsPerTask', String(value)); } }; const handleDownloadModeChange = (mode: 'browser' | 'filesystem') => { // 如果选择 filesystem 模式,先检测浏览器是否支持 if (mode === 'filesystem' && typeof window !== 'undefined' && !('showDirectoryPicker' in window)) { setConfirmDialog({ isOpen: true, title: '浏览器不支持', message: '您的浏览器不支持 File System Access API,请使用 Chrome 86+ 或 Edge 86+', onConfirm: () => { setConfirmDialog({ ...confirmDialog, isOpen: false }); }, }); return; } setDownloadMode(mode); if (typeof window !== 'undefined') { localStorage.setItem('downloadMode', mode); } }; const handleSelectSavePath = async () => { try { const dirHandle = await (window as any).showDirectoryPicker(); setFilesystemSavePath(dirHandle.name); localStorage.setItem('filesystemSavePath', dirHandle.name); // 保存目录句柄到 IndexedDB const dbName = 'MoonTVPlus'; const storeName = 'dirHandles'; // 使用 Promise 包装 IndexedDB 操作 await new Promise((resolve, reject) => { const request = indexedDB.open(dbName, 2); // 使用版本 2,与 download-db.ts 保持一致 request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; // 创建 dirHandles 表(如果不存在) if (!db.objectStoreNames.contains(storeName)) { db.createObjectStore(storeName); } // 创建 activeTasks 表(如果不存在) if (!db.objectStoreNames.contains('activeTasks')) { const activeStore = db.createObjectStore('activeTasks', { keyPath: 'id' }); activeStore.createIndex('status', 'status', { unique: false }); activeStore.createIndex('createdAt', 'createdAt', { unique: false }); } // 创建 completedTasks 表(如果不存在) if (!db.objectStoreNames.contains('completedTasks')) { 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 }); } }; request.onsuccess = (event) => { const db = (event.target as IDBOpenDBRequest).result; const transaction = db.transaction([storeName], 'readwrite'); const store = transaction.objectStore(storeName); const putRequest = store.put(dirHandle, 'downloadDir'); putRequest.onsuccess = () => { db.close(); resolve(); }; putRequest.onerror = () => { db.close(); reject(new Error('保存目录句柄失败')); }; }; request.onerror = () => { reject(new Error('无法打开 IndexedDB')); }; }); } catch (err) { console.error('选择目录失败:', err); } }; const handleFluidSearchToggle = (value: boolean) => { setFluidSearch(value); if (typeof window !== 'undefined') { localStorage.setItem('fluidSearch', JSON.stringify(value)); } }; const handleLiveDirectConnectToggle = (value: boolean) => { setLiveDirectConnect(value); if (typeof window !== 'undefined') { localStorage.setItem('liveDirectConnect', JSON.stringify(value)); } }; const handleTmdbBackdropDisabledToggle = (value: boolean) => { setTmdbBackdropDisabled(value); if (typeof window !== 'undefined') { localStorage.setItem('tmdb_backdrop_disabled', String(value)); } }; const handleEnableTrailersToggle = (value: boolean) => { setEnableTrailers(value); if (typeof window !== 'undefined') { localStorage.setItem('enableTrailers', String(value)); } }; const handleDoubanDataSourceChange = (value: string) => { setDoubanDataSource(value); if (typeof window !== 'undefined') { localStorage.setItem('doubanDataSource', value); } }; const handleDoubanImageProxyTypeChange = (value: string) => { setDoubanImageProxyType(value); if (typeof window !== 'undefined') { localStorage.setItem('doubanImageProxyType', value); } }; const handleDoubanImageProxyUrlChange = (value: string) => { setDoubanImageProxyUrl(value); if (typeof window !== 'undefined') { localStorage.setItem('doubanImageProxyUrl', value); } }; const handleTmdbImageBaseUrlChange = (value: string) => { setTmdbImageBaseUrl(value); if (typeof window !== 'undefined') { localStorage.setItem('tmdbImageBaseUrl', value); } }; const handleBufferStrategyChange = (value: string) => { setBufferStrategy(value); if (typeof window !== 'undefined') { localStorage.setItem('bufferStrategy', value); } }; // 将滑块值转换为策略值 const getBufferStrategyFromSlider = (sliderValue: number): string => { const strategies = ['low', 'medium', 'high', 'ultra']; return strategies[sliderValue] || 'medium'; }; // 将策略值转换为滑块值 const getSliderValueFromStrategy = (strategy: string): number => { const strategies = ['low', 'medium', 'high', 'ultra']; const index = strategies.indexOf(strategy); return index >= 0 ? index : 1; // 默认返回 1 (medium) }; const handleNextEpisodePreCacheToggle = (value: boolean) => { setNextEpisodePreCache(value); if (typeof window !== 'undefined') { localStorage.setItem('nextEpisodePreCache', String(value)); } }; const handleNextEpisodeDanmakuPreloadToggle = (value: boolean) => { setNextEpisodeDanmakuPreload(value); if (typeof window !== 'undefined') { localStorage.setItem('nextEpisodeDanmakuPreload', String(value)); } }; const handleDisableAutoLoadDanmakuToggle = (value: boolean) => { setDisableAutoLoadDanmaku(value); if (typeof window !== 'undefined') { localStorage.setItem('disableAutoLoadDanmaku', String(value)); } }; const handleDanmakuMaxCountChange = (value: number) => { setDanmakuMaxCount(value); if (typeof window !== 'undefined') { localStorage.setItem('danmakuMaxCount', String(value)); } }; const handleDanmakuHeatmapDisabledToggle = (value: boolean) => { setDanmakuHeatmapDisabled(value); if (typeof window !== 'undefined') { localStorage.setItem('danmaku_heatmap_disabled', String(value)); } }; const handleSearchTraditionalToSimplifiedToggle = (value: boolean) => { setSearchTraditionalToSimplified(value); if (typeof window !== 'undefined') { localStorage.setItem('searchTraditionalToSimplified', String(value)); } }; const handleExactSearchToggle = (value: boolean) => { setExactSearch(value); if (typeof window !== 'undefined') { localStorage.setItem('exactSearch', String(value)); } }; const handleHomeBannerToggle = (value: boolean) => { setHomeBannerEnabled(value); if (typeof window !== 'undefined') { localStorage.setItem('homeBannerEnabled', String(value)); window.dispatchEvent(new CustomEvent('homeModulesUpdated')); } }; const handleHomeContinueWatchingToggle = (value: boolean) => { setHomeContinueWatchingEnabled(value); if (typeof window !== 'undefined') { localStorage.setItem('homeContinueWatchingEnabled', String(value)); window.dispatchEvent(new CustomEvent('homeModulesUpdated')); } }; // 首页模块配置处理函数 const handleHomeModuleToggle = (id: string, enabled: boolean) => { const updatedModules = homeModules.map(module => module.id === id ? { ...module, enabled } : module ); setHomeModules(updatedModules); if (typeof window !== 'undefined') { localStorage.setItem('homeModules', JSON.stringify(updatedModules)); // 触发自定义事件通知首页刷新 window.dispatchEvent(new CustomEvent('homeModulesUpdated')); } }; const handleHomeModuleMoveUp = (index: number) => { if (index === 0) return; const updatedModules = [...homeModules]; const temp = updatedModules[index]; updatedModules[index] = updatedModules[index - 1]; updatedModules[index - 1] = temp; // 更新order updatedModules.forEach((module, idx) => { module.order = idx; }); setHomeModules(updatedModules); if (typeof window !== 'undefined') { localStorage.setItem('homeModules', JSON.stringify(updatedModules)); window.dispatchEvent(new CustomEvent('homeModulesUpdated')); } }; const handleHomeModuleMoveDown = (index: number) => { if (index === homeModules.length - 1) return; const updatedModules = [...homeModules]; const temp = updatedModules[index]; updatedModules[index] = updatedModules[index + 1]; updatedModules[index + 1] = temp; // 更新order updatedModules.forEach((module, idx) => { module.order = idx; }); setHomeModules(updatedModules); if (typeof window !== 'undefined') { localStorage.setItem('homeModules', JSON.stringify(updatedModules)); window.dispatchEvent(new CustomEvent('homeModulesUpdated')); } }; // 获取感谢信息 const getThanksInfo = (dataSource: string) => { switch (dataSource) { case 'cors-proxy-zwei': return { text: 'Thanks to @Zwei', url: 'https://github.com/bestzwei', }; case 'cmliussss-cdn-tencent': case 'cmliussss-cdn-ali': return { text: 'Thanks to @CMLiussss', url: 'https://github.com/cmliu', }; default: return null; } }; const handleResetSettings = () => { const defaultDoubanProxyType = (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'; const defaultDoubanImageProxyUrl = (window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY || ''; const defaultFluidSearch = (window as any).RUNTIME_CONFIG?.FLUID_SEARCH !== false; setDefaultAggregateSearch(true); setEnableOptimization(true); setFluidSearch(defaultFluidSearch); setLiveDirectConnect(false); setTmdbBackdropDisabled(false); setEnableTrailers(false); setDoubanProxyUrl(defaultDoubanProxy); setDoubanDataSource(defaultDoubanProxyType); setDoubanImageProxyType(defaultDoubanImageProxyType); setDoubanImageProxyUrl(defaultDoubanImageProxyUrl); setTmdbImageBaseUrl('https://image.tmdb.org'); setBufferStrategy('medium'); setNextEpisodePreCache(true); setNextEpisodeDanmakuPreload(true); setDisableAutoLoadDanmaku(false); setHomeBannerEnabled(true); setHomeContinueWatchingEnabled(true); setHomeModules(defaultHomeModules); setSearchTraditionalToSimplified(false); if (typeof window !== 'undefined') { localStorage.setItem('defaultAggregateSearch', JSON.stringify(true)); localStorage.setItem('enableOptimization', JSON.stringify(true)); localStorage.setItem('fluidSearch', JSON.stringify(defaultFluidSearch)); localStorage.setItem('liveDirectConnect', JSON.stringify(false)); localStorage.setItem('tmdb_backdrop_disabled', 'false'); localStorage.setItem('enableTrailers', 'false'); localStorage.setItem('doubanProxyUrl', defaultDoubanProxy); localStorage.setItem('doubanDataSource', defaultDoubanProxyType); localStorage.setItem('doubanImageProxyType', defaultDoubanImageProxyType); localStorage.setItem('doubanImageProxyUrl', defaultDoubanImageProxyUrl); localStorage.setItem('tmdbImageBaseUrl', 'https://image.tmdb.org'); localStorage.setItem('bufferStrategy', 'medium'); localStorage.setItem('nextEpisodePreCache', 'true'); localStorage.setItem('nextEpisodeDanmakuPreload', 'true'); localStorage.setItem('disableAutoLoadDanmaku', 'false'); localStorage.setItem('danmakuMaxCount', '0'); localStorage.setItem('danmaku_heatmap_disabled', 'false'); localStorage.setItem('homeBannerEnabled', 'true'); localStorage.setItem('homeContinueWatchingEnabled', 'true'); localStorage.setItem('homeModules', JSON.stringify(defaultHomeModules)); localStorage.setItem('searchTraditionalToSimplified', 'false'); window.dispatchEvent(new CustomEvent('homeModulesUpdated')); } }; // 清除弹幕缓存 const handleClearDanmakuCache = async () => { setIsClearingCache(true); setClearCacheMessage(null); try { await clearAllDanmakuCache(); setClearCacheMessage('弹幕缓存已清除成功!'); console.log('弹幕缓存已清除'); // 3秒后自动清除提示 setTimeout(() => { setClearCacheMessage(null); }, 3000); } catch (error) { console.error('清除弹幕缓存失败:', error); setClearCacheMessage('清除失败,请重试'); // 3秒后自动清除提示 setTimeout(() => { setClearCacheMessage(null); }, 3000); } finally { setIsClearingCache(false); } }; // 检查是否显示管理面板按钮 const showAdminPanel = (authInfo?.role === 'owner' || authInfo?.role === 'admin') && storageType !== 'localstorage'; // 检查是否显示离线下载按钮 const showOfflineDownload = (authInfo?.role === 'owner' || authInfo?.role === 'admin') && typeof window !== 'undefined' && (window as any).RUNTIME_CONFIG?.ENABLE_OFFLINE_DOWNLOAD === true; // 检查是否显示修改密码按钮 const showChangePassword = authInfo?.role !== 'owner' && storageType !== 'localstorage'; // 角色中文映射 const getRoleText = (role?: string) => { switch (role) { case 'owner': return '站长'; case 'admin': return '管理员'; case 'user': return '用户'; default: return ''; } }; // 菜单面板内容 const menuPanel = ( <> {/* 背景遮罩 - 普通菜单无需模糊 */}
{/* 菜单面板 */}
{/* 用户信息区域 */}
当前用户 {/* 邮件设置图标按钮 */} {/* 设备管理图标按钮 */} {storageType !== 'localstorage' && ( )}
{getRoleText(authInfo?.role || 'user')}
{authInfo?.username || 'default'}
数据存储: {storageType === 'localstorage' ? '本地' : storageType}
{/* 菜单项 */}
{/* 通知按钮 */} {/* 我的收藏按钮 */} {/* 设置按钮 */} {/* 管理面板按钮 */} {showAdminPanel && ( )} {/* 离线下载按钮 */} {showOfflineDownload && ( )} {/* 修改密码按钮 */} {showChangePassword && ( )} {/* 订阅按钮 */} {subscribeEnabled && ( )} {/* 生态应用按钮 */} {/* 分割线 */}
{/* 登出按钮 */} {/* 分割线 */}
{/* 版本信息 */}
); // 设置面板内容 const settingsPanel = ( <> {/* 背景遮罩 */}
{ // 只阻止滚动,允许其他触摸事件 e.preventDefault(); }} onWheel={(e) => { // 阻止滚轮滚动 e.preventDefault(); }} style={{ touchAction: 'none', }} /> {/* 设置面板 */}
{/* 内容容器 - 独立的滚动区域 */}
{/* 标题栏 */}

本地设置

{/* 设置项 */}
{/* 豆瓣设置 */}
{isDoubanSectionOpen && (
{/* 豆瓣数据源选择 */}

豆瓣数据代理

选择获取豆瓣数据的方式

{/* 自定义下拉选择框 */} {/* 下拉箭头 */}
{/* 下拉选项列表 */} {isDoubanDropdownOpen && (
{doubanDataSourceOptions.map((option) => ( ))}
)}
{/* 感谢信息 */} {getThanksInfo(doubanDataSource) && (
)}
{/* 豆瓣代理地址设置 - 仅在选择自定义代理时显示 */} {doubanDataSource === 'custom' && (

豆瓣代理地址

自定义代理服务器地址

handleDoubanProxyUrlChange(e.target.value)} />
)} {/* 分割线 */}
{/* 豆瓣图片代理设置 */}

豆瓣图片代理

选择获取豆瓣图片的方式

{/* 自定义下拉选择框 */} {/* 下拉箭头 */}
{/* 下拉选项列表 */} {isDoubanImageProxyDropdownOpen && (
{doubanImageProxyTypeOptions.map((option) => ( ))}
)}
{/* 感谢信息 */} {getThanksInfo(doubanImageProxyType) && (
)}
{/* 豆瓣图片代理地址设置 - 仅在选择自定义代理时显示 */} {doubanImageProxyType === 'custom' && (

豆瓣图片代理地址

自定义图片代理服务器地址

handleDoubanImageProxyUrlChange(e.target.value) } />
)} {/* 分割线 */}
{/* TMDB 图片网络请求地址设置 */}

TMDB 图片网络请求地址

TMDB 图片的 Base URL(默认: https://image.tmdb.org)

handleTmdbImageBaseUrlChange(e.target.value) } />
)}
{isUsageSectionOpen && (
{/* 默认聚合搜索结果 */}

默认聚合搜索结果

搜索时默认按标题和年份聚合显示结果

{/* 优选和测速 */}

优选和测速

如出现播放器劫持问题可关闭

{/* 测速超时设置 */} {enableOptimization && (
换源面板测速超时 {speedTestTimeout / 1000}秒
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%)` }} />

注:此设置仅对换源面板测速生效,优选播放源时仍使用4秒超时

)} {/* 流式搜索 */}

流式搜索输出

启用搜索结果实时流式输出,关闭后使用传统一次性搜索

{/* 直播视频浏览器直连 */}

IPTV 视频浏览器直连

开启 IPTV 视频浏览器直连时,需要自备 Allow CORS 插件

{/* 禁用背景图渲染 */}

禁用背景图渲染

关闭播放页面的TMDB背景图显示(需手动刷新页面生效)

{/* 启用预告片 */}

首页预告片

在首页轮播图中显示视频预告片(需刷新页面生效)

{/* 搜索繁体转简体 */}

搜索繁体转简体

搜索时自动将繁体中文转换为简体中文

{/* 精确搜索 */}

精确搜索

开启后,搜索结果将过滤掉不包含搜索词的内容

)}
{/* 下载设置 */}
{isDownloadSectionOpen && (
{/* 最大同时下载限制 */}

最大同时下载限制

控制播放页面下载时的同时下载数量

同时下载数量 {maxConcurrentDownloads}个
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%)` }} />
{/* 单任务线程数 */}

单任务线程数

控制每个下载任务使用的线程数量,线程越多下载越快但占用资源越多

线程数量 {downloadThreadsPerTask}个
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%)` }} />
{/* 下载模式 */}

下载模式

{/* 保存路径选择(仅在 filesystem 模式显示) */} {downloadMode === 'filesystem' && (

需要 Chrome 86+ 或 Edge 86+ 浏览器支持

)}
{/* 下载文件管理 */}
)}
{/* 缓冲设置 */}
{isBufferSectionOpen && (

调整播放器缓冲策略(仅在播放页面生效)

{/* 缓冲策略 */}

缓冲策略

设置视频缓冲块大小,影响播放流畅度和流量消耗

{/* 滑块控件 */}
{ const sliderValue = parseInt(e.target.value); 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%)` }} /> {/* 标签显示 */}
低缓冲 中缓冲 高缓冲 超高缓冲
{/* 当前选择的说明 */}
{ bufferStrategyOptions.find( (option) => option.value === bufferStrategy )?.label }
{/* 下集预缓冲 */}

下集预缓冲

播放进度达到90%时,自动预缓冲下一集内容

)}
{/* 弹幕设置 */}
{isDanmakuSectionOpen && (
{/* 禁用自动装填弹幕 */}

禁用自动装填弹幕

开启后,播放页面不会自动匹配弹幕,只能手动匹配

{/* 下集弹幕预加载 */}

下集弹幕预加载

播放进度达到90%时,自动预加载下一集弹幕

{/* 禁用弹幕热力图 */}

禁用弹幕热力图

开启后不显示弹幕热力图和热力图开关

{/* 弹幕加载上限 */}
弹幕加载上限 {danmakuMaxCount === 0 ? '无上限' : `${danmakuMaxCount} 条`}
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%)` }} />

限制加载的弹幕数量,减少性能消耗

{/* 清除弹幕缓存 */}

弹幕缓存管理

清除所有已缓存的弹幕数据

{/* 成功/失败提示 */} {clearCacheMessage && (
{clearCacheMessage}
)}
)}
{/* 首页设置 */}
{isHomepageSectionOpen && (

配置首页模块的显示顺序和可见性

{/* 首页顶部组件显示 */}
首页轮播图
继续观看
{/* 模块列表 */}
{homeModules.map((module, index) => (
{/* 左侧:显示/隐藏开关 */} {/* 中间:模块名称 */}
{module.name}
{/* 右侧:上下移动按钮 */}
))}
{/* 恢复默认按钮 */} {/* 提示信息 */}

💡 提示:点击眼睛图标可显示/隐藏模块,使用箭头按钮调整模块顺序

)}
{/* 底部说明 */}

这些设置保存在本地浏览器中

); // 订阅面板内容 const subscribePanel = ( <> {/* 背景遮罩 */}
{ e.preventDefault(); }} onWheel={(e) => { e.preventDefault(); }} style={{ touchAction: 'none', }} /> {/* 订阅面板 */}
{ e.stopPropagation(); }} style={{ touchAction: 'auto', }} > {/* 标题栏 */}

TVBox订阅

{/* 内容 */}
{isLoadingSubscribeUrl ? ( <> {/* 加载骨架 - 订阅链接(标准) */}
{/* 加载骨架 - 订阅链接(去广告) */}
{/* 加载骨架 - 重置按钮 */}
) : ( <> {/* 订阅链接(标准) */}

订阅链接(标准)

{/* 订阅链接(去广告) */}

订阅链接(去广告)

💡 去广告需要经过服务器代理,某些源可能因为区域或兼容问题无法播放

{/* 重置Token按钮 */}

⚠️ 重置后旧链接将失效

{/* 消息提示 */}

)}
{/* 底部说明 */}

将订阅链接复制到TVBox应用中使用

); // 修改密码面板内容 const changePasswordPanel = ( <> {/* 背景遮罩 */}
{ // 只阻止滚动,允许其他触摸事件 e.preventDefault(); }} onWheel={(e) => { // 阻止滚轮滚动 e.preventDefault(); }} style={{ touchAction: 'none', }} /> {/* 修改密码面板 */}
{/* 内容容器 - 独立的滚动区域 */}
{ // 阻止事件冒泡到遮罩层,但允许内部滚动 e.stopPropagation(); }} style={{ touchAction: 'auto', // 允许所有触摸操作 }} > {/* 标题栏 */}

修改密码

{/* 表单 */}
{/* 新密码输入 */}
setNewPassword(e.target.value)} disabled={passwordLoading} />
{/* 确认密码输入 */}
setConfirmPassword(e.target.value)} disabled={passwordLoading} />
{/* 错误信息 */} {passwordError && (
{passwordError}
)}
{/* 操作按钮 */}
{/* 底部说明 */}

修改密码后需要重新登录

); // 邮件设置面板内容 const emailSettingsPanel = ( <> {/* 背景遮罩 */}
setIsEmailSettingsOpen(false)} onTouchMove={(e) => { e.preventDefault(); }} onWheel={(e) => { e.preventDefault(); }} style={{ touchAction: 'none', }} /> {/* 邮件设置面板 */}
{ e.stopPropagation(); }} style={{ touchAction: 'auto', }} > {/* 标题栏 */}

邮件通知设置

{/* 表单 */} {emailSettingsLoading ? (
{/* 加载骨架屏 */}
加载中...
) : (
setUserEmail(e.target.value)} placeholder='输入您的邮箱地址' disabled={emailSettingsSaving} className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed' />

接收收藏更新通知

当收藏的影片有更新时发送邮件通知

)} {/* 提示信息 */}

💡 提示:需要管理员先在管理面板中配置邮件服务

); // 设备管理面板内容 const deviceManagementPanel = ( <> {/* 背景遮罩 */}
setIsDeviceManagementOpen(false)} onTouchMove={(e) => { e.preventDefault(); }} onWheel={(e) => { e.preventDefault(); }} style={{ touchAction: 'none', }} /> {/* 设备管理面板 */}
{ e.stopPropagation(); }} style={{ touchAction: 'auto', }} > {/* 标题栏 */}

设备管理

{/* 设备列表 */}
{devicesLoading ? (
{[1, 2, 3].map((i) => (
))}
加载中...
) : devices.length === 0 ? (

暂无登录设备

) : (
{devices .sort((a, b) => { // 当前设备置顶 if (a.isCurrent && !b.isCurrent) return -1; if (!a.isCurrent && b.isCurrent) return 1; return 0; }) .map((device) => { const DeviceIcon = getDeviceIcon(device.deviceInfo); return (
{device.deviceInfo} {device.isCurrent && ( 当前设备 )}
登录时间: {new Date(device.createdAt).toLocaleString('zh-CN')}
最后活跃: {new Date(device.lastUsed).toLocaleString('zh-CN')}
{!device.isCurrent && ( )}
); })}
)}
{/* 底部操作 */}

登出所有设备后需要重新登录

); // 举报信息弹窗 const reportPanel = ( <> {/* 背景遮罩 */}
setIsReportOpen(false)} onTouchMove={(e) => { e.preventDefault(); }} onWheel={(e) => { e.preventDefault(); }} style={{ touchAction: 'none', }} /> {/* 举报信息面板 */}
{ e.stopPropagation(); }} style={{ touchAction: 'auto', }} > {/* 标题栏 */}

耻辱柱

{/* 内容区域 */}

抄袭狗SzeMeng76毫无廉耻,盯着本项目的commit区,疯狂抄袭。警告亦全当看不见,实为开源界耻辱。

超分,观影室,豆瓣反爬,精确搜索等等等等,直接抄袭,最不要脸的就是,刚更新一版,几小时后直接抄走。

2026-02-25:抄袭emby功能

{/* 底部按钮 */}
); // 生态应用面板内容 const ecoAppsPanel = ( <> {/* 背景遮罩 */}
setIsEcoAppsOpen(false)} onTouchMove={(e) => { e.preventDefault(); }} onWheel={(e) => { e.preventDefault(); }} style={{ touchAction: 'none', }} /> {/* 生态应用面板 */}
{ e.stopPropagation(); }} style={{ touchAction: 'auto', }} > {/* 标题栏 */}

生态应用

{/* 举报按钮 */} {/* 关闭按钮 */}
{/* 应用列表 */}
{/* MoonTVPlus-PC 客户端 */}
MoonTVPlus-PC

MoonTVPlus-PC客户端

专为Windows开发的客户端,完美支持私人影库mkv视频

下载
{/* Selene 跨平台客户端 */}
Selene 二开

Selene 跨平台客户端

多平台客户端

{/* OrionTV TV专用客户端 */}
OrionTV 二开

OrionTV TV专用客户端

tv专用

下载
{/* 底部说明 */}

选择适合您设备的客户端下载使用

); return ( <>
{/* 版本更新红点 */} {updateStatus === UpdateStatus.HAS_UPDATE && (
)} {/* 未读通知红点 */} {unreadCount > 0 && (
)}
{/* 使用 Portal 将菜单面板渲染到 document.body */} {isOpen && mounted && createPortal(menuPanel, document.body)} {/* 使用 Portal 将设置面板渲染到 document.body */} {isSettingsOpen && mounted && createPortal(settingsPanel, document.body)} {/* 使用 Portal 将修改密码面板渲染到 document.body */} {isChangePasswordOpen && mounted && createPortal(changePasswordPanel, document.body)} {/* 使用 Portal 将订阅面板渲染到 document.body */} {isSubscribeOpen && mounted && createPortal(subscribePanel, document.body)} {/* 版本面板 */} setIsVersionPanelOpen(false)} /> {/* 离线下载面板 */} setIsOfflineDownloadPanelOpen(false)} /> {/* 使用 Portal 将通知面板渲染到 document.body */} {isNotificationPanelOpen && mounted && createPortal( { setIsNotificationPanelOpen(false); // 不需要在这里刷新,NotificationPanel 内部会触发事件 }} />, document.body )} {/* 使用 Portal 将收藏面板渲染到 document.body */} {isFavoritesPanelOpen && mounted && createPortal( setIsFavoritesPanelOpen(false)} />, document.body )} {/* 使用 Portal 将下载文件管理面板渲染到 document.body */} {isDownloadManagementOpen && mounted && createPortal( setIsDownloadManagementOpen(false)} />, document.body )} {/* 使用 Portal 将邮件设置面板渲染到 document.body */} {isEmailSettingsOpen && mounted && createPortal(emailSettingsPanel, document.body)} {/* 使用 Portal 将设备管理面板渲染到 document.body */} {isDeviceManagementOpen && mounted && createPortal(deviceManagementPanel, document.body)} {/* 使用 Portal 将生态应用面板渲染到 document.body */} {isEcoAppsOpen && mounted && createPortal(ecoAppsPanel, document.body)} {/* 使用 Portal 将举报信息面板渲染到 document.body */} {isReportOpen && mounted && createPortal(reportPanel, document.body)} {/* 确认对话框 */} {confirmDialog.isOpen && mounted && createPortal(
{/* 标题 */}

{confirmDialog.title}

{/* 内容 */}

{confirmDialog.message}

{/* 按钮 */}
, document.body )} ); };