feat: Ordneruebersicht in der Galerie -- und der Proxy haelt keine Bilder mehr fest
ORDNERUEBERSICHT. Eine Spalte neben der Galerie mit allen Ordnern, auf allen Ebenen, mit Anzahl und Groesse; ein Klick filtert. Neu dafuer: - GET /api/dash/folders -- die Ordner werden in JavaScript zusammengezaehlt und nicht in SQL: fuer eine Baumansicht braucht es JEDE Ebene, und das waere in SQLite eine rekursive Abfrage, die niemand mehr liest. - GET /api/dash/media?folder=... als EIGENER Filter neben der Suche. Die Suche findet mit LIKE %...% auch "alt/vehicles/adder.webp"; ein Ordner, der Dinge zeigt, die nicht darin liegen, ist keiner. Hier ist der Anfang verankert. - Die LIKE-Sonderzeichen werden maskiert: ein Ordner "fotos_alt" wuerde sonst auch "fotosXalt" treffen, weil _ in LIKE ein beliebiges Zeichen ist. Die Zahlen sind kumulativ -- "vehicles" enthaelt, was in "vehicles/showroom" liegt. Alles andere ueberrascht in einer Baumansicht. Nachgemessen ueber drei Ebenen: vehicles (24) > showroom (2) > tief (1), Filter und Zaehlung stimmen, und nach dem Aufraeumen steht wieder vehicles (22). Flache Liste mit Einrueckung statt aufklappbarem Baum: bei einer Handvoll Ordner ist ein Aufklappmechanismus mehr Bedienung als Nutzen -- und was man nicht aufklappen muss, kann man auch nicht zugeklappt uebersehen. Dazu: der NPM-Zwischenspeicher ist zu. Der Schnipsel steht im Feld "Custom Nginx Configuration"; NPM setzt ihn innerhalb des server-Blocks ein, eine location ist dort also gueltig. Nachgemessen: cache-control kommt wieder von uns, und ein ueberschriebenes Bild ist sofort das neue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,16 +12,19 @@ import {
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import { api, vorschauUrl, type MediaItem, type MediaPage } from '../api'
|
||||
import { api, vorschauUrl, type Folders, type MediaItem, type MediaPage } from '../api'
|
||||
import { run, useStore } from '../store'
|
||||
import { bytes, istBild, zeit } from '../format'
|
||||
import { Knopf, Leer, Nachfrage, useKopieren } from '../components/Bausteine'
|
||||
import { Ordner } from '../components/Ordner'
|
||||
|
||||
const PRO_SEITE = 60
|
||||
|
||||
export function Galerie() {
|
||||
const note = useStore((s) => s.note)
|
||||
const [suche, setSuche] = useState('')
|
||||
const [ordner, setOrdner] = useState('')
|
||||
const [ordnerDaten, setOrdnerDaten] = useState<Folders | null>(null)
|
||||
const [seite, setSeite] = useState(0)
|
||||
const [daten, setDaten] = useState<MediaPage | null>(null)
|
||||
const [laedt, setLaedt] = useState(true)
|
||||
@@ -51,7 +54,7 @@ export function Galerie() {
|
||||
const meine = ++lauf.current
|
||||
setLaedt(true)
|
||||
try {
|
||||
const seiteDaten = await api.media(sucheAktiv, PRO_SEITE, seite * PRO_SEITE)
|
||||
const seiteDaten = await api.media(sucheAktiv, ordner, PRO_SEITE, seite * PRO_SEITE)
|
||||
if (meine !== lauf.current) return
|
||||
setDaten(seiteDaten)
|
||||
} catch (err) {
|
||||
@@ -60,12 +63,28 @@ export function Galerie() {
|
||||
} finally {
|
||||
if (meine === lauf.current) setLaedt(false)
|
||||
}
|
||||
}, [sucheAktiv, seite, note])
|
||||
}, [sucheAktiv, ordner, seite, note])
|
||||
|
||||
useEffect(() => {
|
||||
void laden()
|
||||
}, [laden])
|
||||
|
||||
// Die Ordnerzahlen aendern sich beim Loeschen und beim Hochladen. Sie
|
||||
// haengen deshalb am selben Auslöser wie die Liste -- eine Uebersicht, die
|
||||
// "21 Dateien" behauptet, waehrend daneben 19 stehen, ist schlimmer als
|
||||
// keine.
|
||||
const ordnerLaden = useCallback(async () => {
|
||||
try {
|
||||
setOrdnerDaten(await api.folders())
|
||||
} catch (err) {
|
||||
note('fehler', err instanceof Error ? err.message : 'Unbekannter Fehler')
|
||||
}
|
||||
}, [note])
|
||||
|
||||
useEffect(() => {
|
||||
void ordnerLaden()
|
||||
}, [ordnerLaden])
|
||||
|
||||
const items = daten?.items ?? []
|
||||
const gesamt = daten?.total ?? 0
|
||||
const seiten = Math.max(1, Math.ceil(gesamt / PRO_SEITE))
|
||||
@@ -116,6 +135,7 @@ export function Galerie() {
|
||||
if (ergebnis) {
|
||||
setGewaehlt(new Set())
|
||||
void laden()
|
||||
void ordnerLaden()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +145,18 @@ export function Galerie() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 lg:flex-row">
|
||||
<Ordner
|
||||
daten={ordnerDaten}
|
||||
gewaehlt={ordner}
|
||||
onWahl={(o) => {
|
||||
setOrdner(o)
|
||||
setSeite(0)
|
||||
setGewaehlt(new Set())
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-4">
|
||||
{/* Kopfzeile: Suche, Auswahl, Löschen */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="relative min-w-56 flex-1">
|
||||
@@ -190,8 +221,10 @@ export function Galerie() {
|
||||
kind={<ImageOff size={28} className="text-schrift-leise/50" />}
|
||||
text={
|
||||
sucheAktiv
|
||||
? `Nichts gefunden für "${sucheAktiv}".`
|
||||
: 'Noch keine Datei hochgeladen.'
|
||||
? `Nichts gefunden für "${sucheAktiv}"${ordner ? ` in ${ordner}/` : ''}.`
|
||||
: ordner
|
||||
? `In ${ordner}/ liegt nichts.`
|
||||
: 'Noch keine Datei hochgeladen.'
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
@@ -321,6 +354,8 @@ export function Galerie() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
<Nachfrage
|
||||
offen={loeschen !== null}
|
||||
titel={loeschen?.length === 1 ? 'Datei löschen?' : `${loeschen?.length} Dateien löschen?`}
|
||||
|
||||
Reference in New Issue
Block a user