import { useEffect, useRef, useState } from 'react' import { Timeline } from './components/timeline/Timeline' import { DuplicatesView } from './components/duplicates/DuplicatesView' import { MapView } from './components/map/MapView' import { MemoriesView } from './components/memories/MemoriesView' import { TagsView } from './components/tags/TagsView' import { ColorsView } from './components/colors/ColorsView' import { RatedView } from './components/rated/RatedView' import { LeftSidebar } from './components/layout/LeftSidebar' import { RightSidebar } from './components/layout/RightSidebar' import { TopBar } from './components/layout/TopBar' import { ScanProgress } from './components/ScanProgress' import { ToastContainer } from './components/ToastContainer' import { KeyboardHints } from './components/KeyboardHints' import { PreviewView } from './components/preview/PreviewView' import { FilterBar } from './components/filter/FilterBar' import { DiscardActionBar } from './components/discard/DiscardActionBar' import { SettingsPage } from './components/dialogs/SettingsDialog' import { usePhotoStore } from './store/photoStore' import { useFilterStore } from './store/filterStore' import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts' import { useFilterUrlSync } from './hooks/useFilterUrlSync' import { usePhotosQuery } from './hooks/usePhotosQuery' import { AuthProvider, useAuth } from './contexts/AuthContext' import { LoginPage } from './components/auth/LoginPage' import { SetupPage } from './components/auth/SetupPage' import { OidcCallback } from './components/auth/OidcCallback' import { TooltipProvider } from '@/components/ui/tooltip' function MainApp() { const [leftSidebarOpen, setLeftSidebarOpen] = useState(true) const [rightSidebarOpen, setRightSidebarOpen] = useState(true) // Respect the user's manual collapse of the metadata panel. Once they // close it explicitly (via `i` hotkey or the sidebar toggle button), // selecting a new photo should NOT force it back open. Cleared when // they open it manually again. const rightCollapsedByUser = useRef(false) const viewMode = usePhotoStore((state) => state.viewMode) const activePhotoId = usePhotoStore((state) => state.activePhotoId) const currentSection = useFilterStore((s) => s.currentSection) // Close the metadata panel when the user switches between sections so // it doesn't carry over a now-irrelevant selection. It re-opens once a // photo gains focus in the new section (effect below). const prevSectionRef = useRef(currentSection) useEffect(() => { if (prevSectionRef.current !== currentSection) { prevSectionRef.current = currentSection setRightSidebarOpen(false) } }, [currentSection]) useEffect(() => { if (!activePhotoId) { setRightSidebarOpen(false) return } // User explicitly collapsed the panel — don't undo that just because // they picked a different photo. if (rightCollapsedByUser.current) return setRightSidebarOpen(true) }, [activePhotoId]) const toggleRightSidebar = () => { setRightSidebarOpen((prev) => { const next = !prev rightCollapsedByUser.current = !next return next }) } // Bidirectional sync of filter store with URL query params. useFilterUrlSync() // Subscribe to the same photos query the Timeline uses, so the keyboard // "open preview on first photo" path can read from the live cache regardless // of what filter key it's stored under. const { data: allPhotos } = usePhotosQuery() // Set up global keyboard shortcuts. Prefer the Timeline's published // visible sequence (which respects per-month ordering) over the raw // backend list — otherwise "Space on a blank selection" would open // the globally first photo, which isn't what the user sees at the // top-left of the grid. useKeyboardShortcuts({ onToggleLeftSidebar: () => setLeftSidebarOpen(!leftSidebarOpen), onToggleRightSidebar: toggleRightSidebar, getFirstPhotoId: () => usePhotoStore.getState().visiblePhotoIds[0] ?? allPhotos?.[0]?.id ?? null, }) // Settings page is a full-page section — hide filter bar, right sidebar, // and keyboard hints when it's active. const isSettings = currentSection === 'settings' // Right sidebar stays open by default and shows whatever's selected // (or an empty state if nothing is). User can still toggle it manually. const showRightSidebar = rightSidebarOpen && !isSettings return (
{/* Left Sidebar */}
{/* Main column — filter bar, discard bar, timeline. Lives to the * right of the left sidebar so the filter row doesn't bleed * across the sidebar. relative so the KeyboardHints overlay * centers against this column, not the viewport. */}
{!isSettings && ( setLeftSidebarOpen(!leftSidebarOpen)} onToggleRightSidebar={toggleRightSidebar} /> )} {!isSettings && }
{currentSection === 'settings' ? ( ) : currentSection === 'map' ? ( ) : currentSection === 'memories' ? ( ) : currentSection === 'duplicates' ? ( ) : currentSection === 'tags' ? ( ) : currentSection === 'colors' ? ( ) : currentSection === 'rated' ? ( ) : ( )}
{!isSettings && viewMode !== 'preview' && }
{/* Right Sidebar */}
{/* Scan Progress Indicator */} {/* Toast Notifications */} {/* Preview overlay — covers TopBar when active */} {viewMode === 'preview' && }
) } /** Auth-gated shell: shows setup, login, or the main app. */ function App() { return ( ) } function AuthGate() { const { user, isLoading, needsSetup } = useAuth() // OIDC callback lands on /auth/callback — handle it even while // isLoading, so the callback page can adopt tokens and transition // straight to MainApp without flashing the login screen. if (window.location.pathname.startsWith('/auth/callback')) { return } if (isLoading) { return (
Loading…
) } if (needsSetup) return if (!user) return return } export default App