feat: nach Medienart filtern -- und die Endung schlaegt das Achselzucken
FILTER. Eine Leiste ueber der Galerie: Alles, Bilder, Video, Ton, Dokumente, Anderes -- mit der Anzahl AM KNOPF. Ein Knopf "Video", hinter dem nichts liegt, kostet sonst einen Klick, um das herauszufinden; leere Arten sind deshalb sichtbar, aber gesperrt. Die Uebersetzung Art -> SQL steht an EINER Stelle (ARTEN), weil zwei Stellen zwangslaeufig auseinanderlaufen: die Liste zeigte dann etwas anderes, als der Zaehler daneben behauptet. "Anderes" ist bewusst die VERNEINUNG der uebrigen und keine eigene Liste -- sonst faellt ein neuer Typ durch alle Filter und ist nirgends zu finden. Nachgemessen: die Summe der fuenf Arten ergibt die Gesamtzahl. /media/ids nimmt denselben Filter. Ohne das waehlt "alle waehlen" mehr aus, als zu sehen ist -- und geloescht wuerde Unsichtbares. DER FUND BEIM MESSEN: clip.mp4, ton.mp3, handbuch.pdf und notiz.txt landeten ALLE unter "Anderes". Ein Formularteil ohne erkannte Art traegt "application/octet-stream", und mein Code hat das der Endungstabelle vorgezogen. Was der Absender ueber die Art sagt, gilt weiterhin -- aber nicht sein Achselzucken. Danach: mp4 -> video/mp4, mp3 -> audio/mpeg, pdf und txt -> Dokumente, und .zip bleibt richtigerweise "Anderes". POLITUR. Ein ruhiger Verlauf im Hintergrund statt einer flachen Flaeche (ein einfarbiges Dunkel sieht auf einem grossen Monitor aus wie ein Ladefehler), Bildlaufleisten im Ton der Oberflaeche, ein Balken unter der aktiven Seite, Karten mit einem Hauch Tiefe, ein spuerbarer Druckpunkt an den Knoepfen, und Kacheln, die sich beim Darueberfahren leicht heben. Der Seitenwechsel blendet auf -- und wer prefers-reduced-motion gesetzt hat, bekommt nichts davon. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,10 @@ import {
|
||||
ExternalLink,
|
||||
FileQuestion,
|
||||
ArrowDownUp,
|
||||
FileText,
|
||||
Film,
|
||||
Layers,
|
||||
Music,
|
||||
ImageOff,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
@@ -14,7 +18,17 @@ import {
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import { api, vorschauUrl, type Folders, type MediaItem, type MediaPage, type Richtung, type Sortierung } from '../api'
|
||||
import {
|
||||
api,
|
||||
vorschauUrl,
|
||||
type Art,
|
||||
type Arten,
|
||||
type Folders,
|
||||
type MediaItem,
|
||||
type MediaPage,
|
||||
type Richtung,
|
||||
type Sortierung,
|
||||
} from '../api'
|
||||
import { run, useStore } from '../store'
|
||||
import { bytes, istBild, zeit } from '../format'
|
||||
import { Knopf, Leer, Nachfrage, useKopieren } from '../components/Bausteine'
|
||||
@@ -31,6 +45,8 @@ export function Galerie() {
|
||||
const [ordner, setOrdner] = useState('')
|
||||
const [ordnerDaten, setOrdnerDaten] = useState<Folders | null>(null)
|
||||
const [seite, setSeite] = useState(0)
|
||||
const [art, setArt] = useState<Art>('')
|
||||
const [arten, setArten] = useState<Arten | null>(null)
|
||||
const [sortierung, setSortierung] = useState<Sortierung>('geaendert')
|
||||
const [richtung, setRichtung] = useState<Richtung>('ab')
|
||||
const [daten, setDaten] = useState<MediaPage | null>(null)
|
||||
@@ -67,6 +83,7 @@ export function Galerie() {
|
||||
const seiteDaten = await api.media(
|
||||
sucheAktiv,
|
||||
ordner,
|
||||
art,
|
||||
PRO_SEITE,
|
||||
seite * PRO_SEITE,
|
||||
sortierung,
|
||||
@@ -80,7 +97,7 @@ export function Galerie() {
|
||||
} finally {
|
||||
if (meine === lauf.current) setLaedt(false)
|
||||
}
|
||||
}, [sucheAktiv, ordner, seite, sortierung, richtung, note])
|
||||
}, [sucheAktiv, ordner, art, seite, sortierung, richtung, note])
|
||||
|
||||
useEffect(() => {
|
||||
void laden()
|
||||
@@ -90,6 +107,22 @@ export function Galerie() {
|
||||
// haengen deshalb am selben Auslöser wie die Liste -- eine Uebersicht, die
|
||||
// "21 Dateien" behauptet, waehrend daneben 19 stehen, ist schlimmer als
|
||||
// keine.
|
||||
// Die Zahlen an den Knoepfen haengen an Suche und Ordner, aber NICHT an
|
||||
// der gewaehlten Art: sonst zeigte "Video (3)" nach dem Klick "Video (3)"
|
||||
// und alle anderen 0.
|
||||
const artenLaden = useCallback(async () => {
|
||||
try {
|
||||
setArten(await api.arten(sucheAktiv, ordner))
|
||||
} catch {
|
||||
// Die Zahlen sind Beiwerk. Faellt der Aufruf aus, bleiben die
|
||||
// Knoepfe ohne Zahl statt die Seite mit einem Fehler zu fluten.
|
||||
}
|
||||
}, [sucheAktiv, ordner])
|
||||
|
||||
useEffect(() => {
|
||||
void artenLaden()
|
||||
}, [artenLaden])
|
||||
|
||||
const ordnerLaden = useCallback(async () => {
|
||||
try {
|
||||
setOrdnerDaten(await api.folders())
|
||||
@@ -105,7 +138,8 @@ export function Galerie() {
|
||||
const allesLaden = useCallback(() => {
|
||||
void laden()
|
||||
void ordnerLaden()
|
||||
}, [laden, ordnerLaden])
|
||||
void artenLaden()
|
||||
}, [laden, ordnerLaden, artenLaden])
|
||||
|
||||
// Blockiert, solange eine Auswahl steht oder ein Bild offen ist: dann gibt
|
||||
// es nur den Hinweis und keinen Neuaufbau unter den Haenden.
|
||||
@@ -131,7 +165,7 @@ export function Galerie() {
|
||||
|
||||
async function alleTrefferWaehlen() {
|
||||
const antwort = await run(
|
||||
() => api.medienIds(sucheAktiv, ordner),
|
||||
() => api.medienIds(sucheAktiv, ordner, art),
|
||||
(r) =>
|
||||
r.gekappt
|
||||
? `${r.ids.length} gewählt — mehr gibt der Dienst auf einmal nicht heraus.`
|
||||
@@ -198,6 +232,50 @@ export function Galerie() {
|
||||
/>
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-4">
|
||||
{/* Nach Art filtern. Die Zahlen stehen dabei — ein Knopf "Video",
|
||||
hinter dem nichts liegt, kostet einen Klick, um das
|
||||
herauszufinden. */}
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{(
|
||||
[
|
||||
['', 'Alles', Layers],
|
||||
['bild', 'Bilder', ImageOff],
|
||||
['video', 'Video', Film],
|
||||
['ton', 'Ton', Music],
|
||||
['dokument', 'Dokumente', FileText],
|
||||
['andere', 'Anderes', FileQuestion],
|
||||
] as const
|
||||
).map(([wert, text, Symbol]) => {
|
||||
const n = arten?.[wert || 'alle']
|
||||
const an = art === wert
|
||||
// Leere Arten bleiben stehen statt zu verschwinden: eine
|
||||
// Leiste, die ihre Knöpfe wechselt, trifft man nicht.
|
||||
return (
|
||||
<button
|
||||
key={wert}
|
||||
onClick={() => {
|
||||
setArt(wert as Art)
|
||||
setSeite(0)
|
||||
setGewaehlt(new Set())
|
||||
}}
|
||||
disabled={n === 0 && wert !== ''}
|
||||
className={`flex items-center gap-1.5 rounded-md border px-2.5 py-1 text-xs
|
||||
transition-colors disabled:cursor-not-allowed disabled:opacity-40 ${
|
||||
an
|
||||
? 'border-akzent bg-akzent/15 text-schrift'
|
||||
: 'border-rand text-schrift-leise hover:text-schrift'
|
||||
}`}
|
||||
>
|
||||
<Symbol size={13} />
|
||||
{text}
|
||||
{n !== undefined && (
|
||||
<span className="tabular-nums opacity-70">{n}</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Kopfzeile: Suche, Auswahl, Löschen */}
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="relative min-w-56 flex-1">
|
||||
@@ -349,7 +427,12 @@ export function Galerie() {
|
||||
<figure
|
||||
key={item.id}
|
||||
className={`group relative overflow-hidden rounded-lg border bg-flaeche
|
||||
transition-colors ${an ? 'border-akzent' : 'border-rand'}`}
|
||||
transition-all hover:-translate-y-0.5 hover:shadow-lg
|
||||
hover:shadow-black/30 ${
|
||||
an
|
||||
? 'border-akzent ring-1 ring-akzent/40'
|
||||
: 'border-rand hover:border-schrift-leise/40'
|
||||
}`}
|
||||
>
|
||||
{/* Klick auf die Kachel ÖFFNET, Klick auf das
|
||||
Kästchen WÄHLT AUS. Vorher tat der Klick
|
||||
|
||||
Reference in New Issue
Block a user