import React, { useState, useEffect, useMemo, useRef } from 'react'; import Clock from './components/Clock'; import SearchBox from './components/SearchBox'; import SettingsModal from './components/SettingsModal'; import ErrorBoundary from './components/ErrorBoundary'; import GlobalContextMenu from './components/GlobalContextMenu'; import { SettingsIcon } from './components/Icons'; import { UserSettings, WallpaperFit } from './types'; import { PRESET_WALLPAPERS, SEARCH_ENGINES, THEMES } from './constants'; import { loadSettings, saveSettings } from './utils/storage'; import { I18nProvider } from './i18n'; // Default settings - moved outside component to avoid recreation on each render const DEFAULT_SETTINGS: UserSettings = { use24HourFormat: true, showSeconds: true, backgroundBlur: 8, searchEngines: [...SEARCH_ENGINES], selectedEngine: SEARCH_ENGINES[0].name, themeColor: THEMES[0].hex, searchOpacity: 0.8, enableMaskBlur: false, backgroundUrl: PRESET_WALLPAPERS[0].url, backgroundType: PRESET_WALLPAPERS[0].type, wallpaperFit: 'cover', customWallpapers: [], enableSearchHistory: true, searchHistory: [], language: 'en' }; type ViewMode = 'search' | 'dashboard'; const App: React.FC = () => { // State for settings visibility const [isSettingsOpen, setIsSettingsOpen] = useState(false); // State for view mode (Search Panel vs Dashboard Panel) const [viewMode, setViewMode] = useState('search'); // State for wallpaper loaded (prevent flash) const [bgLoaded, setBgLoaded] = useState(false); // State for search box interaction (controls background blur) const [isSearchActive, setIsSearchActive] = useState(false); // Application Settings - loaded from Local Storage const [settings, setSettings] = useState(() => loadSettings(DEFAULT_SETTINGS)); // Flag to track if this is the initial mount const isInitialMount = useRef(true); // Save settings to Local Storage (skip initial mount) useEffect(() => { if (isInitialMount.current) { isInitialMount.current = false; return; } saveSettings(settings); }, [settings]); // Preload background when URL changes useEffect(() => { setBgLoaded(false); let isMounted = true; if (settings.backgroundType === 'image') { const img = new Image(); img.src = settings.backgroundUrl; img.onload = () => { if (isMounted) { setBgLoaded(true); } }; // Handle error case to avoid stuck loading state img.onerror = () => { if (isMounted) { setBgLoaded(true); } }; // Cleanup function return () => { isMounted = false; // Clean up Image object event handlers img.onload = null; img.onerror = null; // Cancel image loading img.src = ''; }; } else { // For video, we can consider it "loaded" once it starts playing or immediately // depending on desired UX. Here we'll set it true immediately to show the video element // which handles its own buffering. setBgLoaded(true); } }, [settings.backgroundUrl, settings.backgroundType]); const handleSelectEngine = (name: string) => { setSettings(prev => ({ ...prev, selectedEngine: name })); }; const handleUpdateHistory = (newHistory: string[]) => { setSettings(prev => ({ ...prev, searchHistory: newHistory })); }; const getBackgroundStyle = (fit: WallpaperFit): React.CSSProperties => { const baseStyle = { backgroundImage: `url(${settings.backgroundUrl})` }; switch (fit) { case 'contain': return { ...baseStyle, backgroundSize: 'contain', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }; case 'fill': return { ...baseStyle, backgroundSize: '100% 100%', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }; case 'repeat': return { ...baseStyle, backgroundSize: 'auto', backgroundPosition: 'top left', backgroundRepeat: 'repeat' }; case 'center': return { ...baseStyle, backgroundSize: 'auto', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }; case 'cover': default: return { ...baseStyle, backgroundSize: 'cover', backgroundPosition: 'center', backgroundRepeat: 'no-repeat' }; } }; const getVideoClass = (fit: WallpaperFit) => { switch (fit) { case 'contain': return 'object-contain'; case 'fill': return 'object-fill'; case 'center': return 'object-none'; case 'repeat': return 'object-cover'; // Video tile not supported natively in same way, fallback to cover case 'cover': default: return 'object-cover'; } }; // Handle right-click on the background to switch to Dashboard const handleBackgroundContextMenu = (e: React.MouseEvent) => { // If settings modal is open, let standard behavior apply (or it's covered by modal backdrop) if (isSettingsOpen) return; // We only want to capture clicks on the "background" or general containers. // Specific interactive elements (like SearchBox) should stop propagation. e.preventDefault(); if (viewMode === 'search') { setViewMode('dashboard'); } }; // Handle left-click on the dashboard to return to Search const handleDashboardClick = (e: React.MouseEvent) => { if (viewMode === 'dashboard' && !isSettingsOpen) { setViewMode('search'); } }; const handleLanguageChange = (lang: 'en' | 'zh') => { setSettings(prev => ({ ...prev, language: lang })); }; return (
{/* Context Menu Global Listener */} {/* Background Layer */}
{settings.backgroundType === 'video' ? (