feat: die Oberflaeche zu den vier Werkzeugen

Bisher liessen sie sich nur ueber die API benutzen. Jetzt stehen sie dort, wo
man sie sucht.

SPEICHER bekommt zwei Karten:

"Wozu fehlt ein Bild?" -- Muster und Namensliste hinein, beide Richtungen
heraus. Das Muster kommt aus den ADRESSVORLAGEN und wird als Knopfreihe
angeboten, damit hier nicht dieselbe Zeichenkette ein zweites Mal getippt wird.

"Suchen und Ersetzen in Pfaden" -- und dort gibt es KEINEN Knopf, der es sofort
tut. Erst zeigen, dann fragen, dann machen. Die Vorschau nennt die Zahl, die
die Entscheidung traegt: wie oft die betroffenen Dateien zusammen geholt
wurden. Null heisst, es schaut niemand zu; viertausend heisst, es schaut jemand
zu. Gibt es auch nur ein Problem, verschwindet der Ausfuehren-Knopf ganz --
nicht ausgegraut, sondern weg, denn ein Problem verhindert ohnehin alles.

EINSTELLUNGEN bekommt "Einstellungen mitnehmen". Herunterladen ist der
harmlose Knopf; "mit Geheimnissen" ist rot und fragt nach, mit dem Satz, dass
die Datei das Discord-Geheimnis und das Nextcloud-Passwort im Klartext traegt
und der Download im Verwaltungs-Verlauf steht.

TOKEN bekommen das Feld "Bilder verkleinern auf" -- im Anlege-Formular und im
Bearbeiten-Dialog, mit dem Hinweis, dass kleinere Bilder NICHT aufgeblasen
werden. In der Liste steht es als "max 512 px" neben den anderen Grenzen.

Zwei eigene Stolperer beim Bauen, beide vom Uebersetzer gefangen: ein Patch
brach vor dem Schreiben ab, sodass die Haelfte der API-Aufrufe fehlte -- und in
FehlendeKarte blieb ein `note` stehen, das niemand mehr benutzte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 08:03:04 +02:00
co-authored by Claude Opus 5
parent 381feade4e
commit 6a49713821
8 changed files with 630 additions and 1 deletions
+3
View File
@@ -8,6 +8,7 @@ import { DiscordKarte } from '../components/DiscordKarte'
import { VorlagenKarte } from '../components/VorlagenKarte'
import { FreigabenKarte } from '../components/FreigabenKarte'
import { BenutzerKarte } from '../components/BenutzerKarte'
import { MitnehmenKarte } from '../components/MitnehmenKarte'
import { MeldungenKarte } from '../components/MeldungenKarte'
/** Alles, was sich einstellen lässt, an einer Stelle.
@@ -34,6 +35,8 @@ export function Einstellungen() {
<FreigabenKarte />
<BenutzerKarte />
<MitnehmenKarte />
<SicherungKarte />
<MeldungenKarte />
<DiscordKarte />
+15 -1
View File
@@ -14,8 +14,11 @@ import { api, type Stats } from '../api'
import { run, useStore } from '../store'
import { bytes } from '../format'
import { Karte, Knopf, Leer, Nachfrage, useKopieren } from '../components/Bausteine'
import { type Verwaiste } from '../api'
import { type Folders, type Verwaiste } from '../api'
import { SpeicherKarten } from '../components/SpeicherKarten'
import { FehlendeKarte } from '../components/FehlendeKarte'
import { ErsetzenKarte } from '../components/ErsetzenKarte'
import { api as apiFolders } from '../api'
export function Speicher() {
const note = useStore((s) => s.note)
@@ -24,6 +27,13 @@ export function Speicher() {
const [zieht, setZieht] = useState(false)
const [spart, setSpart] = useState(false)
const [fortschritt, setFortschritt] = useState({ thumbs: '', webp: '' })
// Die Ordnerliste brauchen zwei Karten hier — geholt wird sie einmal.
const [ordnerDaten, setOrdnerDaten] = useState<Folders | null>(null)
useEffect(() => {
apiFolders.folders().then(setOrdnerDaten).catch(() => {
/* die Auswahl bleibt dann bei "(überall)" — kein Grund zu lärmen */
})
}, [])
const [verwaiste, setVerwaiste] = useState<Verwaiste | null>(null)
const [sucht, setSucht] = useState(false)
const [handelt, setHandelt] = useState(false)
@@ -136,6 +146,10 @@ export function Speicher() {
</p>
</Karte>
<FehlendeKarte />
<ErsetzenKarte ordnerDaten={ordnerDaten} fertig={() => setOrdnerDaten(null)} />
<Karte titel="Wartung">
<div className="flex flex-wrap items-center gap-3">
<Knopf
+13
View File
@@ -16,6 +16,7 @@ export function Tokens() {
const [darfLoeschen, setDarfLoeschen] = useState(false)
const [maxMb, setMaxMb] = useState('')
const [quotaMb, setQuotaMb] = useState('')
const [maxKante, setMaxKante] = useState('')
const [gueltigTage, setGueltigTage] = useState('')
const [arten, setArten] = useState<string[]>([])
const [legtAn, setLegtAn] = useState(false)
@@ -56,6 +57,7 @@ export function Tokens() {
canDelete: darfLoeschen,
maxMb: maxMb.trim(),
quotaMb: quotaMb.trim(),
maxKante: maxKante.trim(),
arten,
gueltigTage: gueltigTage.trim(),
}),
@@ -125,6 +127,16 @@ export function Tokens() {
hinweis="Die Grenze links gilt je Datei. Diese hier gilt für alles zusammen — ohne sie kann ein Token die Platte füllen, es braucht nur genug kleine Dateien."
onChange={(e) => setQuotaMb(e.target.value)}
/>
<Feld
label="Bilder verkleinern auf (Pixel, optional)"
type="number"
min={16}
step="64"
value={maxKante}
placeholder="leer = ablegen wie es kommt"
hinweis="Größte Kantenlänge. Kleinere Bilder werden NICHT aufgeblasen. Spart Platz und Ladezeit — als Schutz braucht es das nicht."
onChange={(e) => setMaxKante(e.target.value)}
/>
<Feld
label="Gültig für (Tage, optional)"
type="number"
@@ -273,6 +285,7 @@ export function Tokens() {
t.quota_bytes
? `${bytes(t.bytes)} von ${bytes(t.quota_bytes)}`
: null,
t.max_kante ? `max ${t.max_kante} px` : null,
t.arten ? t.arten.replaceAll(',', ', ') : null,
t.expires_at
? `bis ${zeit(t.expires_at).slice(0, 10)}`