fix(nextcloud-status): grosse Logos beim Hochladen verkleinern, Doppelklick auf Jetzt pruefen sperren
Tessera CI/CD / Lint & Type Check (push) Successful in 56s
Tessera CI/CD / Tests (push) Successful in 2m5s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 21s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m30s

Ein 2346-px-Logo liess die Linux-App beim Neuzeichnen (drehende Lade-Symbole)
kurz einfrieren; Logos werden jetzt im Browser auf max. 256 px verkleinert.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-02 23:42:10 +02:00
parent 0afcf237e8
commit 19340fcebd
6 changed files with 171 additions and 13 deletions
+79
View File
@@ -0,0 +1,79 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { shrinkImage } from './shrink-image';
function file(type: string, size = 10): File {
return new File([new Uint8Array(size)], `logo.${type.split('/')[1]}`, { type });
}
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
describe('shrinkImage', () => {
it('laesst GIFs unveraendert (koennen animiert sein)', async () => {
const gif = file('image/gif');
expect(await shrinkImage(gif)).toBe(gif);
});
it('gibt ohne createImageBitmap das Original zurueck', async () => {
vi.stubGlobal('createImageBitmap', undefined);
const png = file('image/png');
expect(await shrinkImage(png)).toBe(png);
});
it('laesst kleine Bilder unveraendert', async () => {
const close = vi.fn();
vi.stubGlobal(
'createImageBitmap',
vi.fn(async () => ({ width: 200, height: 100, close })),
);
const png = file('image/png');
expect(await shrinkImage(png)).toBe(png);
expect(close).toHaveBeenCalled();
});
it('verkleinert grosse Bilder auf hoechstens 256 px an der laengsten Kante', async () => {
vi.stubGlobal(
'createImageBitmap',
vi.fn(async () => ({ width: 2346, height: 1173, close: vi.fn() })),
);
const drawImage = vi.fn();
const canvas = {
width: 0,
height: 0,
getContext: () => ({ drawImage, imageSmoothingQuality: 'low' }),
toBlob: (cb: (b: Blob) => void) => cb(new Blob([new Uint8Array(5)], { type: 'image/png' })),
};
const orig = document.createElement.bind(document);
vi.spyOn(document, 'createElement').mockImplementation((tag: string) =>
tag === 'canvas' ? (canvas as unknown as HTMLCanvasElement) : orig(tag),
);
const big = file('image/jpeg', 50_000);
const out = await shrinkImage(big);
expect(canvas.width).toBe(256);
expect(canvas.height).toBe(128);
expect(out.type).toBe('image/png');
expect(out.name).toBe('logo.png');
expect(out.size).toBe(5);
});
it('behaelt das Original, wenn das Ergebnis nicht kleiner wird', async () => {
vi.stubGlobal(
'createImageBitmap',
vi.fn(async () => ({ width: 1000, height: 1000, close: vi.fn() })),
);
const canvas = {
width: 0,
height: 0,
getContext: () => ({ drawImage: vi.fn() }),
toBlob: (cb: (b: Blob) => void) => cb(new Blob([new Uint8Array(100)])),
};
const orig = document.createElement.bind(document);
vi.spyOn(document, 'createElement').mockImplementation((tag: string) =>
tag === 'canvas' ? (canvas as unknown as HTMLCanvasElement) : orig(tag),
);
const small = file('image/png', 50);
expect(await shrinkImage(small)).toBe(small);
});
});
+42
View File
@@ -0,0 +1,42 @@
/**
* Verkleinert ein hochgeladenes Bild im Browser, bevor es zum Server geht
* (quick-261002). Logos werden nur etwa 40 Pixel hoch angezeigt; ein
* 2000-Pixel-Bild muss die Linux-Webview bei jedem Neuzeichnen (z. B. drehende
* Lade-Symbole) herunterrechnen, was die App kurz einfrieren laesst.
*
* - Bilder, die schon klein genug sind, und GIFs (evtl. animiert) bleiben unveraendert.
* - Ergebnis als PNG (Transparenz bleibt), Seitenverhaeltnis bleibt.
* - Ohne Canvas/`createImageBitmap` (alte Umgebung, Tests) oder bei Fehlern: Original.
*/
export const LOGO_MAX_EDGE = 256;
export async function shrinkImage(file: File, maxEdge = LOGO_MAX_EDGE): Promise<File> {
if (file.type === 'image/gif') return file;
if (typeof createImageBitmap !== 'function' || typeof document === 'undefined') return file;
try {
const bitmap = await createImageBitmap(file);
const { width, height } = bitmap;
if (width <= maxEdge && height <= maxEdge) {
bitmap.close();
return file;
}
const scale = maxEdge / Math.max(width, height);
const canvas = document.createElement('canvas');
canvas.width = Math.max(1, Math.round(width * scale));
canvas.height = Math.max(1, Math.round(height * scale));
const ctx = canvas.getContext('2d');
if (!ctx) {
bitmap.close();
return file;
}
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
bitmap.close();
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/png'));
if (!blob || blob.size >= file.size) return file;
const name = file.name.replace(/\.[^.]+$/, '') + '.png';
return new File([blob], name, { type: 'image/png' });
} catch {
return file;
}
}