release: v0.20.1

This commit is contained in:
Ralksta
2026-10-04 09:49:35 +02:00
committed by GitHub
parent 5f831f719a
commit 9270546c9d
17 changed files with 189 additions and 47 deletions

View File

@@ -1,11 +1,13 @@
import { describe, it, expect } from 'vitest';
import {
CLICK_ZOOM_SCALE,
FIT_VIEW,
canZoom,
clampView,
isZoomed,
maxZoomScale,
oneToOneScale,
onFitBox,
panBy,
pinchView,
stepView,
@@ -127,7 +129,26 @@ describe('zooming around a point', () => {
expect(fit.cx + next.x - (fit.width * 6) / 2).toBeCloseTo(0);
});
it('double-click goes to 1:1 at the point, and back to fit', () => {
it('toggles to a given scale at the point, clamped to 1:1', () => {
const point = { x: 650, y: 420 };
const closer = toggleView(FIT_VIEW, point, fit, viewport, max, CLICK_ZOOM_SCALE);
expect(closer.scale).toBe(1.5);
const after = screenOf(closer, point.x - fit.cx, point.y - fit.cy);
expect(after.x).toBeCloseTo(point.x);
expect(after.y).toBeCloseTo(point.y);
expect(toggleView(closer, point, fit, viewport, max, CLICK_ZOOM_SCALE)).toEqual(FIT_VIEW);
// A photo with less to give than the click asks for stops at 1:1.
expect(toggleView(FIT_VIEW, point, fit, viewport, 1.2, CLICK_ZOOM_SCALE).scale).toBe(1.2);
});
it('knows the photo from the band beside it', () => {
expect(onFitBox({ x: fit.cx, y: fit.cy }, fit)).toBe(true);
expect(onFitBox({ x: fit.cx + fit.width / 2, y: fit.cy }, fit)).toBe(true);
expect(onFitBox({ x: fit.cx + fit.width / 2 + 1, y: fit.cy }, fit)).toBe(false);
expect(onFitBox({ x: fit.cx, y: fit.cy - fit.height / 2 - 1 }, fit)).toBe(false);
});
it('double-tap and the button go to 1:1 at the point, and back to fit', () => {
const point = { x: 650, y: 420 };
const zoomed = toggleView(FIT_VIEW, point, fit, viewport, max);
expect(zoomed.scale).toBe(max);

View File

@@ -187,7 +187,7 @@ export const de: Dictionary = {
shortcutList: 'Diese Liste',
shortcutClose: 'Ansicht schließen',
zoomIn: 'Auf volle Auflösung zoomen',
zoomInTitle: 'Auf volle Auflösung zoomen (+ oder Doppelklick)',
zoomInTitle: 'Auf volle Auflösung zoomen (+)',
zoomOut: 'Foto an den Bildschirm anpassen',
zoomOutTitle: 'Einpassen (0 oder Esc)',
zoomLoading: 'Volle Auflösung wird geladen …',

View File

@@ -191,7 +191,7 @@ export const en = {
shortcutClose: 'Close the viewer',
/** The zoom button (#467): to 1:1 from fit, and back. */
zoomIn: 'Zoom to full resolution',
zoomInTitle: 'Zoom to full resolution (+ or double-click)',
zoomInTitle: 'Zoom to full resolution (+)',
zoomOut: 'Fit the photo to the screen',
zoomOutTitle: 'Fit to screen (0 or Esc)',
zoomLoading: 'Loading full resolution…',

View File

@@ -187,7 +187,7 @@ export const es: Dictionary = {
shortcutList: 'Esta lista',
shortcutClose: 'Cerrar el visor',
zoomIn: 'Ampliar a resolución completa',
zoomInTitle: 'Ampliar a resolución completa (+ o doble clic)',
zoomInTitle: 'Ampliar a resolución completa (+)',
zoomOut: 'Ajustar la foto a la pantalla',
zoomOutTitle: 'Ajustar a la pantalla (0 o Esc)',
zoomLoading: 'Cargando la resolución completa…',

View File

@@ -190,7 +190,7 @@ export const fr: Dictionary = {
shortcutList: 'Cette liste',
shortcutClose: 'Fermer la visionneuse',
zoomIn: 'Zoomer en pleine résolution',
zoomInTitle: 'Zoomer en pleine résolution (+ ou double-clic)',
zoomInTitle: 'Zoomer en pleine résolution (+)',
zoomOut: 'Ajuster la photo à l’écran',
zoomOutTitle: 'Ajuster à l’écran (0 ou Échap)',
zoomLoading: 'Chargement de la pleine résolution…',

View File

@@ -187,7 +187,7 @@ export const it: Dictionary = {
shortcutList: 'Questo elenco',
shortcutClose: 'Chiudi il visualizzatore',
zoomIn: 'Ingrandisci alla risoluzione piena',
zoomInTitle: 'Ingrandisci alla risoluzione piena (+ o doppio clic)',
zoomInTitle: 'Ingrandisci alla risoluzione piena (+)',
zoomOut: 'Adatta la foto allo schermo',
zoomOutTitle: 'Adatta allo schermo (0 o Esc)',
zoomLoading: 'Caricamento della risoluzione piena…',

View File

@@ -188,7 +188,7 @@ export const nl: Dictionary = {
shortcutList: 'Deze lijst',
shortcutClose: 'De viewer sluiten',
zoomIn: 'Inzoomen op volledige resolutie',
zoomInTitle: 'Inzoomen op volledige resolutie (+ of dubbelklik)',
zoomInTitle: 'Inzoomen op volledige resolutie (+)',
zoomOut: 'Foto aan het scherm aanpassen',
zoomOutTitle: 'Aan scherm aanpassen (0 of Esc)',
zoomLoading: 'Volledige resolutie laden…',

View File

@@ -375,7 +375,12 @@ class ImmichClient {
}
if (!res.ok) {
console.error(`[Immich] Failed to stream ${assetId}: ${res.status}`);
console.error(
`[Immich] Failed to stream ${assetId}: ${res.status}` +
(res.status === 403 && endpoint.includes('/original')
? ' (the API key probably lacks the asset.download permission)'
: ''),
);
// Neither branch below reads the body — an unconsumed one keeps its
// socket out of undici's pool until GC finalises it (#635).
await res.body?.cancel();

View File

@@ -134,7 +134,7 @@ export function clampView(
/**
* Change the scale while the photo point under `point` stays under it — the
* cursor for a wheel or a double-click, the midpoint of two fingers for a
* cursor for a wheel or a click, the midpoint of two fingers for a
* pinch. Clamping can still move it at the edges, which is the price of never
* showing an empty band.
*/
@@ -198,16 +198,34 @@ export function panBy(
return clampView({ ...view, x: view.x + dx, y: view.y + dy }, fit, viewport, maxScale);
}
/** Double-click / double-tap: fit ↔ 1:1, around the point. */
/**
* How far a mouse click zooms: half again the fit size, a closer look rather
* than the jump to 1:1, which on a large file is several times the screen.
* Ctrl + wheel goes the rest of the way.
*/
export const CLICK_ZOOM_SCALE = 1.5;
/**
* Fit ↔ zoomed, around the point: to `target` (clamped to 1:1), or to 1:1
* itself without one — the double-tap and the magnifier button.
*/
export function toggleView(
view: ZoomView,
point: Point,
fit: FitBox,
viewport: Viewport,
maxScale: number,
target: number = maxScale,
): ZoomView {
if (isZoomed(view)) return { ...FIT_VIEW };
return zoomAround(view, maxScale, point, fit, viewport, maxScale);
return zoomAround(view, target, point, fit, viewport, maxScale);
}
/** Whether a point lies on the photo at fit — not on the band beside it. */
export function onFitBox(point: Point, fit: FitBox): boolean {
return (
Math.abs(point.x - fit.cx) <= fit.width / 2 && Math.abs(point.y - fit.cy) <= fit.height / 2
);
}
/**