/* eslint-disable @typescript-eslint/no-explicit-any */ import type { Metadata, Viewport } from 'next'; import { Inter } from 'next/font/google'; import { cookies } from 'next/headers'; import './globals.css'; import { parseAuthInfo } from '@/lib/auth'; import { getConfig } from '@/lib/config'; import { getUserFeatureAccess } from '@/lib/permissions'; import { listEnabledSourceScripts } from '@/lib/source-script'; import { StartupCacheCleanup } from '../components/DanmakuCacheCleanup'; import { DownloadBubble } from '../components/DownloadBubble'; import { DownloadPanel } from '../components/DownloadPanel'; import { GlobalErrorIndicator } from '../components/GlobalErrorIndicator'; import RouteScrollReset from '../components/RouteScrollReset'; import { SiteProvider } from '../components/SiteProvider'; import { ThemeProvider } from '../components/ThemeProvider'; import { TokenRefreshManager } from '../components/TokenRefreshManager'; import TopProgressBar from '../components/TopProgressBar'; import ChatFloatingWindow from '../components/watch-room/ChatFloatingWindow'; import { WatchRoomProvider } from '../components/WatchRoomProvider'; import { DownloadProvider } from '../contexts/DownloadContext'; const inter = Inter({ subsets: ['latin'] }); export const dynamic = 'force-dynamic'; // 动态生成 metadata,支持配置更新后的标题变化 export async function generateMetadata(): Promise { const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; const config = await getConfig(); let siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTVPlus'; if (storageType !== 'localstorage') { siteName = config.SiteConfig.SiteName; } return { title: siteName, description: '影视聚合', manifest: '/manifest.json', }; } export const viewport: Viewport = { viewportFit: 'cover', }; export default async function RootLayout({ children, }: { children: React.ReactNode; }) { const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; let siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTVPlus'; let announcement = process.env.ANNOUNCEMENT || '本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。'; let doubanProxyType = process.env.NEXT_PUBLIC_DOUBAN_PROXY_TYPE || 'cmliussss-cdn-tencent'; let doubanProxy = process.env.NEXT_PUBLIC_DOUBAN_PROXY || ''; let doubanImageProxyType = process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY_TYPE || 'cmliussss-cdn-tencent'; let doubanImageProxy = process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY || ''; let disableYellowFilter = process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true'; let fluidSearch = process.env.NEXT_PUBLIC_FLUID_SEARCH !== 'false'; let enableComments = false; let danmakuAutoLoadDefault = true; let recommendationDataSource = 'Mixed'; let tmdbApiKey = ''; let bangumiDataSource = (process.env.NEXT_PUBLIC_BANGUMI_DATA_SOURCE as any) || 'direct'; let bangumiApiBaseUrl = process.env.NEXT_PUBLIC_BANGUMI_API_BASE_URL || process.env.BANGUMI_API_BASE_URL || 'https://api.bgm.tv'; let bangumiImageBaseUrl = process.env.NEXT_PUBLIC_BANGUMI_IMAGE_BASE_URL || process.env.BANGUMI_IMAGE_BASE_URL || ''; let openListEnabled = false; let embyEnabled = false; let xiaoyaEnabled = false; let loginBackgroundImage = ''; let registerBackgroundImage = ''; let homeBackgroundImage = ''; let progressThumbType = 'default'; let progressThumbPresetId = ''; let progressThumbCustomUrl = ''; let enableRegistration = false; let requireRegistrationInviteCode = false; let loginRequireTurnstile = false; let registrationRequireTurnstile = false; let turnstileSiteKey = ''; let enableOIDCLogin = false; let enableOIDCRegistration = false; let oidcButtonText = ''; let aiEnabled = false; let aiEnableHomepageEntry = false; let aiEnableVideoCardEntry = false; let aiEnablePlayPageEntry = false; let aiEnableComments = false; let aiDefaultMessageNoVideo = ''; let aiDefaultMessageWithVideo = ''; let enableMovieRequest = true; let liveEnabled = true; let webLiveEnabled = false; let customAdFilterVersion = 0; let musicFeatureEnabled = false; let suwayomiEnabled = false; let booksEnabled = process.env.OPDS_ENABLED === 'true' || process.env.LEGADO_ENABLED === 'true'; let musicProxyEnabled = true; let advancedRecommendationEnabled = false; let userFeatureAccess = storageType === 'localstorage' ? await getUserFeatureAccess(process.env.USERNAME || 'localstorage-owner') : await getUserFeatureAccess(null); let customCategories = [] as { name: string; type: 'movie' | 'tv'; query: string; }[]; if (storageType !== 'localstorage') { const cookieStore = await cookies(); const authInfo = parseAuthInfo(cookieStore.get('auth')?.value); userFeatureAccess = await getUserFeatureAccess(authInfo?.username); const config = await getConfig(); siteName = config.SiteConfig.SiteName; announcement = config.SiteConfig.Announcement; doubanProxyType = config.SiteConfig.DoubanProxyType; doubanProxy = config.SiteConfig.DoubanProxy; doubanImageProxyType = config.SiteConfig.DoubanImageProxyType; doubanImageProxy = config.SiteConfig.DoubanImageProxy; disableYellowFilter = config.SiteConfig.DisableYellowFilter; customCategories = config.CustomCategories.filter( (category) => !category.disabled ).map((category) => ({ name: category.name || '', type: category.type, query: category.query, })); fluidSearch = config.SiteConfig.FluidSearch; enableComments = config.SiteConfig.EnableComments; danmakuAutoLoadDefault = config.SiteConfig.DanmakuAutoLoadDefault !== false; recommendationDataSource = config.SiteConfig.RecommendationDataSource || 'Mixed'; tmdbApiKey = config.SiteConfig.TMDBApiKey || ''; bangumiDataSource = config.SiteConfig.BangumiDataSource || 'direct'; bangumiApiBaseUrl = config.SiteConfig.BangumiApiBaseUrl || 'https://api.bgm.tv'; bangumiImageBaseUrl = config.SiteConfig.BangumiImageBaseUrl || ''; loginBackgroundImage = config.ThemeConfig?.loginBackgroundImage || ''; registerBackgroundImage = config.ThemeConfig?.registerBackgroundImage || ''; homeBackgroundImage = config.ThemeConfig?.homeBackgroundImage || ''; progressThumbType = config.ThemeConfig?.progressThumbType || 'default'; progressThumbPresetId = config.ThemeConfig?.progressThumbPresetId || ''; progressThumbCustomUrl = config.ThemeConfig?.progressThumbCustomUrl || ''; enableRegistration = config.SiteConfig.EnableRegistration || false; requireRegistrationInviteCode = config.SiteConfig.RequireRegistrationInviteCode || false; loginRequireTurnstile = config.SiteConfig.LoginRequireTurnstile || false; registrationRequireTurnstile = config.SiteConfig.RegistrationRequireTurnstile || false; turnstileSiteKey = config.SiteConfig.TurnstileSiteKey || ''; enableOIDCLogin = config.SiteConfig.EnableOIDCLogin || false; enableOIDCRegistration = config.SiteConfig.EnableOIDCRegistration || false; oidcButtonText = config.SiteConfig.OIDCButtonText || ''; // AI配置 aiEnabled = config.AIConfig?.Enabled || false; aiEnableHomepageEntry = config.AIConfig?.EnableHomepageEntry || false; aiEnableVideoCardEntry = config.AIConfig?.EnableVideoCardEntry || false; aiEnablePlayPageEntry = config.AIConfig?.EnablePlayPageEntry || false; aiEnableComments = config.AIConfig?.EnableAIComments || false; aiDefaultMessageNoVideo = config.AIConfig?.DefaultMessageNoVideo || ''; aiDefaultMessageWithVideo = config.AIConfig?.DefaultMessageWithVideo || ''; // 求片功能配置 enableMovieRequest = config.SiteConfig.EnableMovieRequest ?? true; // 网络直播功能配置 liveEnabled = (config.LiveConfig || []).some((source) => !source.disabled); webLiveEnabled = config.WebLiveEnabled ?? false; // 自定义去广告代码版本号 customAdFilterVersion = config.SiteConfig?.CustomAdFilterVersion || 0; // 音乐功能配置 musicFeatureEnabled = config.MusicConfig?.Enabled || false; musicProxyEnabled = config.MusicConfig?.ProxyEnabled ?? true; // 漫画功能配置 suwayomiEnabled = !!( config.SuwayomiConfig?.Enabled && config.SuwayomiConfig?.ServerURL ); // 电子书功能配置 const opdsConfig = config.OPDSConfig; // 电子书馆同时支持 OPDS 与 Legado。Legado 源通过订阅单独配置, // 不一定会出现在 OPDS Sources 中;入口应由“启用电子书馆”开关控制。 booksEnabled = !!opdsConfig?.Enabled; // 高级推荐功能配置:存在已启用视频源脚本时显示 advancedRecommendationEnabled = (await listEnabledSourceScripts()).length > 0; // 检查是否启用了 OpenList 功能 openListEnabled = !!( config.OpenListConfig?.Enabled && config.OpenListConfig?.URL && config.OpenListConfig?.Username && config.OpenListConfig?.Password ); // 检查是否启用了 Emby 功能(支持多源) embyEnabled = !!( config.EmbyConfig?.Sources && config.EmbyConfig.Sources.length > 0 && config.EmbyConfig.Sources.some((s) => s.enabled && s.ServerURL) ); // 检查是否启用了小雅功能 xiaoyaEnabled = !!( config.XiaoyaConfig?.Enabled && config.XiaoyaConfig?.ServerURL ); } // 将运行时配置注入到全局 window 对象,供客户端在运行时读取 const runtimeStorageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; const isCloudflare = process.env.CF_PAGES === '1' || process.env.BUILD_TARGET === 'cloudflare'; const displayStorageType = runtimeStorageType === 'd1' && !isCloudflare ? 'sqlite' : runtimeStorageType; const runtimeConfig = { STORAGE_TYPE: runtimeStorageType, DISPLAY_STORAGE_TYPE: displayStorageType, DOUBAN_PROXY_TYPE: doubanProxyType, DOUBAN_PROXY: doubanProxy, DOUBAN_IMAGE_PROXY_TYPE: doubanImageProxyType, DOUBAN_IMAGE_PROXY: doubanImageProxy, DISABLE_YELLOW_FILTER: disableYellowFilter, CUSTOM_CATEGORIES: customCategories, FLUID_SEARCH: fluidSearch, EnableComments: enableComments, DANMAKU_AUTO_LOAD_DEFAULT: danmakuAutoLoadDefault, RecommendationDataSource: recommendationDataSource, BANGUMI_DATA_SOURCE: bangumiDataSource, BANGUMI_API_BASE_URL: bangumiApiBaseUrl, BANGUMI_IMAGE_BASE_URL: bangumiImageBaseUrl, ENABLE_TV_MODE: process.env.ENABLE_TV_MODE !== 'false', ENABLE_TVBOX_SUBSCRIBE: process.env.ENABLE_TVBOX_SUBSCRIBE === 'true', ENABLE_OFFLINE_DOWNLOAD: process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true', VOICE_CHAT_STRATEGY: process.env.NEXT_PUBLIC_VOICE_CHAT_STRATEGY || 'webrtc-fallback', OPENLIST_ENABLED: openListEnabled && userFeatureAccess.private_library, EMBY_ENABLED: embyEnabled && userFeatureAccess.emby, XIAOYA_ENABLED: xiaoyaEnabled && userFeatureAccess.xiaoya, PRIVATE_LIBRARY_ENABLED: (openListEnabled && userFeatureAccess.private_library) || (embyEnabled && userFeatureAccess.emby) || (xiaoyaEnabled && userFeatureAccess.xiaoya), LOGIN_BACKGROUND_IMAGE: loginBackgroundImage, REGISTER_BACKGROUND_IMAGE: registerBackgroundImage, HOME_BACKGROUND_IMAGE: homeBackgroundImage, PROGRESS_THUMB_TYPE: progressThumbType, PROGRESS_THUMB_PRESET_ID: progressThumbPresetId, PROGRESS_THUMB_CUSTOM_URL: progressThumbCustomUrl, ENABLE_REGISTRATION: enableRegistration, REQUIRE_REGISTRATION_INVITE_CODE: requireRegistrationInviteCode, LOGIN_REQUIRE_TURNSTILE: loginRequireTurnstile, REGISTRATION_REQUIRE_TURNSTILE: registrationRequireTurnstile, TURNSTILE_SITE_KEY: turnstileSiteKey, ENABLE_OIDC_LOGIN: enableOIDCLogin, ENABLE_OIDC_REGISTRATION: enableOIDCRegistration, OIDC_BUTTON_TEXT: oidcButtonText, AI_ENABLED: aiEnabled && userFeatureAccess.ai_ask, AI_ENABLE_HOMEPAGE_ENTRY: aiEnableHomepageEntry, AI_ENABLE_VIDEOCARD_ENTRY: aiEnableVideoCardEntry, AI_ENABLE_PLAYPAGE_ENTRY: aiEnablePlayPageEntry, AIConfig: { EnableAIComments: aiEnableComments, }, AI_DEFAULT_MESSAGE_NO_VIDEO: aiDefaultMessageNoVideo, AI_DEFAULT_MESSAGE_WITH_VIDEO: aiDefaultMessageWithVideo, ENABLE_MOVIE_REQUEST: enableMovieRequest, LIVE_ENABLED: liveEnabled && userFeatureAccess.live, WEB_LIVE_ENABLED: webLiveEnabled && userFeatureAccess.web_live, ADVANCED_RECOMMENDATION_ENABLED: advancedRecommendationEnabled, CUSTOM_AD_FILTER_VERSION: customAdFilterVersion, MUSIC_ENABLED: musicFeatureEnabled && userFeatureAccess.music, MUSIC_PROXY_ENABLED: musicProxyEnabled, SUWAYOMI_ENABLED: suwayomiEnabled && userFeatureAccess.manga, BOOKS_ENABLED: booksEnabled && userFeatureAccess.books, NETDISK_SEARCH_ENABLED: userFeatureAccess.netdisk_search, MAGNET_SEARCH_ENABLED: userFeatureAccess.magnet_search, MAGNET_SAVE_PRIVATE_LIBRARY_ENABLED: userFeatureAccess.magnet_save_private_library, NETDISK_TRANSFER_ENABLED: userFeatureAccess.netdisk_transfer, NETDISK_TEMP_PLAY_ENABLED: userFeatureAccess.netdisk_temp_play, FESTIVE_EFFECT_ENABLED: process.env.FESTIVE_EFFECT_ENABLED === 'true', }; return ( {/* 主题CSS */} {/* 将配置序列化后直接写入脚本,浏览器端可通过 window.RUNTIME_CONFIG 获取 */} {/* eslint-disable-next-line @next/next/no-sync-scripts */}