import React, { Suspense, useEffect, useMemo, useState } from 'react'; import { useAtomValue } from 'jotai'; import { settingsAtom } from '../../state/settings'; import { useReducedMotion } from '../../hooks/useReducedMotion'; import { zIndices } from '../../styles/zIndex'; import { SeasonTheme, SeasonalOverlayProps } from './types'; import { resolveSeasonTheme } from './seasonSchedule'; // [Gitea #167] Each overlay (its particles + keyframes) is its own chunk, // fetched only when that season is active or previewed in Settings — the // date/override decision stays in the main bundle. const lazyOverlay = (load: () => Promise<{ default: React.ComponentType }>) => React.lazy(load); const HalloweenOverlay = lazyOverlay(() => import('./themes/Halloween').then((m) => ({ default: m.HalloweenOverlay })), ); const ChristmasOverlay = lazyOverlay(() => import('./themes/Christmas').then((m) => ({ default: m.ChristmasOverlay })), ); const NewYearOverlay = lazyOverlay(() => import('./themes/NewYear').then((m) => ({ default: m.NewYearOverlay })), ); const AutumnOverlay = lazyOverlay(() => import('./themes/Autumn').then((m) => ({ default: m.AutumnOverlay })), ); const AprilFoolsOverlay = lazyOverlay(() => import('./themes/AprilFools').then((m) => ({ default: m.AprilFoolsOverlay })), ); const LunarNewYearOverlay = lazyOverlay(() => import('./themes/LunarNewYear').then((m) => ({ default: m.LunarNewYearOverlay })), ); const ValentinesOverlay = lazyOverlay(() => import('./themes/Valentines').then((m) => ({ default: m.ValentinesOverlay })), ); const StPatricksOverlay = lazyOverlay(() => import('./themes/StPatricks').then((m) => ({ default: m.StPatricksOverlay })), ); const EarthDayOverlay = lazyOverlay(() => import('./themes/EarthDay').then((m) => ({ default: m.EarthDayOverlay })), ); const DeepSpaceOverlay = lazyOverlay(() => import('./themes/DeepSpace').then((m) => ({ default: m.DeepSpaceOverlay })), ); const ArcadeOverlay = lazyOverlay(() => import('./themes/Arcade').then((m) => ({ default: m.ArcadeOverlay })), ); // SeasonTheme + the date-window logic now live in leaf modules (single source // of truth, shared with the settings UI). Re-exported here for existing // importers that still reach for it from this file. export type { SeasonTheme }; // ─── Overlay content map (shared between SeasonalOverlay and SeasonalPreview) ── function buildOverlayContent( theme: SeasonTheme, reduced: boolean, preview = false, ): React.ReactNode { switch (theme) { case 'halloween': return ; case 'christmas': return ; case 'newyear': return ; case 'autumn': return ; case 'aprilfools': return ; case 'lunar': return ; case 'valentines': return ; case 'stpatricks': return ; case 'earthday': return ; case 'deepspace': return ; case 'arcade': return ; default: return null; } } // ─── Full-screen overlay (fixed position, used in App) ──────────────────────── function SeasonalOverlay({ theme, reduced }: { theme: SeasonTheme; reduced: boolean }) { return ( ); } // ─── Preview overlay (absolute position, contained in a card) ───────────────── /** * Renders the ambient (reduced-motion) version of a seasonal overlay inside * a parent container. The parent must have `position: relative; overflow: hidden`. */ export function SeasonalPreview({ theme }: { theme: SeasonTheme }) { return ( ); } // ─── Main exported component ────────────────────────────────────────────────── export function SeasonalEffect() { const settings = useAtomValue(settingsAtom); const reduced = useReducedMotion(); const override = settings.seasonalThemeOverride ?? 'auto'; // In auto mode, re-evaluate hourly so a long-lived session crosses a // season/holiday-window boundary (e.g. into a new day) without a reload — // otherwise the active season is frozen at the value it had on mount. const [now, setNow] = useState(() => Date.now()); useEffect(() => { if (override !== 'auto') return undefined; // Refresh on entering auto too: `now` may be a stale mount-time value if we // were previously in a pinned/off mode (the interval only runs while auto). setNow(Date.now()); const id = window.setInterval(() => setNow(Date.now()), 60 * 60 * 1000); return () => window.clearInterval(id); }, [override]); const theme = useMemo( () => resolveSeasonTheme(override, now), [override, now], ); if (!theme) return null; // Suppress seasonal overlay when a chat background is active — both running simultaneously // wastes GPU and looks cluttered. The settings UI enforces mutual exclusion on write; // this guard covers any legacy state already persisted. if (settings.chatBackground !== 'none') return null; return ; }