Ein eigenes Bild bringt auch seinen Ausschnitt mit

Hochladen konnte man Plakat, Hintergrund und Schriftzug schon lange — der
Knopf stand da, der Endpunkt nahm an. Nur ankommen tat nichts: die Ablage
baute einen Schlüssel, den sie selbst nicht wiederfand (heute früh
repariert). Was blieb, waren drei Lücken, die man erst sieht, wenn der Weg
überhaupt funktioniert.

**Der Ausschnitt.** Ein Plakat steht hochkant im Verhältnis 2∶3, ein
Hintergrund liegt quer. Was man aussucht, passt selten von selbst — und
den Rest beschnitt bisher der Browser beim Anzeigen: mittig. Jetzt wählt
man selbst, zieht und vergrössert, und hochgeladen wird genau dieser
Ausschnitt: 600×900 beim Plakat, 1920×1080 beim Hintergrund. Gerechnet
wird im Browser; das spart die Leitung und dem Server eine Bildbibliothek.

**Der Schriftzug nicht.** Er ist ein PNG mit durchsichtigem Grund. Durch
dieselbe Mühle geschickt würde aus der Durchsicht ein weisses Rechteck —
ein Aufkleber über dem Bild. Er geht unverändert hinaus, Byte für Byte.

**Die Kachel merkte nichts.** Nach dem Hochladen fehlte der Anstoss nach
aussen; die Oberfläche zeigte weiter das alte Bild, bis man sie neu lud.

**Und zurück ging es gar nicht.** „Von Hand gesetzt" heisst auch: beim
Auffrischen bleibt es stehen. Ohne Rückweg wäre ein Fehlgriff ein
Dauerzustand — jetzt steht „Wieder automatisch" daneben, sobald etwas
festgesetzt ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-06 19:41:04 +02:00
co-authored by Claude Opus 5
parent 327e2e9195
commit 6dc7be9822
5 changed files with 137 additions and 40 deletions
+73 -10
View File
@@ -1,14 +1,28 @@
import { bildAdresse, bildauswahl, bildSetzen, bildHochladen, type Bildkandidat } from './api';
import { knopf, fehlerzeile, meldung } from './util';
import {
bildAdresse, bildauswahl, bildSetzen, bildHochladen, bildFreigeben, type Bildkandidat,
} from './api';
import { knopf, fehlerzeile, meldung, hinweisZeigen } from './util';
import { icons } from './icons';
import { beimSeitenwechselSchliessen } from './dialog';
import { zuschneiden } from './bildzuschnitt';
type Bildart = 'poster' | 'backdrop' | 'logo';
const ARTEN: { art: Bildart; titel: string; verhaeltnis: string }[] = [
{ art: 'poster', titel: 'Plakat', verhaeltnis: '2 / 3' },
{ art: 'backdrop', titel: 'Hintergrund', verhaeltnis: '16 / 9' },
{ art: 'logo', titel: 'Schriftzug', verhaeltnis: '16 / 9' },
/** Was jede Bildart ist — und in welcher Form sie hochgeladen wird.
*
* **`zuschnitt: null` beim Schriftzug, und das ist kein Versehen.** Ein
* Schriftzug ist ein PNG mit durchsichtigem Grund; durch die Zuschneidemühle
* geschickt würde aus der Durchsicht ein weisses Rechteck, und er läge wie
* ein Aufkleber über dem Bild. Er geht unverändert hinaus. */
const ARTEN: {
art: Bildart; titel: string; verhaeltnis: string;
zuschnitt: { verhaeltnis: number; breite: number } | null;
}[] = [
{ art: 'poster', titel: 'Plakat', verhaeltnis: '2 / 3',
zuschnitt: { verhaeltnis: 2 / 3, breite: 600 } },
{ art: 'backdrop', titel: 'Hintergrund', verhaeltnis: '16 / 9',
zuschnitt: { verhaeltnis: 16 / 9, breite: 1920 } },
{ art: 'logo', titel: 'Schriftzug', verhaeltnis: '16 / 9', zuschnitt: null },
];
/** Plakat, Hintergrund und Schriftzug von Hand wählen.
@@ -122,10 +136,13 @@ export function bilderflaeche(
const bloecke = new Map<Bildart, HTMLElement>();
ARTEN.forEach(({ art: bildart, titel: name, verhaeltnis }) => {
ARTEN.forEach(({ art: bildart, titel: name, verhaeltnis, zuschnitt }) => {
const aktuell = bildart === 'poster' ? auswahl.current.poster
: bildart === 'backdrop' ? auswahl.current.backdrop
: auswahl.current.logo;
const festgesetzt = bildart === 'poster' ? auswahl.current.posterPinned
: bildart === 'backdrop' ? auswahl.current.backdropPinned
: auswahl.current.logoPinned;
const passende = auswahl.candidates.filter(k => k.kind === bildart);
@@ -148,7 +165,8 @@ export function bilderflaeche(
const kopfzeile = document.createElement('div');
kopfzeile.className = 'knopfreihe';
const u = document.createElement('h3');
u.textContent = aktuell ? `${name} — eines ist gesetzt` : name;
u.textContent = festgesetzt ? `${name} — von Hand gesetzt`
: aktuell ? `${name} — eines ist gesetzt` : name;
kopfzeile.appendChild(u);
// Eigenes Bild — ein verstecktes Dateifeld hinter einem Knopf, weil
@@ -162,15 +180,60 @@ export function bilderflaeche(
eigenes.onclick = () => feld.click();
feld.onchange = async () => {
const datei = feld.files?.[0];
feld.value = ''; // Dieselbe Datei zweimal soll wieder auslösen.
if (!datei) return;
// **Erst zuschneiden, dann hochladen** — ein Plakat steht hochkant,
// ein Hintergrund liegt quer. Was nicht passt, beschnitt sonst der
// Browser beim Anzeigen: mittig, und das trifft selten.
let inhalt: Blob = datei;
if (zuschnitt) {
const ausschnitt = await zuschneiden(datei, {
...zuschnitt, titel: `${name} zuschneiden`,
});
if (!ausschnitt) return; // Abgebrochen — das ist keine Meldung wert.
inhalt = ausschnitt;
}
eigenes.disabled = true;
try { await bildHochladen(art, id, bildart, datei); await laden(); }
catch (fehler) {
try {
await bildHochladen(art, id, bildart,
new File([inhalt], zuschnitt ? 'bild.jpg' : datei.name,
{ type: zuschnitt ? 'image/jpeg' : datei.type }));
// **Auch das zählt als Änderung.** Hier stand nichts, und die
// Kachel zeigte danach das alte Bild weiter — bis man die ganze
// Oberfläche neu lud.
beiAenderung?.();
hinweisZeigen(`${name} gesetzt.`);
await laden();
} catch (fehler) {
abschnitt.appendChild(fehlerzeile(meldung(fehler)));
eigenes.disabled = false;
}
};
kopfzeile.append(eigenes, feld);
// **Nur, wenn es etwas zurückzunehmen gibt.** „Von Hand gesetzt" heisst
// auch: beim Auffrischen bleibt es stehen. Ohne diesen Knopf wäre ein
// Fehlgriff ein Dauerzustand.
if (festgesetzt) {
const frei = knopf('Wieder automatisch', 'btn');
frei.type = 'button';
frei.onclick = async () => {
frei.disabled = true;
try {
await bildFreigeben(art, id, bildart);
beiAenderung?.();
hinweisZeigen('Kommt beim nächsten Auffrischen wieder aus der Quelle.');
await laden();
} catch (fehler) {
abschnitt.appendChild(fehlerzeile(meldung(fehler)));
frei.disabled = false;
}
};
kopfzeile.appendChild(frei);
}
abschnitt.appendChild(kopfzeile);
if (passende.length === 0 && !aktuell) {