feat: Medien im Fenster ansehen, und eine Auffrischung, die nichts wegzieht
ANSEHEN OHNE NEUEN TAB. Klick auf eine Kachel oeffnet den Betrachter: Bilder, Videos, Ton und PDF direkt, alles andere mit ehrlichem Hinweis und Herunterladen-Knopf -- ein Betrachter, der bei einer .zip ein schwarzes Feld zeigt, sieht aus wie ein kaputter Betrachter. Pfeiltasten blaettern, Escape schliesst; Adresse kopieren, neuer Tab und Loeschen sitzen in der Kopfzeile. Gezeigt wird IMMER die Vollfassung, nie die Vorschau: 320 Pixel reichen fuer eine Kachel, aber nicht dafuer, ein Bild zu beurteilen -- und genau dazu klickt man darauf. Damit musste die Auswahl umziehen: der Klick auf die Kachel oeffnet jetzt, das Kaestchen oben links waehlt aus. Es erscheint beim Darueberfahren und bleibt sichtbar, solange etwas gewaehlt ist. AUTOMATISCH AUFFRISCHEN, aber nicht stumpf. Ein billiger Fuehler (/api/dash/stand: drei Zahlen, GEMESSEN 6 ms je Abfrage ueber TLS und Proxy) wird alle fuenf Sekunden gefragt; erst wenn er sich aendert, wird richtig nachgeladen. Ein Tab im Hintergrund fragt gar nicht. Die dritte Zahl ist kein Zierrat: nach einem LOESCHEN gehen Anzahl und neuester Zeitstempel auf die alten Werte zurueck -- nur die Ereignis-ID zaehlt weiter. Ohne sie bliebe eine geloeschte Datei in der Galerie stehen. Nachgemessen. UND ES ZIEHT NICHTS WEG: wer etwas ausgewaehlt hat, ein Bild offen hat oder geblaettert ist, bekommt keinen Neuaufbau unter den Haenden, sondern einen Knopf "Neues da -- laden". Eine Liste, die sich beim Klicken selbst neu sortiert, ist der Grund, warum Leute Auto-Refresh abschalten. Der Schalter merkt sich seinen Zustand. Kein SSE, und das mit Absicht: dafuer braeuchte NPM zusaetzlich `proxy_buffering off` -- ein weiterer Handgriff im Proxy, der vergessen werden kann und dann zu einer Verbindung fuehrt, die scheinbar haengt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
FileQuestion,
|
||||
ImageOff,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Trash2,
|
||||
X,
|
||||
@@ -18,6 +19,8 @@ import { bytes, istBild, zeit } from '../format'
|
||||
import { Knopf, Leer, Nachfrage, useKopieren } from '../components/Bausteine'
|
||||
import { Ordner } from '../components/Ordner'
|
||||
import { Hochladen } from '../components/Hochladen'
|
||||
import { Ansehen } from '../components/Ansehen'
|
||||
import { useAuffrischen } from '../auffrischen'
|
||||
|
||||
const PRO_SEITE = 60
|
||||
|
||||
@@ -32,6 +35,9 @@ export function Galerie() {
|
||||
const [gewaehlt, setGewaehlt] = useState<Set<number>>(new Set())
|
||||
const [loeschen, setLoeschen] = useState<MediaItem[] | null>(null)
|
||||
const [loescht, setLoescht] = useState(false)
|
||||
// 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<number | null>(null)
|
||||
const { kopiert, kopieren } = useKopieren()
|
||||
|
||||
// Die Suche wartet einen Moment ab. Ohne das schickt jeder Tastendruck
|
||||
@@ -86,6 +92,18 @@ export function Galerie() {
|
||||
void ordnerLaden()
|
||||
}, [ordnerLaden])
|
||||
|
||||
const allesLaden = useCallback(() => {
|
||||
void laden()
|
||||
void ordnerLaden()
|
||||
}, [laden, ordnerLaden])
|
||||
|
||||
// Blockiert, solange eine Auswahl steht oder ein Bild offen ist: dann gibt
|
||||
// es nur den Hinweis und keinen Neuaufbau unter den Haenden.
|
||||
const frisch = useAuffrischen({
|
||||
laden: allesLaden,
|
||||
blockiert: gewaehlt.size > 0 || ansehen !== null || loeschen !== null,
|
||||
})
|
||||
|
||||
const items = daten?.items ?? []
|
||||
const gesamt = daten?.total ?? 0
|
||||
const seiten = Math.max(1, Math.ceil(gesamt / PRO_SEITE))
|
||||
@@ -137,6 +155,7 @@ export function Galerie() {
|
||||
setGewaehlt(new Set())
|
||||
void laden()
|
||||
void ordnerLaden()
|
||||
void frisch.merken()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +214,32 @@ export function Galerie() {
|
||||
)}
|
||||
</span>
|
||||
|
||||
{frisch.wartet && (
|
||||
<Knopf
|
||||
art="haupt"
|
||||
onClick={frisch.jetztLaden}
|
||||
kind={
|
||||
<>
|
||||
<RefreshCw size={14} />
|
||||
Neues da — laden
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<label
|
||||
className="flex cursor-pointer items-center gap-1.5 text-xs text-schrift-leise"
|
||||
title="Alle fünf Sekunden nachsehen, ob sich etwas geändert hat"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={frisch.an}
|
||||
onChange={(e) => frisch.setAn(e.target.checked)}
|
||||
className="accent-akzent"
|
||||
/>
|
||||
automatisch
|
||||
</label>
|
||||
|
||||
{items.length > 0 && (
|
||||
<Knopf
|
||||
onClick={alleUmschalten}
|
||||
@@ -221,6 +266,7 @@ export function Galerie() {
|
||||
fertig={() => {
|
||||
void laden()
|
||||
void ordnerLaden()
|
||||
void frisch.merken()
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -246,11 +292,15 @@ export function Galerie() {
|
||||
className={`group relative overflow-hidden rounded-lg border bg-flaeche
|
||||
transition-colors ${an ? 'border-akzent' : 'border-rand'}`}
|
||||
>
|
||||
{/* Klick auf die Kachel ÖFFNET, Klick auf das
|
||||
Kästchen WÄHLT AUS. Vorher tat der Klick
|
||||
beides nicht gleichzeitig — er wählte nur
|
||||
aus, und zum Ansehen musste man einen
|
||||
neuen Tab aufmachen. */}
|
||||
<button
|
||||
onClick={() => umschalten(item.id)}
|
||||
onClick={() => setAnsehen(item.id)}
|
||||
className="block w-full"
|
||||
aria-pressed={an}
|
||||
aria-label={`${item.path} auswählen`}
|
||||
aria-label={`${item.path} ansehen`}
|
||||
>
|
||||
<div className="flex aspect-square items-center justify-center bg-grund">
|
||||
{istBild(item.mime) ? (
|
||||
@@ -282,11 +332,25 @@ export function Galerie() {
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{an && (
|
||||
<span className="pointer-events-none absolute top-2 left-2 rounded bg-akzent p-1 text-grund">
|
||||
<Check size={12} />
|
||||
</span>
|
||||
)}
|
||||
{/* Immer sichtbar, wenn gewählt; sonst erst
|
||||
beim Darüberfahren. Ein Kästchen, das man
|
||||
erst suchen muss, ist keines — eines, das
|
||||
bei 60 Kacheln immer leuchtet, ist Lärm. */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
umschalten(item.id)
|
||||
}}
|
||||
aria-pressed={an}
|
||||
aria-label={`${item.path} auswählen`}
|
||||
className={`absolute top-2 left-2 rounded border p-1 transition-opacity ${
|
||||
an
|
||||
? 'border-akzent bg-akzent text-grund opacity-100'
|
||||
: 'border-rand bg-grund/80 text-schrift-leise opacity-0 group-hover:opacity-100 hover:text-schrift focus-visible:opacity-100'
|
||||
}`}
|
||||
>
|
||||
<Check size={12} />
|
||||
</button>
|
||||
|
||||
<figcaption className="border-t border-rand p-2">
|
||||
<p
|
||||
@@ -365,6 +429,30 @@ export function Galerie() {
|
||||
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
if (ansehen === null) return null
|
||||
const i = items.findIndex((x) => x.id === ansehen)
|
||||
// Die Datei kann zwischenzeitlich verschwunden sein -- etwa
|
||||
// weil daneben geloescht wurde. Dann schliesst sich der
|
||||
// Betrachter, statt auf ein Loch zu zeigen.
|
||||
if (i === -1) return null
|
||||
const item = items[i]!
|
||||
return (
|
||||
<Ansehen
|
||||
item={item}
|
||||
hatVorher={i > 0}
|
||||
hatNachher={i < items.length - 1}
|
||||
onVorher={() => setAnsehen(items[i - 1]!.id)}
|
||||
onNachher={() => setAnsehen(items[i + 1]!.id)}
|
||||
onSchliessen={() => setAnsehen(null)}
|
||||
onLoeschen={() => {
|
||||
setAnsehen(null)
|
||||
setLoeschen([item])
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
|
||||
<Nachfrage
|
||||
offen={loeschen !== null}
|
||||
titel={loeschen?.length === 1 ? 'Datei löschen?' : `${loeschen?.length} Dateien löschen?`}
|
||||
|
||||
Reference in New Issue
Block a user