diff --git a/server/ui/src/api.ts b/server/ui/src/api.ts index d95256d..d9547e1 100644 --- a/server/ui/src/api.ts +++ b/server/ui/src/api.ts @@ -178,6 +178,8 @@ export type TokenRow = { max_bytes: number | null /** Wieviel dieser Token INSGESAMT halten darf. Null = unbegrenzt. */ quota_bytes: number | null + /** Groesste Kantenlaenge fuer Bilder. Null = ablegen wie es kommt. */ + max_kante: number | null /** Erlaubte Arten, mit Komma. Leer = alle. */ arten: string expires_at: number | null @@ -508,6 +510,38 @@ export const api = { { aktion }, ), + /** Wozu fehlt ein Bild — und was liegt da, ohne in der Liste zu stehen. */ + fehlende: (muster: string, namen: string) => + post<{ + muster: string + geprueft: number + da: number + fehlt: { name: string; pfad: string }[] + ueberzaehlig: string[] + ueberzaehligGesamt: number + unbrauchbar: { name: string; grund: string }[] + }>('/api/dash/fehlende', { muster, namen }), + + /** Ohne `ausfuehren` wird nur gerechnet und gezeigt — angefasst wird nichts. */ + ersetzen: (suchen: string, ersetzen: string, ordner: string, ausfuehren = false) => + post<{ + vorschau?: true + treffer?: number + plan?: { alt: string; neu: string; abrufe: number }[] + planGesamt?: number + probleme?: { alt: string; grund: string }[] + abrufeGesamt?: number + umbenannt?: number + gescheitert?: { alt: string; grund: string }[] + beispiele?: { alt: string; neu: string }[] + }>('/api/dash/media/ersetzen', { suchen, ersetzen, ordner, ausfuehren }), + + einstellungenImport: (werte: Record, ueberschreiben: boolean) => + post<{ gesetzt: string[]; uebersprungen: string[] }>('/api/dash/einstellungen/import', { + werte, + ueberschreiben, + }), + verwaltung: (limit = 100, offset = 0) => get<{ items: VerwaltungRow[]; total: number; limit: number; offset: number }>( `/api/dash/verwaltung?limit=${limit}&offset=${offset}`, @@ -534,6 +568,7 @@ export const api = { canDelete?: boolean maxMb?: string | null quotaMb?: string | null + maxKante?: string | null arten?: string[] | null gueltigTage?: string | null }, @@ -573,6 +608,7 @@ export const api = { prefix: string canDelete: boolean maxMb?: string + maxKante?: string quotaMb?: string arten?: string[] gueltigTage?: string diff --git a/server/ui/src/components/ErsetzenKarte.tsx b/server/ui/src/components/ErsetzenKarte.tsx new file mode 100644 index 0000000..060374b --- /dev/null +++ b/server/ui/src/components/ErsetzenKarte.tsx @@ -0,0 +1,232 @@ +import { useState } from 'react' +import { AlertTriangle, Eye, Replace } from 'lucide-react' +import { api, type Folders } from '../api' +import { run } from '../store' +import { Karte, Knopf, Nachfrage } from './Bausteine' + +type Vorschau = Awaited> + +/** Suchen und Ersetzen über viele Pfade. + * + * DIE VORSCHAU IST DER EIGENTLICHE TEIL, nicht die Bequemlichkeit dahinter. + * Was hier passiert, ist dieselbe Warnung wie beim Umbenennen eines Ordners — + * **jede alte Adresse gibt danach 404** —, nur mal tausend. Wer das ohne + * Gegenlesen auslöst, hat keine Gelegenheit mehr, es zu bemerken. + * + * Deshalb gibt es hier keinen Knopf, der es sofort tut. Erst zeigen, dann + * fragen, dann machen. */ +export function ErsetzenKarte({ + ordnerDaten, + fertig, +}: { + ordnerDaten: Folders | null + fertig: () => void +}) { + const [suchen, setSuchen] = useState('') + const [ersetzen, setErsetzen] = useState('') + const [ordner, setOrdner] = useState('') + const [vorschau, setVorschau] = useState(null) + const [laeuft, setLaeuft] = useState(false) + const [fragt, setFragt] = useState(false) + + const bereit = suchen.trim().length > 0 + const machbar = + vorschau?.vorschau === true && + (vorschau.probleme?.length ?? 0) === 0 && + (vorschau.planGesamt ?? 0) > 0 + + return ( + +

+ Für den Fall, dass sich eine Namenskonvention ändert oder ein Ordner anders heißen + soll. Erst wird gezeigt, was passieren würde{' '} + — angefasst wird nichts, bevor du es gelesen hast. +

+ +
+ + + +
+ +
+ { + setLaeuft(true) + const r = await run( + () => api.ersetzen(suchen, ersetzen, ordner, false), + (x) => + (x.probleme?.length ?? 0) > 0 + ? `${x.planGesamt} möglich, ${x.probleme?.length} nicht — siehe unten.` + : `${x.planGesamt} Datei(en) würden umbenannt.`, + ) + setLaeuft(false) + setVorschau(r) + }} + kind={ + <> + + Zeigen, was passieren würde + + } + /> + {machbar && ( + setFragt(true)} + kind={ + <> + + {vorschau?.planGesamt} umbenennen + + } + /> + )} +
+ + {vorschau?.vorschau && ( +
+

+ {vorschau.treffer} Treffer ·{' '} + {vorschau.planGesamt} umbenennbar ·{' '} + + {vorschau.probleme?.length ?? 0} + {' '} + Probleme +

+ + {(vorschau.abrufeGesamt ?? 0) > 0 && ( +

+ + + Die betroffenen Dateien wurden zusammen{' '} + {vorschau.abrufeGesamt}× geholt. Jede alte Adresse + gibt danach 404 — und keine davon meldet sich von selbst. + +

+ )} + + {(vorschau.probleme?.length ?? 0) > 0 && ( +
+

+ Das geht nicht — und deshalb wird gar nichts angefasst +

+
+                                {vorschau.probleme?.map((p) => `${p.alt}\n  → ${p.grund}`).join('\n')}
+                            
+
+ )} + + {(vorschau.plan?.length ?? 0) > 0 && ( +
+

+ So sähe es aus + {(vorschau.planGesamt ?? 0) > (vorschau.plan?.length ?? 0) && + ` — erste ${vorschau.plan?.length} von ${vorschau.planGesamt}`} +

+
+                                {vorschau.plan?.map((p) => `${p.alt}\n  → ${p.neu}`).join('\n')}
+                            
+
+ )} +
+ )} + + setFragt(false)} + onJa={async () => { + setLaeuft(true) + const r = await run( + () => api.ersetzen(suchen, ersetzen, ordner, true), + (x) => + `${x.umbenannt} umbenannt` + + ((x.gescheitert?.length ?? 0) > 0 + ? `, ${x.gescheitert?.length} nicht` + : '.'), + ) + setLaeuft(false) + setFragt(false) + if (r) { + setVorschau(null) + setSuchen('') + setErsetzen('') + fertig() + } + }} + text={ +

+ + + {suchen} wird zu{' '} + {ersetzen || '(nichts)'}, in{' '} + {vorschau?.planGesamt} Datei(en).{' '} + + Alle bisherigen Adressen dieser Dateien geben danach 404. + {' '} + Rückgängig geht das nur, indem du es andersherum noch einmal machst — + und das findet die Dateien nur, solange niemand sonst etwas geändert + hat. + +

+ } + /> +
+ ) +} diff --git a/server/ui/src/components/FehlendeKarte.tsx b/server/ui/src/components/FehlendeKarte.tsx new file mode 100644 index 0000000..d2a6e95 --- /dev/null +++ b/server/ui/src/components/FehlendeKarte.tsx @@ -0,0 +1,174 @@ +import { useEffect, useState } from 'react' +import { Check, ListChecks } from 'lucide-react' +import { api, type Vorlage } from '../api' +import { run } from '../store' +import { Karte, Knopf } from './Bausteine' + +/** Wozu fehlt ein Bild? + * + * DIE FRAGE, DIE BISHER NIEMAND BEANTWORTET HAT. Das Fotostudio weiß, was es + * aufgenommen hat. Dieser Dienst weiß, was liegt. Die Differenz rechnet + * keiner aus — und deshalb fällt „das Handy zeigt bei manchen Autos kein Bild" + * erst im Spiel auf. + * + * Das Muster kommt aus den **Adressvorlagen**, damit hier nicht dieselbe + * Zeichenkette ein zweites Mal getippt wird. Wer keine angelegt hat, tippt + * sie — aber der Vorschlag steht daneben. */ +export function FehlendeKarte() { + const [vorlagen, setVorlagen] = useState([]) + const [muster, setMuster] = useState('vehicles/{model}.webp') + const [namen, setNamen] = useState('') + const [laeuft, setLaeuft] = useState(false) + const [ergebnis, setErgebnis] = useState + > | null>(null) + + useEffect(() => { + api.einstellungen() + .then((e) => { + setVorlagen(e.vorlagen) + if (e.vorlagen[0]) setMuster(e.vorlagen[0].muster) + }) + .catch(() => { + /* dann eben ohne Vorschläge — das Feld ist trotzdem benutzbar */ + }) + }, []) + + const anzahl = namen.split(/[\r\n,;]+/).filter((n) => n.trim()).length + + return ( + +

+ Eine Liste von Namen hineingeben — Fahrzeugmodelle, Item-Namen — und beantwortet + bekommen, wozu kein Bild daliegt. Und + umgekehrt: was daliegt, ohne in der Liste zu stehen. +

+ +
+ + +