diff --git a/CHANGELOG.md b/CHANGELOG.md index 55d000f..eaa639c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Releases up to and including v0.9.2 are documented in the [GitHub releases](https://github.com/ralksta/immich-folio/releases). +## [0.20.1] — 2026-10-04 + +### Changed + +- **A single click zooms the lightbox photo in by 1.5×** + ([#835](https://github.com/ralksta/immich-folio/pull/835)). The first + version of zoom needed a double-click and went straight to 1:1, which was + more than visitors wanted when they only wanted a closer look. A click now + zooms to 1.5× around the cursor and a second click returns to fit. A press + that moves more than 10 px or is held longer than half a second is a pan, so + dragging a zoomed photo no longer drops out of the zoom. Ctrl and wheel, + pinch, touch double-tap, the magnifier button (still 1:1) and `+`, `-`, `0` + and `Esc` work as before. + +### Fixed + +- **Photo zoom and originals download need the `asset.download` permission** + on the Immich API key, which the setup guide never said. Zoom into a JPEG or + AVIF, the single download and both ZIPs stream `/assets/:id/original`, and + Immich answers `403` to a key that only has `asset.read` and `asset.view`. + The README permission table (which also credited `asset.read` with + originals), the photo zoom and originals download sections of the gallery + guide and the setup wizard hint now name it, and the log says so when a + `403` hits an original. Nothing changes for a site that uses neither + feature. Reported by a user who had to widen their key after enabling zoom. + ## [0.20.0] — 2026-10-02 ### Security diff --git a/README.md b/README.md index 06d2413..18afd88 100644 --- a/README.md +++ b/README.md @@ -237,11 +237,14 @@ PROOFING_WEBHOOK_URL=https://… # notify on submitted client proofing s Create a dedicated API key in Immich under **Account Settings → API Keys**. Immich Folio only needs **read access** — it never modifies your library. -| Permission | Required | Used for | -| ------------ | -------- | -------------------------------------------------------------------- | -| `album.read` | ✅ Yes | List and fetch album metadata & photo lists | -| `asset.read` | ✅ Yes | Fetch asset metadata, EXIF data, thumbnails, previews, and originals | -| `asset.view` | ✅ Yes | Stream image/video files (thumbnail, preview, video playback) | +| Permission | Required | Used for | +| ---------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `album.read` | ✅ Yes | List and fetch album metadata & photo lists | +| `asset.read` | ✅ Yes | Fetch asset metadata, EXIF data and search results | +| `asset.view` | ✅ Yes | Stream image/video files (thumbnail, preview, video playback) | +| `asset.download` | Only for zoom and downloads | Stream originals: [photo zoom](docs/gallery-config.md#photo-zoom) on JPEG/AVIF and [originals download](docs/gallery-config.md#originals-download) | + +> **Zoom and downloads need `asset.download`.** Without it Immich answers `403` for the original file, so the zoom button reports that full resolution is unavailable and downloads fail. Leave it off if you use neither feature. > **No write permissions needed.** `album.create`, `asset.upload`, `asset.delete`, etc. can all be left **off**. @@ -347,7 +350,7 @@ own password, separate from any album passwords, and writes straight to v0.20.0 — zoom, five new presets and photos as you edited them: -- **1:1 zoom in the lightbox** — double-click, pinch or press `+` to see a photo at full resolution and judge its sharpness; off by default, switched on for the site, a page or a single album ([guide](docs/gallery-config.md#photo-zoom)) +- **1:1 zoom in the lightbox** — click, pinch, Ctrl + scroll or press `+` to see a photo at full resolution and judge its sharpness; off by default, switched on for the site, a page or a single album ([guide](docs/gallery-config.md#photo-zoom)) - **Five new theme presets** — Kunsthalle (a grey gallery wall with numbered wall labels), Ma, Cyanotype, Salon and Birch, each in dark and light; the passepartout mat of Studio, Classic and Noir is finally visible too - **Edits made in Immich show up** — crops and rotations from Immich's editor appear in the grid, the lightbox, share images and downloads - **No GPS in downloads** — original downloads and ZIPs keep camera data and colour profile but lose the coordinates and sub-city place names ([advisory](https://github.com/ralksta/immich-folio/security/advisories/GHSA-g4xf-4285-4cv4)) diff --git a/app/install/InstallWizard.tsx b/app/install/InstallWizard.tsx index c8a1a7f..f9ed629 100644 --- a/app/install/InstallWizard.tsx +++ b/app/install/InstallWizard.tsx @@ -238,7 +238,9 @@ export function InstallWizard({ initialApiUrl, setupToken }: Props) { disabled={testing} />

- Create one in Immich under Admin Settings → API Keys. + Create one in Immich under Account Settings → API Keys. It needs album.read, + asset.read and asset.view, plus asset.download if you want photo zoom or + original downloads.

diff --git a/components/__tests__/LightboxZoom.test.tsx b/components/__tests__/LightboxZoom.test.tsx index 60d7148..05c64d8 100644 --- a/components/__tests__/LightboxZoom.test.tsx +++ b/components/__tests__/LightboxZoom.test.tsx @@ -7,7 +7,7 @@ import type { PhotoItem } from '@/app/[...path]/PhotoGrid'; /** * The lightbox zoom (#467), wired up: the control, the keys, Esc order, - * double-click, swipe while zoomed, and that the full-resolution file is + * mouse click and pan, swipe while zoomed, and that the full-resolution file is * requested only when the visitor zooms. * * jsdom does no layout, so the fit box is stubbed: every image is laid out at @@ -229,14 +229,44 @@ describe('keys', () => { }); describe('gestures', () => { - it('double-click goes to 1:1 around the cursor, and back', () => { + /** A mouse press at a point, released `moveBy` px further on. */ + const press = (surface: HTMLElement, x: number, y: number, moveBy = 0) => { + const at = { pointerId: 1, pointerType: 'mouse', button: 0, clientY: y }; + fireEvent.pointerDown(surface, { ...at, clientX: x }); + if (moveBy) fireEvent.pointerMove(surface, { ...at, clientX: x + moveBy }); + fireEvent.pointerUp(surface, { ...at, clientX: x + moveBy }); + }; + + it('a click on the photo zooms in by half around the cursor, and back', () => { + render(); + const surface = preview().parentElement!; + press(surface, 600, 300); + // Fit box centre (500, 300); the cursor 100px right of it stays put at + // 1.5×, and the top edge is clamped to the 768px jsdom viewport. + expect(preview().style.transform).toBe('translate3d(-50px, 18px, 0) scale(1.5)'); + press(surface, 600, 300); + expect(preview().style.transform).toContain('scale(1)'); + }); + + it('a drag pans the zoomed photo instead of leaving the zoom', () => { + render(); + const surface = preview().parentElement!; + press(surface, 600, 300); + press(surface, 600, 300, 100); + expect(preview().style.transform).toBe('translate3d(50px, 18px, 0) scale(1.5)'); + }); + + it('a click beside the photo does not zoom', () => { + render(); + press(preview().parentElement!, 1010, 300); + expect(preview().style.transform).toBe(''); + }); + + it('the dblclick event itself no longer zooms', () => { render(); const surface = preview().parentElement!; fireEvent.doubleClick(surface, { clientX: 600, clientY: 300 }); - // Fit box centre (500, 300); the cursor 100px right of it stays put at 6×. - expect(preview().style.transform).toBe('translate3d(-500px, 0px, 0) scale(6)'); - fireEvent.doubleClick(surface, { clientX: 600, clientY: 300 }); - expect(preview().style.transform).toContain('scale(1)'); + expect(preview().style.transform).toBe(''); }); it('no swipe navigation while zoomed', () => { diff --git a/docs/gallery-config.md b/docs/gallery-config.md index 7bdc282..d21fae3 100644 --- a/docs/gallery-config.md +++ b/docs/gallery-config.md @@ -262,6 +262,9 @@ albums: download: true ``` +The API key needs the `asset.download` permission in Immich, since originals +are streamed from `/assets/:id/original`. + Once enabled, visitors get a **Download album** link in the album header (every original as a ZIP), and — where [client proofing](#client-proofing) is on — a **Download selected (.zip)** action in the export dialog that zips just the @@ -762,13 +765,15 @@ In the admin panel the switch is under **Settings › General › Portfolio Features**, and the page and album drawers in the page builder have an _Inherit / On / Off_ choice. -**Using it.** Double-click (or double-tap) zooms to 1:1 around the point -clicked and back to fit; so does the magnifier button in the lightbox bar. -Pinch, or Ctrl + scroll wheel / trackpad pinch, zooms continuously between fit -and 1:1 — never further, since enlarging past 1:1 only interpolates pixels and -makes good focus look soft. Drag (or one finger) pans; `+` / `-` step, `0` and -`Esc` go back to fit (a second `Esc` closes the viewer). Swiping to the next -photo is off while zoomed, and changing photos resets the zoom. +**Using it.** A click on the photo zooms in by half (1.5× the fit size) +around the point clicked, and a second click goes back to fit. Double-tap on a +touch screen, or the magnifier button in the lightbox bar, zooms straight to +1:1 and back. Pinch, or Ctrl + scroll wheel / trackpad pinch, zooms +continuously between fit and 1:1 — never further, since enlarging past 1:1 +only interpolates pixels and makes good focus look soft. Drag (or one finger) +pans; `+` / `-` step, `0` and `Esc` go back to fit (a second `Esc` closes the +viewer). Swiping to the next photo is off while zoomed, and changing photos +resets the zoom. **What is shown.** The full-resolution file is requested only when a visitor zooms — never on opening the viewer, and never for the neighbouring photos. @@ -788,6 +793,13 @@ GPS coordinates and place names below city level are overwritten in place; camera data and colour profile stay. A file whose metadata cannot be read is refused rather than sent. +Zoom into a JPEG or AVIF streams the original from Immich, so the API key +needs the `asset.download` permission (_Account Settings › API Keys_). With +only `asset.read` and `asset.view`, Immich answers `403` and the lightbox says +full resolution is unavailable; the Folio log shows +`Failed to stream … 403`. The same permission covers +[originals download](#originals-download). + Formats a browser cannot display need Immich's **full-size preview** (_Administration › Settings › Image Settings › Full-size image_, JPEG format). Immich generates it only while that setting is on, so turn it on and run the diff --git a/hooks/useZoom.ts b/hooks/useZoom.ts index 68eaa4b..4c4cbad 100644 --- a/hooks/useZoom.ts +++ b/hooks/useZoom.ts @@ -18,11 +18,13 @@ import { useCallback, useEffect, useRef, useState, type RefObject } from 'react'; import { + CLICK_ZOOM_SCALE, FIT_VIEW, canZoom, clampView, isZoomed, maxZoomScale, + onFitBox, panBy, pinchView, stepView, @@ -61,6 +63,8 @@ const DOUBLE_TAP_MS = 300; const DOUBLE_TAP_PX = 30; /** A pointer that moved further than this was a drag, not a tap. */ const TAP_SLOP_PX = 10; +/** A mouse press held longer than this was not a click. */ +const CLICK_MS = 500; /** The `+` / `-` step. */ const KEY_STEP = 2; /** Wheel delta per e-fold of zoom; see onWheel. */ @@ -163,6 +167,26 @@ export function useZoom({ photoKey, naturalWidth, imageRef, surface }: UseZoomOp [apply, usable], ); + /** + * A mouse click: on the photo at fit, a closer look around the cursor; + * anywhere while zoomed, back to fit. A click beside the photo does nothing. + */ + const click = useCallback( + (point: Point) => { + if (isZoomed(viewRef.current)) { + apply(FIT_VIEW, true); + return; + } + const m = usable(); + if (!m || !onFitBox(point, m.fit)) return; + apply( + toggleView(viewRef.current, point, m.fit, m.viewport, m.maxScale, CLICK_ZOOM_SCALE), + true, + ); + }, + [apply, usable], + ); + const step = useCallback( (factor: number) => { const m = usable(); @@ -231,7 +255,6 @@ export function useZoom({ photoKey, naturalWidth, imageRef, surface }: UseZoomOp /** Two fingers were down at some point of the current touch sequence. */ let multi = false; let lastTap: { t: number; x: number; y: number } | null = null; - let lastPointerType = ''; const midAndDistance = () => { const [a, b] = [...pointers.values()]; @@ -246,7 +269,6 @@ export function useZoom({ photoKey, naturalWidth, imageRef, surface }: UseZoomOp }; const onPointerDown = (e: PointerEvent) => { - lastPointerType = e.pointerType; if (e.pointerType === 'mouse' && e.button !== 0) return; if (statusRef.current === 'failed') return; const at = { x: e.clientX, y: e.clientY }; @@ -329,6 +351,19 @@ export function useZoom({ photoKey, naturalWidth, imageRef, surface }: UseZoomOp gesture = { kind: 'none' }; } + // A mouse press that neither moved nor lingered is a click; one that + // moved was a pan and leaves the view where the pan put it. + const wasClick = + e.type === 'pointerup' && + e.pointerType === 'mouse' && + down && + !down.moved && + e.timeStamp - down.t < CLICK_MS; + if (wasClick) { + click({ x: e.clientX, y: e.clientY }); + return; + } + const wasTap = e.type === 'pointerup' && e.pointerType !== 'mouse' && @@ -352,14 +387,6 @@ export function useZoom({ photoKey, naturalWidth, imageRef, surface }: UseZoomOp } }; - // Mouse only: touch has its own double-tap above, and some browsers also - // synthesise a dblclick from it, which would toggle straight back. - const onDoubleClick = (e: MouseEvent) => { - if (lastPointerType && lastPointerType !== 'mouse') return; - e.preventDefault(); - toggle({ x: e.clientX, y: e.clientY }); - }; - // Ctrl+wheel is how Chrome, Firefox and Edge report a trackpad pinch (and // what a mouse user presses to zoom); a plain wheel pans once zoomed. const onWheel = (e: WheelEvent) => { @@ -435,7 +462,6 @@ export function useZoom({ photoKey, naturalWidth, imageRef, surface }: UseZoomOp surface.addEventListener('pointermove', onPointerMove); surface.addEventListener('pointerup', onPointerEnd); surface.addEventListener('pointercancel', onPointerEnd); - surface.addEventListener('dblclick', onDoubleClick); surface.addEventListener('wheel', onWheel, { passive: false }); surface.addEventListener('gesturestart', onGestureStart); surface.addEventListener('gesturechange', onGestureChange); @@ -445,13 +471,12 @@ export function useZoom({ photoKey, naturalWidth, imageRef, surface }: UseZoomOp surface.removeEventListener('pointermove', onPointerMove); surface.removeEventListener('pointerup', onPointerEnd); surface.removeEventListener('pointercancel', onPointerEnd); - surface.removeEventListener('dblclick', onDoubleClick); surface.removeEventListener('wheel', onWheel); surface.removeEventListener('gesturestart', onGestureStart); surface.removeEventListener('gesturechange', onGestureChange); surface.removeEventListener('gestureend', onGestureEnd); }; - }, [enabled, photoKey, surface, measure, apply, toggle]); + }, [enabled, photoKey, surface, measure, apply, toggle, click]); const zoomed = isZoomed(view); /** The full-resolution image is in the page (requested, loading or shown). */ diff --git a/lib/__tests__/zoom.test.ts b/lib/__tests__/zoom.test.ts index 340a84c..a3133e2 100644 --- a/lib/__tests__/zoom.test.ts +++ b/lib/__tests__/zoom.test.ts @@ -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); diff --git a/lib/i18n/locales/de.ts b/lib/i18n/locales/de.ts index 326998d..1268e4b 100644 --- a/lib/i18n/locales/de.ts +++ b/lib/i18n/locales/de.ts @@ -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 …', diff --git a/lib/i18n/locales/en.ts b/lib/i18n/locales/en.ts index d281b6f..09a265d 100644 --- a/lib/i18n/locales/en.ts +++ b/lib/i18n/locales/en.ts @@ -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…', diff --git a/lib/i18n/locales/es.ts b/lib/i18n/locales/es.ts index 44ba15d..cec61dc 100644 --- a/lib/i18n/locales/es.ts +++ b/lib/i18n/locales/es.ts @@ -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…', diff --git a/lib/i18n/locales/fr.ts b/lib/i18n/locales/fr.ts index 49cb6ce..2761a36 100644 --- a/lib/i18n/locales/fr.ts +++ b/lib/i18n/locales/fr.ts @@ -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…', diff --git a/lib/i18n/locales/it.ts b/lib/i18n/locales/it.ts index 70093f4..b6c81a1 100644 --- a/lib/i18n/locales/it.ts +++ b/lib/i18n/locales/it.ts @@ -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…', diff --git a/lib/i18n/locales/nl.ts b/lib/i18n/locales/nl.ts index 530c19b..94e17b4 100644 --- a/lib/i18n/locales/nl.ts +++ b/lib/i18n/locales/nl.ts @@ -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…', diff --git a/lib/immich.ts b/lib/immich.ts index 877777a..edf01af 100644 --- a/lib/immich.ts +++ b/lib/immich.ts @@ -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(); diff --git a/lib/zoom.ts b/lib/zoom.ts index 88acc93..41e108c 100644 --- a/lib/zoom.ts +++ b/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 + ); } /** diff --git a/package-lock.json b/package-lock.json index 578ef2c..7a7e33e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-folio", - "version": "0.20.0", + "version": "0.20.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-folio", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/package.json b/package.json index 145135c..9312c79 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "immich-folio", - "version": "0.20.0", + "version": "0.20.1", "license": "MIT", "scripts": { "dev": "next dev",