import { initAudioListeners } from '../store/initAudioListeners'; import { lazy, Suspense, useEffect, useState } from 'react'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { invoke } from '@tauri-apps/api/core'; import { showToast } from '../utils/ui/toast'; import { WindowVisibilityProvider } from '../hooks/useWindowVisibility'; import { DragDropProvider } from '../contexts/DragDropContext'; import PasteClipboardHandler from '../components/PasteClipboardHandler'; import ExportPickerModal from '../components/ExportPickerModal'; import ZipDownloadOverlay from '../components/ZipDownloadOverlay'; import FpsOverlay from '../components/FpsOverlay'; import { useAuthStore } from '../store/authStore'; import { useLibraryIndexStore } from '../store/libraryIndexStore'; import { useGlobalShortcutsStore } from '../store/globalShortcutsStore'; import { initHotCachePrefetch } from '../hotCachePrefetch'; import { initLocalPlaybackInvalidation } from '../localPlaybackInvalidation'; import { initFavoritesOfflineSync } from '../utils/offline/favoritesOfflineSync'; import { initPinnedOfflineSync } from '../utils/offline/pinnedOfflineSync'; import { initResumeIncompleteOfflinePins, scheduleResumeIncompleteOfflinePins } from '../utils/offline/resumeIncompleteOfflinePins'; import { runLegacyOfflineFileMigration } from '../utils/migrations/legacyOfflineFileMigration'; import { reconcileLibraryTierForServer } from '../utils/offline/libraryTierReconcile'; import { initMiniPlayerBridgeOnMain } from '../utils/miniPlayerBridge'; import { runAdvancedModeMigration } from '../utils/migrations/advancedModeMigration'; import { bootstrapAllIndexedServers } from '../utils/library/librarySession'; import { hydrateQueueFromIndex } from '../utils/library/queueRestore'; import { useLibraryAnalysisBackfill } from '../hooks/useLibraryAnalysisBackfill'; import { useCoverArtPrefetch } from '../cover/useCoverArtPrefetch'; import { useLibraryCoverBackfill } from '../hooks/useLibraryCoverBackfill'; import { useCoverRevalidateScheduler } from '../cover/useCoverRevalidateScheduler'; import { runCoverIdbUpgradeMigration } from '../utils/migrations/coverIdbUpgradeMigration'; import { useMigrationOrchestrator } from '../hooks/useMigrationOrchestrator'; import { IS_WINDOWS } from '../utils/platform'; import TauriEventBridge from './TauriEventBridge'; import AppShell from './AppShell'; import BlockingMigrationGate from './BlockingMigrationGate'; import RequireAuth from './RequireAuth'; import { useMigrationStore } from '../store/migrationStore'; const Login = lazy(() => import('../pages/Login')); /** * Main webview tree. Hosts the router, the application shell (sidebar / * player bar / queue panel / main scroll viewport), the Tauri event bridge, * and all background lifecycle hooks (audio listeners, hot-cache prefetch, * global shortcuts, mini-player bridge, easter egg, scrollbar auto-hide). */ export default function MainApp() { const [exportPickerOpen, setExportPickerOpen] = useState(false); // One-time bridge from the per-tab Advanced group (v1.46) to the global // Advanced Mode toggle. Idempotent — flagged in localStorage. useEffect(() => { runAdvancedModeMigration(); }, []); // Re-bind the library sync session whenever the active server changes // (covers app startup + server switch). The session is Rust // process-memory only while the per-server index toggle persists, so // without this the background scheduler + Sync now report // "no bound session" after a restart. const activeServerId = useAuthStore(s => s.activeServerId); const serverIdsKey = useAuthStore(s => s.servers.map(srv => srv.id).join(',')); const masterEnabled = useLibraryIndexStore(s => s.masterEnabled); const migrationPhase = useMigrationStore(s => s.phase); const migrationReady = migrationPhase === 'completed'; useMigrationOrchestrator(); useEffect(() => { if (!migrationReady) return; void (async () => { await bootstrapAllIndexedServers(); void hydrateQueueFromIndex(); })(); }, [activeServerId, serverIdsKey, masterEnabled, migrationReady]); useLibraryAnalysisBackfill(migrationReady); useCoverArtPrefetch(migrationReady); useLibraryCoverBackfill(migrationReady); useCoverRevalidateScheduler(migrationReady); useEffect(() => { if (!migrationReady) return; void runCoverIdbUpgradeMigration(); }, [migrationReady]); // Push playback state to mini window + handle control events. useEffect(() => { return initMiniPlayerBridgeOnMain(); }, []); // Optionally pre-create the mini player webview hidden so the first open // is instant. Windows already does this unconditionally in Rust .setup() as // a hang workaround — skip here to avoid double-building. const preloadMiniPlayer = useAuthStore(s => s.preloadMiniPlayer); useEffect(() => { if (!migrationReady || IS_WINDOWS || !preloadMiniPlayer) return; invoke('preload_mini_player').catch(() => {}); }, [preloadMiniPlayer, migrationReady]); useEffect(() => { if (!migrationReady) return undefined; return initAudioListeners(); }, [migrationReady]); useEffect(() => { if (!migrationReady) return undefined; return initHotCachePrefetch(); }, [migrationReady]); useEffect(() => { if (!migrationReady) return undefined; void (async () => { await runLegacyOfflineFileMigration(); const servers = useAuthStore.getState().servers; for (const server of servers) { await reconcileLibraryTierForServer(server.id); } scheduleResumeIncompleteOfflinePins(); })(); const stopInvalidation = initLocalPlaybackInvalidation(); const stopFavoritesSync = initFavoritesOfflineSync(); const stopPinnedOfflineSync = initPinnedOfflineSync(); const stopOfflineResume = initResumeIncompleteOfflinePins(); return () => { stopInvalidation(); stopFavoritesSync(); stopPinnedOfflineSync(); stopOfflineResume(); }; }, [migrationReady, serverIdsKey]); useEffect(() => { if (!migrationReady) return; useGlobalShortcutsStore.getState().registerAll(); }, [migrationReady]); // ── Easter egg: Ctrl+Shift+Alt+N → export new albums image ── useEffect(() => { const onKey = (e: KeyboardEvent) => { if (!e.ctrlKey || !e.shiftKey || !e.altKey || e.code !== 'KeyN') return; e.preventDefault(); setExportPickerOpen(true); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); const handleExport = async (since: number) => { setExportPickerOpen(false); try { const { exportNewAlbumsImage } = await import('../utils/export/exportNewAlbums'); const result = await exportNewAlbumsImage(since); if (result) { const files = result.paths.length > 1 ? ` (${result.paths.length} Dateien)` : ''; showToast(`📸 ${result.count} Alben exportiert${files}`); } else { showToast('📭 Keine Alben in diesem Zeitraum gefunden'); } } catch (err) { showToast(`❌ Export fehlgeschlagen: ${String(err).slice(0, 80)}`); console.error('[easter egg] export failed:', err); } }; useEffect(() => { const timers = new Map>(); const onScroll = (e: Event) => { const el = e.target as HTMLElement; el.classList.add('is-scrolling'); const existing = timers.get(el); if (existing !== undefined) clearTimeout(existing); timers.set(el, setTimeout(() => { el.classList.remove('is-scrolling'); timers.delete(el); }, 800)); }; document.addEventListener('scroll', onScroll, true); return () => { document.removeEventListener('scroll', onScroll, true); timers.forEach(t => clearTimeout(t)); }; }, []); return ( } /> } /> {exportPickerOpen && setExportPickerOpen(false)} />} ); }