diff --git a/server/ui/src/App.tsx b/server/ui/src/App.tsx index 5005208..0fefa7e 100644 --- a/server/ui/src/App.tsx +++ b/server/ui/src/App.tsx @@ -6,12 +6,10 @@ import { History, Images, KeyRound, - LogOut, PieChart, Settings, - UserCog, } from 'lucide-react' -import { run, useStore } from './store' +import { useStore } from './store' import { Anmelden } from './pages/Anmelden' import { Start } from './pages/Start' import { Galerie } from './pages/Galerie' @@ -23,7 +21,7 @@ import { Verlauf } from './pages/Verlauf' import { Api } from './pages/Api' import { Meldungen } from './components/Meldungen' import { gehZu, usePfad } from './router' -import { Avatar } from './components/Avatar' +import { Kontomenue } from './components/Kontomenue' /** Die Breite des Inhalts — an EINER Stelle. * @@ -41,10 +39,14 @@ const seiten = [ { pfad: '/speicher', name: 'Speicher', kind: PieChart, Inhalt: Speicher }, { pfad: '/tokens', name: 'Token', kind: KeyRound, Inhalt: Tokens }, { pfad: '/api', name: 'API', kind: Code2, Inhalt: Api }, - { pfad: '/konto', name: 'Konto', kind: UserCog, Inhalt: Konto }, { pfad: '/einstellungen', name: 'Einstellungen', kind: Settings, Inhalt: Einstellungen }, ] as const +/** Seiten OHNE Reiter. Sie sind ueber die Adresse und ueber das Kontomenue + * erreichbar -- ein Lesezeichen auf /konto soll nicht ins Leere fuehren, nur + * weil der Reiter verschwunden ist. */ +const versteckt = [{ pfad: '/konto', Inhalt: Konto }] as const + export function App() { // Einzeln ausgewählt und nicht als ganzer Laden: `useStore()` ohne // Auswahlfunktion abonniert ALLES, und dann zeichnet jede Meldung das @@ -52,7 +54,6 @@ export function App() { const user = useStore((s) => s.user) const checking = useStore((s) => s.checking) const checkSession = useStore((s) => s.checkSession) - const logout = useStore((s) => s.logout) const pfad = usePfad() useEffect(() => { @@ -80,8 +81,9 @@ export function App() { ) } + const ohneReiter = versteckt.find((s) => s.pfad === pfad) const aktiv = seiten.find((s) => s.pfad === pfad) ?? seiten[0] - const Inhalt = aktiv.Inhalt + const Inhalt = ohneReiter ? ohneReiter.Inhalt : aktiv.Inhalt return (
@@ -124,21 +126,8 @@ export function App() { })} -
- - - {user.username} - - -
+ +
diff --git a/server/ui/src/auffrischen.ts b/server/ui/src/auffrischen.ts index db04189..c9dbd9a 100644 --- a/server/ui/src/auffrischen.ts +++ b/server/ui/src/auffrischen.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { api, type Stand } from './api' +import { useMerker } from './merker' // Automatisch auffrischen — aber nicht, indem man alle fünf Sekunden alles neu // lädt. @@ -13,7 +14,6 @@ import { api, type Stand } from './api' // Knopf. Eine Liste, die sich beim Klicken selbst neu sortiert, ist der Grund, // warum Leute Auto-Refresh abschalten. -const SPEICHER_SCHLUESSEL = 'd4rk_media.auffrischen' const TAKT_MS = 5000 const gleich = (a: Stand | null, b: Stand | null) => @@ -33,16 +33,12 @@ export function useAuffrischen({ * Hinweis. Etwa: eine Auswahl steht, oder ein Bild ist offen. */ blockiert: boolean }) { - const [an, setAn] = useState(() => localStorage.getItem(SPEICHER_SCHLUESSEL) !== 'aus') + const [an, setAn] = useMerker('auffrischen', true) const [wartet, setWartet] = useState(false) const bekannt = useRef(null) const blockiertRef = useRef(blockiert) blockiertRef.current = blockiert - useEffect(() => { - localStorage.setItem(SPEICHER_SCHLUESSEL, an ? 'an' : 'aus') - }, [an]) - /** Nach eigenem Zutun (Löschen, Hochladen) gleichzuziehen — sonst meldet * der Fühler die eigene Änderung als „neu". */ const merken = useCallback(async () => { diff --git a/server/ui/src/components/Kontomenue.tsx b/server/ui/src/components/Kontomenue.tsx new file mode 100644 index 0000000..9867b14 --- /dev/null +++ b/server/ui/src/components/Kontomenue.tsx @@ -0,0 +1,102 @@ +import { useEffect, useRef, useState } from 'react' +import { ChevronDown, LogOut, UserCog } from 'lucide-react' +import { run, useStore } from '../store' +import { gehZu } from '../router' +import { Avatar } from './Avatar' + +/** Das Konto hinter dem eigenen Namen. + * + * Vorher war es ein eigener Reiter neben Galerie und Verlauf — und stand damit + * gleichrangig neben Dingen, die man täglich benutzt, obwohl man sein Passwort + * zweimal im Jahr ändert. Hinter dem Namen ist der Platz, an dem jeder danach + * sucht. + * + * Der Pfad /konto bleibt bestehen: ein Lesezeichen darauf soll nicht ins Leere + * führen, nur weil der Reiter verschwunden ist. */ +export function Kontomenue() { + const user = useStore((s) => s.user) + const logout = useStore((s) => s.logout) + const [offen, setOffen] = useState(false) + const huelle = useRef(null) + + // Klick daneben und Escape schließen. Ein Menü, das offen bleibt, bis man + // genau den Knopf wieder trifft, ist ein Menü, das im Weg steht. + useEffect(() => { + if (!offen) return + const daneben = (e: MouseEvent) => { + if (!huelle.current?.contains(e.target as Node)) setOffen(false) + } + const taste = (e: KeyboardEvent) => { + if (e.key === 'Escape') setOffen(false) + } + document.addEventListener('mousedown', daneben) + window.addEventListener('keydown', taste) + return () => { + document.removeEventListener('mousedown', daneben) + window.removeEventListener('keydown', taste) + } + }, [offen]) + + if (!user) return null + + const eintrag = + 'flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-schrift-leise ' + + 'transition-colors hover:bg-flaeche-hoch hover:text-schrift' + + return ( +
+ + + {offen && ( +
+
+ +
+

{user.username}

+

+ {user.discord ? 'über Discord' : 'mit Passwort'} +

+
+
+ + + + +
+ )} +
+ ) +} diff --git a/server/ui/src/merker.ts b/server/ui/src/merker.ts new file mode 100644 index 0000000..9e92d93 --- /dev/null +++ b/server/ui/src/merker.ts @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react' + +/** Ein Zustand, der einen Neuladevorgang überlebt. + * + * Für ANSICHTSSACHEN gedacht: wie viele Kacheln je Seite, wonach sortiert + * wird. Wer 120 pro Seite einstellt, meint das dauerhaft und nicht bis zum + * nächsten F5. + * + * BEWUSST NICHT für Filter. Ein gemerkter Ordner- oder Artfilter führt zu der + * Frage „warum sehe ich nichts", und die Antwort steht dann in einer Leiste, + * auf die niemand schaut, weil er sie nicht selbst gesetzt hat. Was die + * Ansicht formt, wird gemerkt; was die Menge einschränkt, nicht. + * + * localStorage kann werfen — im privaten Fenster mancher Browser, oder wenn + * der Speicher voll ist. Dann verhält sich der Haken wie ein gewöhnlicher + * Zustand, statt die Seite mitzureißen. */ +export function useMerker(schluessel: string, standard: T) { + const voll = `d4rk_media.${schluessel}` + + const [wert, setWert] = useState(() => { + try { + const roh = localStorage.getItem(voll) + return roh === null ? standard : (JSON.parse(roh) as T) + } catch { + return standard + } + }) + + useEffect(() => { + try { + localStorage.setItem(voll, JSON.stringify(wert)) + } catch { + /* dann eben nur für diese Sitzung */ + } + }, [voll, wert]) + + return [wert, setWert] as const +} diff --git a/server/ui/src/pages/Galerie.tsx b/server/ui/src/pages/Galerie.tsx index dc03ef9..b764321 100644 --- a/server/ui/src/pages/Galerie.tsx +++ b/server/ui/src/pages/Galerie.tsx @@ -38,6 +38,7 @@ import { Hochladen } from '../components/Hochladen' import { Ansehen } from '../components/Ansehen' import { Verschieben } from '../components/Verschieben' import { useAuffrischen } from '../auffrischen' +import { useMerker } from '../merker' const SEITENGROESSEN = [30, 60, 120, 200] as const @@ -49,9 +50,11 @@ export function Galerie() { const [seite, setSeite] = useState(0) const [art, setArt] = useState('') const [arten, setArten] = useState(null) - const [proSeite, setProSeite] = useState(60) - const [sortierung, setSortierung] = useState('geaendert') - const [richtung, setRichtung] = useState('ab') + // Ansichtssachen ueberleben das Neuladen -- wer 120 einstellt, meint das + // dauerhaft und nicht bis zum naechsten F5. + const [proSeite, setProSeite] = useMerker('galerie.proSeite', 60) + const [sortierung, setSortierung] = useMerker('galerie.sort', 'geaendert') + const [richtung, setRichtung] = useMerker('galerie.dir', 'ab') const [daten, setDaten] = useState(null) const [laedt, setLaedt] = useState(true) const [gewaehlt, setGewaehlt] = useState>(new Set()) diff --git a/server/ui/src/pages/Verlauf.tsx b/server/ui/src/pages/Verlauf.tsx index 6e84bd1..bf1f789 100644 --- a/server/ui/src/pages/Verlauf.tsx +++ b/server/ui/src/pages/Verlauf.tsx @@ -16,6 +16,7 @@ import { useStore } from '../store' import { bytes, zeit } from '../format' import { Karte, Knopf, Leer } from '../components/Bausteine' import { useAuffrischen } from '../auffrischen' +import { useMerker } from '../merker' import { Avatar } from '../components/Avatar' const SEITENGROESSEN = [25, 50, 100, 200] as const @@ -34,7 +35,7 @@ export function Verlauf() { const [seite, setSeite] = useState(0) const [art, setArt] = useState('') const [wer, setWer] = useState('') - const [proSeite, setProSeite] = useState(50) + const [proSeite, setProSeite] = useMerker('verlauf.proSeite', 50) const [suche, setSuche] = useState('') // Wie in der Galerie: erst tippen lassen, dann fragen. Sonst geht bei