Files
MoonTVPlus/src/app/layout.tsx
T

356 lines
15 KiB
TypeScript

/* 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<Metadata> {
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 (
<html lang='zh-CN' suppressHydrationWarning>
<head>
<meta
name='viewport'
content='width=device-width, initial-scale=1.0, viewport-fit=cover'
/>
<link rel='apple-touch-icon' href='/icons/icon-192x192.png' />
{/* 主题CSS */}
<link rel='stylesheet' href='/api/theme/css' />
{/* 将配置序列化后直接写入脚本,浏览器端可通过 window.RUNTIME_CONFIG 获取 */}
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
<script
dangerouslySetInnerHTML={{
__html: `window.RUNTIME_CONFIG = ${JSON.stringify(runtimeConfig)};`,
}}
/>
</head>
<body
className={`${inter.className} min-h-screen bg-white text-gray-900 dark:bg-black dark:text-gray-200`}
>
<ThemeProvider
attribute='class'
defaultTheme='system'
enableSystem
disableTransitionOnChange
>
<TopProgressBar />
<RouteScrollReset />
<TokenRefreshManager />
<SiteProvider
siteName={siteName}
announcement={announcement}
tmdbApiKey={tmdbApiKey}
>
<WatchRoomProvider>
<DownloadProvider>
<StartupCacheCleanup />
{children}
<GlobalErrorIndicator />
<ChatFloatingWindow />
<DownloadBubble />
<DownloadPanel />
</DownloadProvider>
</WatchRoomProvider>
</SiteProvider>
</ThemeProvider>
</body>
</html>
);
}