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:
@@ -283,6 +283,23 @@ dashRoutes.post('/auth/password', async (c) => {
|
||||
|
||||
// ------------------------------------------------------------------- Medien
|
||||
|
||||
/** Die Medienarten, wie sie im Dashboard heissen -- und was sie in SQL sind.
|
||||
*
|
||||
* An EINER Stelle, weil zwei Stellen zwangslaeufig auseinanderlaufen: die
|
||||
* Liste zeigte dann etwas anderes als der Zaehler daneben behauptet.
|
||||
*
|
||||
* "andere" ist bewusst die Verneinung der uebrigen und keine eigene Liste:
|
||||
* sonst faellt ein neuer Typ (z. B. application/zip) durch alle Filter und
|
||||
* ist nirgends zu finden. */
|
||||
const ARTEN: Record<string, string> = {
|
||||
bild: "mime LIKE 'image/%'",
|
||||
video: "mime LIKE 'video/%'",
|
||||
ton: "mime LIKE 'audio/%'",
|
||||
dokument: "(mime = 'application/pdf' OR mime LIKE 'text/%' OR mime LIKE 'application/json%')",
|
||||
andere: "(mime NOT LIKE 'image/%' AND mime NOT LIKE 'video/%' AND mime NOT LIKE 'audio/%' " +
|
||||
"AND mime <> 'application/pdf' AND mime NOT LIKE 'text/%' AND mime NOT LIKE 'application/json%')",
|
||||
}
|
||||
|
||||
dashRoutes.get('/media', (c) => {
|
||||
const query = c.req.query('query')?.trim() ?? ''
|
||||
const limit = Math.min(Number(c.req.query('limit') ?? 60) || 60, 200)
|
||||
@@ -311,6 +328,11 @@ dashRoutes.get('/media', (c) => {
|
||||
params.push(`${maskiert(folder)}/%`)
|
||||
}
|
||||
|
||||
// Die Art kommt aus der Adresszeile und geht in SQL -- also gegen die
|
||||
// feste Tabelle getauscht und niemals durchgereicht.
|
||||
const art = c.req.query('art') ?? ''
|
||||
if (ARTEN[art]) bedingungen.push(ARTEN[art])
|
||||
|
||||
const where = bedingungen.length ? `WHERE ${bedingungen.join(' AND ')}` : ''
|
||||
|
||||
// Die Sortierung kommt vom Aufrufer und geht in SQL -- also NIEMALS
|
||||
@@ -357,6 +379,41 @@ dashRoutes.get('/media', (c) => {
|
||||
*
|
||||
* Die Zahlen sind KUMULATIV: "vehicles" enthaelt, was in "vehicles/showroom"
|
||||
* liegt. Alles andere ueberrascht in einer Baumansicht. */
|
||||
/** Wie viele Dateien es je Art gibt -- fuer die Filterknoepfe.
|
||||
*
|
||||
* Mit den gerade geltenden Einschraenkungen (Ordner, Suche), sonst stuende an
|
||||
* einem Knopf eine Zahl, die nach dem Klick nicht stimmt. */
|
||||
dashRoutes.get('/arten', (c) => {
|
||||
const query = c.req.query('query')?.trim() ?? ''
|
||||
const folder = c.req.query('folder')?.trim().replace(/\/+$/, '') ?? ''
|
||||
const maskiert = (t: string) => t.replace(/[\\%_]/g, (z) => `\\${z}`)
|
||||
|
||||
const grund: string[] = []
|
||||
const params: unknown[] = []
|
||||
if (query) {
|
||||
grund.push(`path LIKE ? ESCAPE '\\'`)
|
||||
params.push(`%${maskiert(query)}%`)
|
||||
}
|
||||
if (folder) {
|
||||
grund.push(`path LIKE ? ESCAPE '\\'`)
|
||||
params.push(`${maskiert(folder)}/%`)
|
||||
}
|
||||
|
||||
const zaehle = (zusatz?: string) => {
|
||||
const alle = zusatz ? [...grund, zusatz] : grund
|
||||
const where = alle.length ? `WHERE ${alle.join(' AND ')}` : ''
|
||||
return (
|
||||
db.prepare(`SELECT COUNT(*) AS n FROM media ${where}`).get(...params) as {
|
||||
n: number
|
||||
}
|
||||
).n
|
||||
}
|
||||
|
||||
const out: Record<string, number> = { alle: zaehle() }
|
||||
for (const [name, bedingung] of Object.entries(ARTEN)) out[name] = zaehle(bedingung)
|
||||
return c.json(out)
|
||||
})
|
||||
|
||||
dashRoutes.get('/folders', (c) => {
|
||||
const rows = db.prepare('SELECT path, size FROM media').all() as {
|
||||
path: string
|
||||
@@ -418,6 +475,12 @@ dashRoutes.get('/media/ids', (c) => {
|
||||
bedingungen.push(`path LIKE ? ESCAPE '\\'`)
|
||||
params.push(`${maskiert(folder)}/%`)
|
||||
}
|
||||
// MUSS dieselbe Einschraenkung haben wie die Liste: sonst waehlt "alle
|
||||
// waehlen" mehr aus, als der Filter zeigt -- und geloescht wird dann
|
||||
// Unsichtbares.
|
||||
const art = c.req.query('art') ?? ''
|
||||
if (ARTEN[art]) bedingungen.push(ARTEN[art])
|
||||
|
||||
const where = bedingungen.length ? `WHERE ${bedingungen.join(' AND ')}` : ''
|
||||
|
||||
const DECKEL = 5000
|
||||
@@ -551,7 +614,16 @@ dashRoutes.post('/upload', async (c) => {
|
||||
continue
|
||||
}
|
||||
|
||||
const mime = datei.type || mimeFor(path)
|
||||
// Was der Absender ueber die Art sagt, gilt -- ABER NICHT sein
|
||||
// Achselzucken. Ein Formularteil ohne erkannte Art traegt
|
||||
// "application/octet-stream", und das ist weniger wert als unsere
|
||||
// Endungstabelle: beim Messen landeten so clip.mp4, ton.mp3 und
|
||||
// handbuch.pdf allesamt unter "Anderes".
|
||||
const gemeldet = (datei.type || '').split(';')[0]?.trim().toLowerCase() ?? ''
|
||||
const mime =
|
||||
gemeldet && gemeldet !== 'application/octet-stream'
|
||||
? datei.type
|
||||
: mimeFor(path)
|
||||
await writeFileAtomic(path, data)
|
||||
if (kannVorschau(mime)) await writeThumb(path, data)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user