release: v0.20.1
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 …',
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -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…',
|
||||
|
||||
@@ -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();
|
||||
|
||||
24
lib/zoom.ts
24
lib/zoom.ts
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user