diff --git a/server/ui/src/components/Ordner.tsx b/server/ui/src/components/Ordner.tsx index 1dd9b43..2241626 100644 --- a/server/ui/src/components/Ordner.tsx +++ b/server/ui/src/components/Ordner.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { Check, Folder, FolderOpen, FolderPlus, Layers, PenLine, X } from 'lucide-react' +import { Check, Folder, FolderOpen, FolderPlus, Layers, PenLine, Trash2, X } from 'lucide-react' import { api, type Folders } from '../api' import { bytes } from '../format' import { run } from '../store' @@ -22,6 +22,7 @@ export function Ordner({ onWahl, onAngelegt, onUmbenennen, + onEntfernt, }: { daten: Folders | null gewaehlt: string @@ -29,12 +30,28 @@ export function Ordner({ /** Damit die Liste den neuen Ordner auch zeigt. */ onAngelegt: () => void onUmbenennen: (pfad: string) => void + /** Nach dem Entfernen muss die Liste neu geladen werden -- sonst steht + * dort ein Ordner, den es nicht mehr gibt. */ + onEntfernt: () => void }) { const [neu, setNeu] = useState(null) const [legtAn, setLegtAn] = useState(false) if (!daten) return null + async function entfernen(pfad: string) { + // Nachfragen, und dabei sagen, was NICHT passiert: der Ordner geht, + // die Dateien darin gehen nicht -- der Dienst laesst ihn ohnehin nur + // leer entfernen. + if (!confirm(`Ordner "${pfad}" entfernen? + +Er muss leer sein; Dateien darin werden nicht angetastet.`)) { + return + } + const r = await run(() => api.ordnerEntfernen(pfad), `Ordner ${pfad} entfernt.`) + if (r) onEntfernt() + } + async function anlegen() { const name = (neu ?? '').trim() if (!name) return @@ -96,6 +113,30 @@ export function Ordner({ )} + {/* ORDNER ENTFERNEN. Den Weg gab es im Dienst und in `api.ts` + laengst -- es fehlte allein der Knopf, und damit war die + Funktion fuer den, der sie sucht, nicht vorhanden. Dieselbe + Lehre wie beim "Neuer Ordner" ein paar Zeilen weiter oben. + + DIE PRUEFUNG BLEIBT BEIM DIENST: er lehnt einen Ordner mit + Inhalt ab und sagt, wieviele Dateien darin liegen. Diese Regel + hier zu wiederholen hiesse, sie zweimal zu pflegen -- und die + zweite Fassung ist irgendwann die falsche. */} + {pfad !== undefined && ( + + )} + {anzahl} diff --git a/server/ui/src/components/Verschieben.tsx b/server/ui/src/components/Verschieben.tsx index cec7534..46085a7 100644 --- a/server/ui/src/components/Verschieben.tsx +++ b/server/ui/src/components/Verschieben.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { AlertTriangle, FolderInput, Plus } from 'lucide-react' import { api, type Folders, type MediaItem } from '../api' -import { run } from '../store' +import { run, useStore } from '../store' import { Feld, Knopf } from './Bausteine' /** Dateien in einen anderen Ordner legen. @@ -12,12 +12,17 @@ import { Feld, Knopf } from './Bausteine' * Zwischenspeicher. Das ist kein Fehler, sondern der Preis vorhersagbarer * Adressen; nur muss man es vorher wissen und nicht hinterher suchen. */ export function Verschieben({ - dateien, + ids, + namen, ordner, onFertig, onAbbruch, }: { - dateien: MediaItem[] + /** ALLE Gewaehlten. */ + ids: number[] + /** Nur die, deren Pfad bekannt ist -- die der offenen Seite. Fuer die + * Anzeige, nicht fuer die Handlung. */ + namen: MediaItem[] ordner: Folders | null onFertig: () => void onAbbruch: () => void @@ -25,27 +30,56 @@ export function Verschieben({ const [ziel, setZiel] = useState('') const [neuerOrdner, setNeuerOrdner] = useState('') const [laeuft, setLaeuft] = useState(false) + const [fortschritt, setFortschritt] = useState(0) - const beispiel = dateien[0] + const beispiel = namen[0] const name = beispiel?.path.split('/').pop() ?? '' const neuerPfad = ziel ? `${ziel}/${name}` : name async function verschieben() { setLaeuft(true) - const r = await run( - () => api.verschieben(dateien.map((d) => d.id), ziel), - (x) => - x.fehlgeschlagen === 0 - ? `${x.verschoben} verschoben.` - : `${x.verschoben} verschoben, ${x.fehlgeschlagen} nicht: ` + - x.ergebnisse - .filter((e) => !e.ok) - .slice(0, 3) - .map((e) => `${e.path} (${e.error})`) - .join('; '), - ) + const { note } = useStore.getState() + + // IN HAEPPCHEN, wie beim Loeschen: der Dienst verschiebt jede Datei + // einzeln, und tausende in einer Anfrage laufen in die Zeitgrenze des + // Proxys. 200 je Runde, und der Knopf sagt, wie weit es ist. + const PORTION = 200 + let bewegt = 0 + let daneben: string[] = [] + let abbruch: string | null = null + + for (let i = 0; i < ids.length; i += PORTION) { + try { + const x = await api.verschieben(ids.slice(i, i + PORTION), ziel) + bewegt += x.verschoben + daneben.push( + ...x.ergebnisse.filter((e) => !e.ok).map((e) => `${e.path} (${e.error})`), + ) + } catch (e) { + abbruch = e instanceof Error ? e.message : 'Unbekannt' + break + } + setFortschritt(bewegt) + } + setLaeuft(false) - if (r) onFertig() + if (abbruch) { + note('fehler', `${bewegt} von ${ids.length} verschoben, dann brach es ab: ${abbruch}`) + return + } + if (daneben.length === 0) { + note('ok', `${bewegt} verschoben.`) + } else { + // Was NICHT ging, wird benannt -- ein "verschoben" ueber einer + // halben Bewegung waere die Sorte Rueckmeldung, die hier nicht + // vorkommt. + note( + 'hinweis', + `${bewegt} verschoben, ${daneben.length} nicht: ${daneben.slice(0, 3).join('; ')}` + + (daneben.length > 3 ? ` … und ${daneben.length - 3} weitere` : ''), + ) + } + onFertig() } async function ordnerAnlegen() { @@ -69,7 +103,7 @@ export function Verschieben({ >

- {dateien.length === 1 ? 'Datei verschieben' : `${dateien.length} Dateien verschieben`} + {ids.length === 1 ? 'Datei verschieben' : `${ids.length} Dateien verschieben`}

@@ -126,9 +160,9 @@ export function Verschieben({
{beispiel.path}
{neuerPfad}
- {dateien.length > 1 && ( + {ids.length > 1 && (
- … und {dateien.length - 1} weitere + … und {ids.length - 1} weitere
)}
@@ -141,7 +175,13 @@ export function Verschieben({ art="haupt" wartet={laeuft} onClick={() => void verschieben()} - kind="Verschieben" + kind={ + // Bei tausenden Dateien dauert es; Stillstand ist + // sonst von Haengen nicht zu unterscheiden. + laeuft && fortschritt > 0 + ? `${fortschritt} von ${ids.length} …` + : 'Verschieben' + } />
diff --git a/server/ui/src/pages/Galerie.tsx b/server/ui/src/pages/Galerie.tsx index ee224d2..0468d3b 100644 --- a/server/ui/src/pages/Galerie.tsx +++ b/server/ui/src/pages/Galerie.tsx @@ -74,7 +74,10 @@ export function Galerie() { // Welche Datei gerade gross zu sehen ist -- als ID und nicht als Objekt: // nach einem Neuladen der Liste zeigt sie sonst auf einen alten Datensatz. const [ansehen, setAnsehen] = useState(null) - const [verschieben, setVerschieben] = useState(null) + /** Dieselbe Trennung wie beim Loeschen -- und aus demselben Grund: hier + * stand `items.filter(...)`, also die offene Seite, und von 3565 + * gewaehlten Bildern wanderten 60. */ + const [verschieben, setVerschieben] = useState<{ ids: number[]; namen: MediaItem[] } | null>(null) const [umbenennen, setUmbenennen] = useState(null) const [ordnerUmbenennen, setOrdnerUmbenennen] = useState(null) const { kopiert, kopieren } = useKopieren() @@ -282,6 +285,7 @@ export function Galerie() { daten={ordnerDaten} gewaehlt={ordner} onAngelegt={() => void ordnerLaden()} + onEntfernt={() => void ordnerLaden()} onUmbenennen={setOrdnerUmbenennen} onWahl={(o) => { setOrdner(o) @@ -467,7 +471,12 @@ export function Galerie() { {gewaehlt.size > 0 && ( setVerschieben(items.filter((i) => gewaehlt.has(i.id)))} + onClick={() => + setVerschieben({ + ids: [...gewaehlt], + namen: items.filter((i) => gewaehlt.has(i.id)), + }) + } kind={ <> @@ -760,7 +769,8 @@ export function Galerie() { {verschieben && ( setVerschieben(null)} onFertig={() => {