Files
mule-image/frontend/src/App.tsx
dtoro 9ed577f40c feat: settings panel + thumbnail pipeline fixes
- photos.py: stop crashing in FileResponse when a thumb hasn't been
  generated; return a clean 404 with Retry-After so the frontend can
  back off.
- thumbs.py: fix process_video_thumbnail (overwrite_output, robust
  duration probe across stream/format, eager frame load + temp cleanup)
  so videos stop ending up as the gray placeholder.
- library.py: new /maintenance/* endpoints — thumbnail-stats,
  regenerate-thumbnails (with media_type / only_failed filters), and a
  manual data-integrity cleanup trigger.
- Frontend Settings panel (gear in TopBar) surfacing those endpoints
  plus a re-scan button and live thumbnail status counts.
- PhotoThumbnail: stretch the auto-retry schedule for slow RAW jobs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:24:08 +02:00

115 lines
4.4 KiB
TypeScript

import { useEffect, useState } from 'react'
import { Timeline } from './components/timeline/Timeline'
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 { AppFooter } from './components/AppFooter'
import { PreviewView } from './components/preview/PreviewView'
import { FilterBar } from './components/filter/FilterBar'
import { DiscardActionBar } from './components/discard/DiscardActionBar'
import { SettingsDialog } from './components/dialogs/SettingsDialog'
import { usePhotoStore } from './store/photoStore'
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
import { useFilterUrlSync } from './hooks/useFilterUrlSync'
import { usePhotosQuery } from './hooks/usePhotosQuery'
function App() {
const [leftSidebarOpen, setLeftSidebarOpen] = useState(true)
const [rightSidebarOpen, setRightSidebarOpen] = useState(false)
const [settingsOpen, setSettingsOpen] = useState(false)
const selectedPhotos = usePhotoStore((state) => state.selectedPhotos)
const viewMode = usePhotoStore((state) => state.viewMode)
// 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
useKeyboardShortcuts({
onToggleLeftSidebar: () => setLeftSidebarOpen(!leftSidebarOpen),
onToggleRightSidebar: () => setRightSidebarOpen(!rightSidebarOpen),
getFirstPhotoId: () => allPhotos?.[0]?.id ?? null,
})
// Auto-show right sidebar when photos are selected — but only in grid mode,
// so leaving the preview doesn't fight the user's prior sidebar state.
// Lives in an effect (not the render body) to avoid setState-during-render
// and the cascading re-renders the audit caught.
useEffect(() => {
if (viewMode !== 'grid') return
if (selectedPhotos.length > 0 && !rightSidebarOpen) {
setRightSidebarOpen(true)
} else if (selectedPhotos.length === 0 && rightSidebarOpen) {
setRightSidebarOpen(false)
}
}, [viewMode, selectedPhotos.length, rightSidebarOpen])
const showRightSidebar = rightSidebarOpen && viewMode === 'grid'
return (
<div className="flex flex-col h-screen bg-bg text-text">
<TopBar onOpenSettings={() => setSettingsOpen(true)} />
<div className="flex flex-1 overflow-hidden">
{/* Left Sidebar */}
<div
className={`transition-all duration-200 ${
leftSidebarOpen ? 'w-64' : 'w-0'
} overflow-hidden border-r border-border bg-surface`}
>
<LeftSidebar />
</div>
{/* 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. */}
<div className="relative flex min-w-0 flex-1 flex-col">
<FilterBar />
<DiscardActionBar />
<div className="flex-1 overflow-auto">
<Timeline />
</div>
{/* Floating keyboard hints — bottom-center of the main column,
* glassy. Mounted here so it's centered against the timeline,
* not the viewport (which would be offset by the sidebars). */}
<KeyboardHints />
<AppFooter />
</div>
{/* Right Sidebar */}
<div
className={`transition-all duration-200 ${
showRightSidebar ? 'w-80' : 'w-0'
} overflow-hidden border-l border-border bg-surface`}
>
<RightSidebar />
</div>
</div>
{/* Scan Progress Indicator */}
<ScanProgress />
{/* Toast Notifications */}
<ToastContainer />
{/* Preview overlay — covers TopBar when active */}
{viewMode === 'preview' && <PreviewView />}
{/* Settings panel — admin/maintenance actions */}
<SettingsDialog
isOpen={settingsOpen}
onClose={() => setSettingsOpen(false)}
/>
</div>
)
}
export default App