feat: Das Logo laesst sich zuschneiden

Ein Screenshot von einer Website ist selten schon das Logo – da ist Rand
drumherum oder drei Marken nebeneinander. Nach dem Waehlen kommt deshalb ein
Rahmen: ziehen zum Verschieben, Regler zum Zoomen.

Der Rahmen IST der Ausschnitt. Deshalb nichts abgedunkelt und keine
Hilfslinien darin – man soll das Ergebnis sehen und nicht eine Vorschau
davon, die man erst uebersetzen muss.

Zu Beginn steht das GANZE Bild im Rahmen. Wer nicht zuschneiden will, drueckt
Uebernehmen und bekommt, was er hochgeladen hat; Zuschneiden ist ein Zoom,
kein Zwang. Verschieben geht nur, solange das Bild den Rahmen ueberragt –
sonst schoebe man ein kleines Bild aus seinem eigenen Ausschnitt heraus.

Geschnitten wird im Browser, hochgeladen nur noch der Ausschnitt: Der Server
bekommt kein Bild, das er gar nicht behalten soll.

Der durchsichtige Grund bleibt durchsichtig – ein weisser Kasten fiele auf
dunklem Hintergrund sofort auf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-07 19:50:16 +02:00
co-authored by Claude Opus 5
parent dd2c33725f
commit 9b9ce77267
5 changed files with 259 additions and 5 deletions
+21 -3
View File
@@ -12,6 +12,7 @@ import CategorySelect from "../components/CategorySelect";
import AccountSelect from "../components/AccountSelect";
import { gruppenFuer } from "../kategorien";
import CurrencySelect from "../components/CurrencySelect";
import LogoZuschnitt from "../components/LogoZuschnitt";
import { centsToInput, formatDate, formatMoney, parseAmountToCents } from "../format";
// Muss zu INTERVALL_MONATE im Server passen.
@@ -50,6 +51,8 @@ export default function Abos() {
const [form, setForm] = useState(null); // Formularzustand oder null
const [editing, setEditing] = useState(null);
const [busy, setBusy] = useState(false);
// Die gewaehlte Datei, solange der Ausschnitt noch nicht bestaetigt ist.
const [zuschnitt, setZuschnitt] = useState(null);
async function laden() {
try {
@@ -136,8 +139,15 @@ export default function Abos() {
}
}
async function logoWaehlen(datei) {
if (!datei || !editing) return;
// Nicht sofort hochladen: Erst waehlt man den Ausschnitt. Was danach zum
// Server geht, ist genau das, was im Rahmen stand.
function logoWaehlen(datei) {
if (datei && editing) setZuschnitt(datei);
}
async function logoHochladen(datei) {
setZuschnitt(null);
if (!editing) return;
setBusy(true);
try {
const aktualisiert = await api.uploadSubscriptionLogo(editing.id, datei);
@@ -476,7 +486,7 @@ export default function Abos() {
<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])} />
onChange={(e) => { logoWaehlen(e.target.files?.[0]); e.target.value = ""; }} />
</label>
{editing?.has_logo && (
<button type="button" className="btn sm ghost" onClick={logoEntfernen}>
@@ -531,6 +541,14 @@ export default function Abos() {
</form>
</Modal>
)}
{zuschnitt && (
<LogoZuschnitt
datei={zuschnitt}
onAbbrechen={() => setZuschnitt(null)}
onFertig={logoHochladen}
/>
)}
</>
);
}