import { useCallback, useEffect, useState } from 'react' import { filesApi } from '../api/client' import type { FolderItem, User } from '../api/client' import { useNavHistory, type View } from '../hooks/useNavHistory' import { useEdgeSwipe } from '../hooks/useEdgeSwipe' import { useBackClose } from '../hooks/useBackClose' import { Toaster } from './Toasts' import Sidebar from './Sidebar' import FileBrowser from '../pages/FileBrowser' import TrashPage from '../pages/TrashPage' import SharedPage from '../pages/SharedPage' import SharedWithMePage from '../pages/SharedWithMePage' import AdminPage from '../pages/AdminPage' import ProfilePage from '../pages/ProfilePage' import TagsPage from '../pages/TagsPage' import CalendarPage from '../pages/CalendarPage' import { UserMenuProvider } from './UserMenu' import { wideEnough } from '../breakpoints' export type { View } interface Props { user: User onLogout: () => Promise /** Lädt den Nutzer neu – für Speicherverbrauch und Anzeigename. */ onUserChanged: () => Promise } export default function Shell({ user, onLogout, onUserChanged }: Props) { const nav = useNavHistory() const { view, folderPath } = nav const [sidebarOpen, setSidebarOpen] = useState(false) // Am Finger: vom linken Rand nach rechts wischen holt die Leiste, nach // links wischen schickt sie zurück. Mit Maus passiert nichts. useEdgeSwipe({ open: sidebarOpen, onOpen: useCallback(() => setSidebarOpen(true), []), onClose: useCallback(() => setSidebarOpen(false), []), }) // Ist die Leiste am Telefon aufgeschoben, holt die Zurück-Geste sie // zurück – sie ist dann das Oberste auf dem Schirm, und genau darauf zielt // die Bewegung. useBackClose({ active: sidebarOpen, onClose: useCallback(() => { setSidebarOpen(false) }, []), }) // Wird das Gerät aufgeklappt, während die Leiste aufgeschoben war, steht sie // ab da ohnehin fest im Bild. Bliebe „aufgeschoben" dabei stehen, schluckte // die Zurück-Geste einmal ins Leere – sie zielte auf etwas, das nichts mehr // verdeckt. useEffect(() => { if (typeof matchMedia !== 'function') return const mq = matchMedia(wideEnough) const auf = () => { if (mq.matches) setSidebarOpen(false) } auf() mq.addEventListener('change', auf) return () => mq.removeEventListener('change', auf) }, []) // Hochgezählt, wenn etwas auf den Papierkorb gezogen wurde – die Dateiliste // hängt daran und holt sich daraufhin neu. const [trashBump, setTrashBump] = useState(0) /** * Auf den Papierkorb gezogene Dateien. * * Bewusst **ohne** Rückfrage: Der Weg dorthin ist eine bewusste Bewegung mit * gedrückter Maustaste, kein Klick, der danebengehen kann – und der * Papierkorb ist umkehrbar. Ein Dialog nach jedem Zug wäre die Art * Sicherheitsabfrage, die man nach dem dritten Mal blind wegklickt. */ async function trashDrop(fileIds: string[]) { await filesApi.deleteMany(fileIds) setTrashBump(n => n + 1) await onUserChanged() } // Aus der Seitenleiste kennen wir nur Kennung und Name – die Kette darueber // holt die Liste nicht mit, also faengt der Pfad hier beim Ordner selbst an. function navigateToFolder(folderId: string, folderName: string) { setSidebarOpen(false) const stub: FolderItem = { id: folderId, owner_id: null, group_id: null, parent_id: null, name: folderName, created_at: '', } nav.setFolderPath([stub]) } return ( { nav.setView(v); setSidebarOpen(false) }, onLogout, onOpenSidebar: () => setSidebarOpen(true), }}>
{/* Die Meldungen liegen über allem und sind an genau einer Stelle eingehängt – absetzen darf sie jedes Modul, anzeigen tut sie nur diese eine Anzeige. */} {/* Der abdunkelnde Grund hinter der aufgeschobenen Leiste. Die Klasse blendet ihn aus, sobald die Leiste fest im Bild steht: Dort liegt sie im Fluss und **ohne** z-index, ein Grund darüber machte sie unanklickbar – sichtbar, aber taub. */} {sidebarOpen && (
setSidebarOpen(false)} /> )} { nav.setView(v); setSidebarOpen(false) }} onNavigateToFolder={navigateToFolder} onTrashDrop={trashDrop} open={sidebarOpen} /> {/* Main content */}
{view === 'files' && ( )} {view === 'calendar' && } {view === 'trash' && } {view === 'incoming' && } {view === 'shared' && } {view === 'admin' && } {view === 'profile' && } {/* Etikett und Absender teilen sich die Seite – es ist dieselbe Frage („alles mit diesem Merkmal"), nur ein anderes Merkmal. */} {(view === 'tags' || view === 'from') && ( { // Klick auf einen Treffer springt zum Ordner – dieselbe Regel wie // in der Suche: Geöffnet wird per Doppelklick auf die Datei. if (id) navigateToFolder(id, name) else nav.setFolderPath([]) }} /> )}
) }