import React from 'react'; import type { Metadata } from "next"; import "./globals.css"; import { ThemeProvider } from "@/components/ThemeProvider"; import { AutoSync } from '@/components/AutoSync'; // <-- 引入了自动同步组件 import { SiteIconProvider } from '@/components/SiteIconProvider'; import { TVProvider } from "@/lib/contexts/TVContext"; import { TVNavigationInitializer } from "@/components/TVNavigationInitializer"; import { Analytics } from "@vercel/analytics/react"; import { ServiceWorkerRegister } from "@/components/ServiceWorkerRegister"; import { PasswordGate } from "@/components/PasswordGate"; import { siteConfig } from "@/lib/config/site-config"; import { AdKeywordsInjector } from "@/components/AdKeywordsInjector"; import { BackToTop } from "@/components/ui/BackToTop"; import { ScrollPositionManager } from "@/components/ScrollPositionManager"; import { LocaleProvider } from "@/components/LocaleProvider"; import { RuntimeFeaturesProvider } from "@/components/RuntimeFeaturesProvider"; import { VideoTogetherController } from '@/components/VideoTogetherController'; import { getRuntimeFeatures } from "@/lib/server/runtime-features"; import { resolveSiteIconSrc } from '@/lib/server/site-icon'; import fs from 'fs'; import path from 'path'; const DEFAULT_VIDEOTOGETHER_SCRIPT_URL = 'https://fastly.jsdelivr.net/gh/VideoTogether/VideoTogether@latest/release/extension.website.user.js'; // Server Component specifically for reading env/file (async for best practices) async function AdKeywordsWrapper() { let keywords: string[] = []; try { // 1. Try reading from file (Docker runtime support) const keywordsFile = process.env.AD_KEYWORDS_FILE; if (keywordsFile) { // Resolve absolute path or relative to CWD const filePath = path.isAbsolute(keywordsFile) ? keywordsFile : path.join(process.cwd(), keywordsFile); try { const content = await fs.promises.readFile(filePath, 'utf-8'); keywords = content.split(/[\n,]/).map((k: string) => k.trim()).filter((k: string) => k); console.log(`[AdFilter] Loaded ${keywords.length} keywords from file: ${filePath}`); } catch (fileError: unknown) { // Handle file not found (ENOENT) gracefully if ((fileError as NodeJS.ErrnoException).code !== 'ENOENT') { console.warn('[AdFilter] Error reading keywords file:', fileError); } } } // 2. Fallback to Env var (Runtime or Build time) if (keywords.length === 0) { const envKeywords = process.env.AD_KEYWORDS || process.env.NEXT_PUBLIC_AD_KEYWORDS; if (envKeywords) { keywords = envKeywords.split(/[\n,]/).map((k: string) => k.trim()).filter((k: string) => k); } } } catch (error) { console.warn('[AdFilter] Failed to load keywords:', error); } return ; } export async function generateMetadata(): Promise { const siteIconSrc = await resolveSiteIconSrc(); return { title: siteConfig.title, description: siteConfig.description, icons: { icon: siteIconSrc, }, }; } export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { const siteIconSrc = await resolveSiteIconSrc(); const runtimeFeatures = getRuntimeFeatures(); const videoTogetherScriptUrl = process.env.VIDEOTOGETHER_SCRIPT_URL?.trim() || DEFAULT_VIDEOTOGETHER_SCRIPT_URL; const videoTogetherSettingUrl = process.env.VIDEOTOGETHER_SETTING_URL?.trim(); const videoTogetherEnvEnabled = process.env.VIDEOTOGETHER_ENABLED !== 'false'; return ( {/* PWA Manifest */} {/* Apple PWA Support */} {/* Theme Color (for browser address bar) */} {/* Mobile viewport */} {/* 加入自动同步组件,它会在后台默默工作,我们放在 ThemeProvider 内部的最前面 */} {children} {/* ARIA Live Region for Screen Reader Announcements */}
{/* Google Cast SDK */}