feat: Ein Markenlogo je Vertrag
Am Logo findet man den Vertrag schneller als am Namen: Netflix erkennt man am roten N, nicht am Wort – und erst recht nicht an einem Zahnrad aus der Symbolauswahl. Die bleibt als Rueckfall fuer alles, wozu es kein Logo gibt. Hochgeladen wird, was das Telefon hergibt; gespeichert ein PNG von hoechstens 128 Pixeln Kantenlaenge. Aus drei Megabyte werden ein paar Kilobyte, und das Seitenverhaeltnis bleibt – ein verzerrtes Logo sieht falscher aus als ein kleines. Immer PNG, egal was hereinkam. Das erspart der Anzeige jede Fallunterscheidung und schliesst SVG aus: Eine SVG-Datei kann Skript enthalten, und ein Bild, das der Nutzer hochlaedt und der Browser ausfuehrt, ist genau die Art Falle, die man sich nicht in ein Finanzprogramm legt. Eigene Tabelle, nicht eine Spalte an den Vertraegen: Sonst zoege jede Listenabfrage saemtliche Bilder mit heraus, obwohl die Liste sie einzeln nachlaedt. ZWEI FEHLER IM SICHERN, die dabei aufgefallen sind: Binaeres liess sich gar nicht sichern – die Ausgabe scheiterte an den Bytes, und zwar erst beim Schreiben, also nachdem der Nutzer glaubt, ein Backup zu haben. Jetzt Base64. Und "Ergaenzen" legte Zeilen von Tabellen mit natuerlichem Schluessel ein zweites Mal an, statt sie wiederzuerkennen. Beim Logo kippte damit die ganze Wiederherstellung. Betraf bisher keine Tabelle, weil noch keine solche einen Vergleichsschluessel hatte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0a790fd45a
commit
dd2c33725f
+62
-1
@@ -136,6 +136,31 @@ export default function Abos() {
|
||||
}
|
||||
}
|
||||
|
||||
async function logoWaehlen(datei) {
|
||||
if (!datei || !editing) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const aktualisiert = await api.uploadSubscriptionLogo(editing.id, datei);
|
||||
// Auch den offenen Dialog nachziehen, sonst zeigt er weiter das Symbol.
|
||||
setEditing(aktualisiert);
|
||||
laden();
|
||||
} catch (e) {
|
||||
toast(e.message, "error");
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function logoEntfernen() {
|
||||
if (!editing) return;
|
||||
try {
|
||||
setEditing(await api.deleteSubscriptionLogo(editing.id));
|
||||
laden();
|
||||
} catch (e) {
|
||||
toast(e.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function loeschen(s) {
|
||||
const ok = await confirm({
|
||||
title: t("abos.delete"),
|
||||
@@ -176,7 +201,14 @@ export default function Abos() {
|
||||
const ueberfaellig = s.tage_bis_faellig < 0 ? " warn" : "";
|
||||
return (
|
||||
<div className={"abo-zeile" + beendet}>
|
||||
<IconChip name={s.icon} color={s.color} size={30} />
|
||||
{/* Das Logo schlaegt das Symbol: Netflix erkennt man am roten N,
|
||||
nicht am Wort – und erst recht nicht an einem Zahnrad. */}
|
||||
{s.has_logo ? (
|
||||
<img className="abo-logo" alt=""
|
||||
src={api.subscriptionLogoUrl(s.id, s.logo_version)} />
|
||||
) : (
|
||||
<IconChip name={s.icon} color={s.color} size={30} />
|
||||
)}
|
||||
<div className="abo-mitte">
|
||||
<div className="abo-kopf">
|
||||
<strong className="ellipsis">{s.name}</strong>
|
||||
@@ -428,6 +460,35 @@ export default function Abos() {
|
||||
</label>
|
||||
<p className="muted small">{t("abos.patternHint")}</p>
|
||||
|
||||
<label style={{ marginBottom: 6 }}>
|
||||
<span>{t("abos.logo")}</span>
|
||||
</label>
|
||||
<div className="abo-logo-feld">
|
||||
{editing?.has_logo ? (
|
||||
<img className="abo-logo gross" alt=""
|
||||
src={api.subscriptionLogoUrl(editing.id, editing.logo_version)} />
|
||||
) : (
|
||||
<IconChip name={form.icon} color={form.color} size={48} />
|
||||
)}
|
||||
<div className="abo-logo-knoepfe">
|
||||
{/* Ohne gespeicherten Vertrag gibt es noch keine Nummer, an die
|
||||
das Bild gehoert. Erst speichern, dann Logo. */}
|
||||
<label className={"btn sm" + (editing ? "" : " deaktiviert")}>
|
||||
<Icon name="upload" size={14} /> {t("abos.logoUpload")}
|
||||
<input type="file" accept="image/*" hidden disabled={!editing}
|
||||
onChange={(e) => logoWaehlen(e.target.files?.[0])} />
|
||||
</label>
|
||||
{editing?.has_logo && (
|
||||
<button type="button" className="btn sm ghost" onClick={logoEntfernen}>
|
||||
{t("abos.logoRemove")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="muted small">
|
||||
{editing ? t("abos.logoHint") : t("abos.logoAfterSave")}
|
||||
</p>
|
||||
|
||||
<label>
|
||||
<span>{t("accounts.icon")}</span>
|
||||
<IconPicker
|
||||
|
||||
Reference in New Issue
Block a user