This commit is contained in:
midzelis
2025-12-16 19:39:23 +00:00
parent f7b08fcd95
commit 1a27bb414d
27 changed files with 850 additions and 545 deletions

164
pnpm-lock.yaml generated
View File

@@ -718,7 +718,7 @@ importers:
version: link:../open-api/typescript-sdk
'@immich/ui':
specifier: ^0.50.1
version: 0.50.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)
version: 0.50.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)
'@mapbox/mapbox-gl-rtl-text':
specifier: 0.2.3
version: 0.2.3(mapbox-gl@1.13.3)
@@ -751,7 +751,7 @@ importers:
version: 0.41.4
'@zoom-image/svelte':
specifier: ^0.3.0
version: 0.3.8(svelte@5.43.3)
version: 0.3.8(svelte@https://pkg.pr.new/svelte@17362)
async-mutex:
specifier: ^0.5.0
version: 0.5.0
@@ -805,13 +805,13 @@ importers:
version: 5.2.2
svelte-i18n:
specifier: ^4.0.1
version: 4.0.1(svelte@5.43.3)
version: 4.0.1(svelte@https://pkg.pr.new/svelte@17362)
svelte-maplibre:
specifier: ^1.2.5
version: 1.2.5(svelte@5.43.3)
version: 1.2.5(svelte@https://pkg.pr.new/svelte@17362)
svelte-persisted-store:
specifier: ^0.12.0
version: 0.12.0(svelte@5.43.3)
version: 0.12.0(svelte@https://pkg.pr.new/svelte@17362)
tabbable:
specifier: ^6.2.0
version: 6.3.0
@@ -836,16 +836,16 @@ importers:
version: 3.1.2
'@sveltejs/adapter-static':
specifier: ^3.0.8
version: 3.0.10(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))
version: 3.0.10(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))
'@sveltejs/enhanced-img':
specifier: ^0.9.0
version: 0.9.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(rollup@4.53.4)(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
version: 0.9.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(rollup@4.53.4)(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
'@sveltejs/kit':
specifier: ^2.27.1
version: 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
version: 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
'@sveltejs/vite-plugin-svelte':
specifier: 6.2.1
version: 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
version: 6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
'@tailwindcss/vite':
specifier: ^4.1.7
version: 4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
@@ -854,7 +854,7 @@ importers:
version: 6.9.1
'@testing-library/svelte':
specifier: ^5.2.8
version: 5.2.9(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
version: 5.2.9(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
'@testing-library/user-event':
specifier: ^14.5.2
version: 14.6.1(@testing-library/dom@10.4.1)
@@ -893,7 +893,7 @@ importers:
version: 6.0.2(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-svelte:
specifier: ^3.12.4
version: 3.13.1(eslint@9.39.2(jiti@2.6.1))(svelte@5.43.3)
version: 3.13.1(eslint@9.39.2(jiti@2.6.1))(svelte@https://pkg.pr.new/svelte@17362)
eslint-plugin-unicorn:
specifier: ^62.0.0
version: 62.0.0(eslint@9.39.2(jiti@2.6.1))
@@ -914,19 +914,19 @@ importers:
version: 4.1.1(prettier@3.7.4)
prettier-plugin-svelte:
specifier: ^3.3.3
version: 3.4.1(prettier@3.7.4)(svelte@5.43.3)
version: 3.4.1(prettier@3.7.4)(svelte@https://pkg.pr.new/svelte@17362)
rollup-plugin-visualizer:
specifier: ^6.0.0
version: 6.0.5(rollup@4.53.4)
svelte:
specifier: 5.43.3
version: 5.43.3
specifier: https://pkg.pr.new/svelte@17362
version: https://pkg.pr.new/svelte@17362
svelte-check:
specifier: ^4.1.5
version: 4.3.4(picomatch@4.0.3)(svelte@5.43.3)(typescript@5.9.3)
version: 4.3.4(picomatch@4.0.3)(svelte@https://pkg.pr.new/svelte@17362)(typescript@5.9.3)
svelte-eslint-parser:
specifier: ^1.3.3
version: 1.4.1(svelte@5.43.3)
version: 1.4.1(svelte@https://pkg.pr.new/svelte@17362)
tailwindcss:
specifier: ^4.1.7
version: 4.1.18
@@ -3009,11 +3009,13 @@ packages:
'@immich/svelte-markdown-preprocess@0.1.0':
resolution: {integrity: sha512-jgSOJEGLPKEXQCNRI4r4YUayeM2b0ZYLdzgKGl891jZBhOQIetlY7rU44kPpV1AA3/8wGDwNFKduIQZZ/qJYzg==}
version: 0.1.0
peerDependencies:
svelte: ^5.0.0
'@immich/ui@0.50.1':
resolution: {integrity: sha512-fNlQGh75ZFa/UZAgJaYk9/ItHOXHNNzN4CunjCmE7WocVVkUZbUxopN9Ku3F5GULSqD/zJ5gNO6PQAZ1ZoSaaQ==}
version: 0.50.1
peerDependencies:
svelte: ^5.0.0
@@ -4326,11 +4328,13 @@ packages:
'@sveltejs/adapter-static@3.0.10':
resolution: {integrity: sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==}
version: 3.0.10
peerDependencies:
'@sveltejs/kit': ^2.0.0
'@sveltejs/enhanced-img@0.9.2':
resolution: {integrity: sha512-hAYZ8YFgYtqrQ0dXyq6rdmHBFyG+eIQnNjdIoVhqeZQEBIREXoBThkx+7FtDa6ZV35lTRaT9dgFKF4W+4LbuaQ==}
version: 0.9.2
peerDependencies:
'@sveltejs/vite-plugin-svelte': ^6.0.0
svelte: ^5.0.0
@@ -4338,6 +4342,7 @@ packages:
'@sveltejs/kit@2.49.2':
resolution: {integrity: sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==}
version: 2.49.2
engines: {node: '>=18.13'}
hasBin: true
peerDependencies:
@@ -4351,6 +4356,7 @@ packages:
'@sveltejs/vite-plugin-svelte-inspector@5.0.1':
resolution: {integrity: sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==}
version: 5.0.1
engines: {node: ^20.19 || ^22.12 || >=24}
peerDependencies:
'@sveltejs/vite-plugin-svelte': ^6.0.0-next.0
@@ -4359,6 +4365,7 @@ packages:
'@sveltejs/vite-plugin-svelte@6.2.1':
resolution: {integrity: sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==}
version: 6.2.1
engines: {node: ^20.19 || ^22.12 || >=24}
peerDependencies:
svelte: ^5.0.0
@@ -4624,6 +4631,7 @@ packages:
'@testing-library/svelte@5.2.9':
resolution: {integrity: sha512-p0Lg/vL1iEsEasXKSipvW9nBCtItQGhYvxL8OZ4w7/IDdC+LGoSJw4mMS5bndVFON/gWryitEhMr29AlO4FvBg==}
version: 5.2.9
engines: {node: '>= 10'}
peerDependencies:
svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0
@@ -5187,6 +5195,7 @@ packages:
'@zoom-image/svelte@0.3.8':
resolution: {integrity: sha512-rkXS+JS4qkBccmRK9+I5j+Pe4rp78GWK/7y0EduBJNtt38q+AwmKhhQs8oTMKTU6lOzLgxjXy1TI802mtvcAmw==}
version: 0.3.8
peerDependencies:
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0
@@ -5565,6 +5574,7 @@ packages:
bits-ui@2.14.4:
resolution: {integrity: sha512-W6kenhnbd/YVvur+DKkaVJ6GldE53eLewur5AhUCqslYQ0vjZr8eWlOfwZnMiPB+PF5HMVqf61vXBvmyrAmPWg==}
version: 2.14.4
engines: {node: '>=20'}
peerDependencies:
'@internationalized/date': ^3.8.1
@@ -6725,6 +6735,7 @@ packages:
eslint-plugin-svelte@3.13.1:
resolution: {integrity: sha512-Ng+kV/qGS8P/isbNYVE3sJORtubB+yLEcYICMkUWNaDTb0SwZni/JhAYXh/Dz/q2eThUwWY0VMPZ//KYD1n3eQ==}
version: 3.13.1
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.1 || ^9.0.0
@@ -9758,6 +9769,7 @@ packages:
prettier-plugin-svelte@3.4.1:
resolution: {integrity: sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg==}
version: 3.4.1
peerDependencies:
prettier: ^3.0.0
svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0
@@ -10230,6 +10242,7 @@ packages:
runed@0.35.1:
resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==}
version: 0.35.1
peerDependencies:
'@sveltejs/kit': ^2.21.0
svelte: ^5.7.0
@@ -10700,6 +10713,7 @@ packages:
svelte-check@4.3.4:
resolution: {integrity: sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw==}
version: 4.3.4
engines: {node: '>= 18.0.0'}
hasBin: true
peerDependencies:
@@ -10708,6 +10722,7 @@ packages:
svelte-eslint-parser@1.4.1:
resolution: {integrity: sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA==}
version: 1.4.1
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.24.0}
peerDependencies:
svelte: ^3.37.0 || ^4.0.0 || ^5.0.0
@@ -10723,6 +10738,7 @@ packages:
svelte-i18n@4.0.1:
resolution: {integrity: sha512-jaykGlGT5PUaaq04JWbJREvivlCnALtT+m87Kbm0fxyYHynkQaxQMnIKHLm2WeIuBRoljzwgyvz0Z6/CMwfdmQ==}
version: 4.0.1
engines: {node: '>= 16'}
hasBin: true
peerDependencies:
@@ -10730,6 +10746,7 @@ packages:
svelte-maplibre@1.2.5:
resolution: {integrity: sha512-Uklcbi6inW9GA0MuSusbXmFr/MQPmXrjuP8hS1+yFX3ySvCQ477tsM3I7Jo/fUDK3XAxFSIHW6hZfucnM3kXwQ==}
version: 1.2.5
peerDependencies:
'@deck.gl/core': ^9
'@deck.gl/layers': ^9
@@ -10745,23 +10762,27 @@ packages:
svelte-parse-markup@0.1.5:
resolution: {integrity: sha512-T6mqZrySltPCDwfKXWQ6zehipVLk4GWfH1zCMGgRtLlOIFPuw58ZxVYxVvotMJgJaurKi1i14viB2GIRKXeJTQ==}
version: 0.1.5
peerDependencies:
svelte: ^3.0.0 || ^4.0.0 || ^5.0.0-next.1
svelte-persisted-store@0.12.0:
resolution: {integrity: sha512-BdBQr2SGSJ+rDWH8/aEV5GthBJDapVP0GP3fuUCA7TjYG5ctcB+O9Mj9ZC0+Jo1oJMfZUd1y9H68NFRR5MyIJA==}
version: 0.12.0
engines: {node: '>=0.14'}
peerDependencies:
svelte: ^3.48.0 || ^4 || ^5
svelte-toolbelt@0.10.6:
resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==}
version: 0.10.6
engines: {node: '>=18', pnpm: '>=8.7.0'}
peerDependencies:
svelte: ^5.30.2
svelte@5.43.3:
resolution: {integrity: sha512-kjkAjCk41mJfvJZG56XcJNOdJSke94JxtcX8zFzzz2vrt47E0LnoBzU6azIZ1aBxJgUep8qegAkguSf1GjxLXQ==}
svelte@https://pkg.pr.new/svelte@17362:
resolution: {tarball: https://pkg.pr.new/svelte@17362}
version: 5.46.0
engines: {node: '>=18'}
svg-parser@2.0.4:
@@ -14649,19 +14670,19 @@ snapshots:
'@immich/justified-layout-wasm@0.4.3': {}
'@immich/svelte-markdown-preprocess@0.1.0(svelte@5.43.3)':
'@immich/svelte-markdown-preprocess@0.1.0(svelte@https://pkg.pr.new/svelte@17362)':
dependencies:
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
'@immich/ui@0.50.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)':
'@immich/ui@0.50.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)':
dependencies:
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.43.3)
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@https://pkg.pr.new/svelte@17362)
'@internationalized/date': 3.10.0
'@mdi/js': 7.4.47
bits-ui: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)
bits-ui: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)
luxon: 3.7.2
simple-icons: 15.22.0
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
svelte-highlight: 7.9.0
tailwind-merge: 3.4.0
tailwind-variants: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18)
@@ -16165,17 +16186,17 @@ snapshots:
dependencies:
acorn: 8.15.0
'@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))':
'@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))':
dependencies:
'@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
'@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
'@sveltejs/enhanced-img@0.9.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(rollup@4.53.4)(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))':
'@sveltejs/enhanced-img@0.9.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(rollup@4.53.4)(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))':
dependencies:
'@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
'@sveltejs/vite-plugin-svelte': 6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
magic-string: 0.30.21
sharp: 0.34.5
svelte: 5.43.3
svelte-parse-markup: 0.1.5(svelte@5.43.3)
svelte: https://pkg.pr.new/svelte@17362
svelte-parse-markup: 0.1.5(svelte@https://pkg.pr.new/svelte@17362)
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)
vite-imagetools: 9.0.2(rollup@4.53.4)
zimmerframe: 1.1.4
@@ -16183,11 +16204,11 @@ snapshots:
- rollup
- supports-color
'@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))':
'@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))':
dependencies:
'@standard-schema/spec': 1.1.0
'@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0)
'@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
'@sveltejs/vite-plugin-svelte': 6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
'@types/cookie': 0.6.0
acorn: 8.15.0
cookie: 0.6.0
@@ -16199,27 +16220,27 @@ snapshots:
sade: 1.8.1
set-cookie-parser: 2.7.2
sirv: 3.0.2
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)
optionalDependencies:
'@opentelemetry/api': 1.9.0
'@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))':
'@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))':
dependencies:
'@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
'@sveltejs/vite-plugin-svelte': 6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
debug: 4.4.3
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)
transitivePeerDependencies:
- supports-color
'@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))':
'@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))':
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
'@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
debug: 4.4.3
deepmerge: 4.3.1
magic-string: 0.30.21
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)
vitefu: 1.1.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
transitivePeerDependencies:
@@ -16467,10 +16488,10 @@ snapshots:
picocolors: 1.1.1
redent: 3.0.0
'@testing-library/svelte@5.2.9(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))':
'@testing-library/svelte@5.2.9(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))':
dependencies:
'@testing-library/dom': 10.4.1
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
optionalDependencies:
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.4)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@20.0.3(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)
@@ -17201,10 +17222,10 @@ snapshots:
dependencies:
'@namnode/store': 0.1.0
'@zoom-image/svelte@0.3.8(svelte@5.43.3)':
'@zoom-image/svelte@0.3.8(svelte@https://pkg.pr.new/svelte@17362)':
dependencies:
'@zoom-image/core': 0.41.4
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
abab@2.0.6:
optional: true
@@ -17569,15 +17590,15 @@ snapshots:
binary-extensions@2.3.0: {}
bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3):
bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362):
dependencies:
'@floating-ui/core': 1.7.3
'@floating-ui/dom': 1.7.4
'@internationalized/date': 3.10.0
esm-env: 1.2.2
runed: 0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)
svelte: 5.43.3
svelte-toolbelt: 0.10.6(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)
runed: 0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)
svelte: https://pkg.pr.new/svelte@17362
svelte-toolbelt: 0.10.6(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)
tabbable: 6.3.0
transitivePeerDependencies:
- '@sveltejs/kit'
@@ -18870,7 +18891,7 @@ snapshots:
'@types/eslint': 9.6.1
eslint-config-prettier: 10.1.8(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-svelte@3.13.1(eslint@9.39.2(jiti@2.6.1))(svelte@5.43.3):
eslint-plugin-svelte@3.13.1(eslint@9.39.2(jiti@2.6.1))(svelte@https://pkg.pr.new/svelte@17362):
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1))
'@jridgewell/sourcemap-codec': 1.5.5
@@ -18882,9 +18903,9 @@ snapshots:
postcss-load-config: 3.1.4(postcss@8.5.6)
postcss-safe-parser: 7.0.1(postcss@8.5.6)
semver: 7.7.3
svelte-eslint-parser: 1.4.1(svelte@5.43.3)
svelte-eslint-parser: 1.4.1(svelte@https://pkg.pr.new/svelte@17362)
optionalDependencies:
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
transitivePeerDependencies:
- ts-node
@@ -22557,10 +22578,10 @@ snapshots:
dependencies:
prettier: 3.7.4
prettier-plugin-svelte@3.4.1(prettier@3.7.4)(svelte@5.43.3):
prettier-plugin-svelte@3.4.1(prettier@3.7.4)(svelte@https://pkg.pr.new/svelte@17362):
dependencies:
prettier: 3.7.4
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
prettier@3.7.4: {}
@@ -23157,14 +23178,14 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
runed@0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3):
runed@0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362):
dependencies:
dequal: 2.0.3
esm-env: 1.2.2
lz-string: 1.5.0
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
optionalDependencies:
'@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
'@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2))
rw@1.3.3: {}
@@ -23779,19 +23800,19 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.43.3)(typescript@5.9.3):
svelte-check@4.3.4(picomatch@4.0.3)(svelte@https://pkg.pr.new/svelte@17362)(typescript@5.9.3):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
chokidar: 4.0.3
fdir: 6.5.0(picomatch@4.0.3)
picocolors: 1.1.1
sade: 1.8.1
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
typescript: 5.9.3
transitivePeerDependencies:
- picomatch
svelte-eslint-parser@1.4.1(svelte@5.43.3):
svelte-eslint-parser@1.4.1(svelte@https://pkg.pr.new/svelte@17362):
dependencies:
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
@@ -23800,7 +23821,7 @@ snapshots:
postcss-scss: 4.0.9(postcss@8.5.6)
postcss-selector-parser: 7.1.1
optionalDependencies:
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
svelte-gestures@5.2.2: {}
@@ -23808,7 +23829,7 @@ snapshots:
dependencies:
highlight.js: 11.11.1
svelte-i18n@4.0.1(svelte@5.43.3):
svelte-i18n@4.0.1(svelte@https://pkg.pr.new/svelte@17362):
dependencies:
cli-color: 2.0.4
deepmerge: 4.3.1
@@ -23816,36 +23837,36 @@ snapshots:
estree-walker: 2.0.2
intl-messageformat: 10.7.18
sade: 1.8.1
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
tiny-glob: 0.2.9
svelte-maplibre@1.2.5(svelte@5.43.3):
svelte-maplibre@1.2.5(svelte@https://pkg.pr.new/svelte@17362):
dependencies:
d3-geo: 3.1.1
dequal: 2.0.3
just-compare: 2.3.0
maplibre-gl: 5.14.0
pmtiles: 3.2.1
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
svelte-parse-markup@0.1.5(svelte@5.43.3):
svelte-parse-markup@0.1.5(svelte@https://pkg.pr.new/svelte@17362):
dependencies:
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
svelte-persisted-store@0.12.0(svelte@5.43.3):
svelte-persisted-store@0.12.0(svelte@https://pkg.pr.new/svelte@17362):
dependencies:
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3):
svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362):
dependencies:
clsx: 2.1.1
runed: 0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.43.3)
runed: 0.35.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.2)))(svelte@https://pkg.pr.new/svelte@17362)
style-to-object: 1.0.14
svelte: 5.43.3
svelte: https://pkg.pr.new/svelte@17362
transitivePeerDependencies:
- '@sveltejs/kit'
svelte@5.43.3:
svelte@https://pkg.pr.new/svelte@17362:
dependencies:
'@jridgewell/remapping': 2.3.5
'@jridgewell/sourcemap-codec': 1.5.5
@@ -23855,6 +23876,7 @@ snapshots:
aria-query: 5.3.2
axobject-query: 4.1.0
clsx: 2.1.1
devalue: 5.6.1
esm-env: 1.2.2
esrap: 2.2.1
is-reference: 3.0.3

View File

@@ -148,6 +148,7 @@ export class MediaRepository {
quality: options.quality,
// this is default in libvips (except the threshold is 90), but we need to set it manually in sharp
chromaSubsampling: options.quality >= 80 ? '4:4:4' : '4:2:0',
progressive: true,
})
.toFile(output);
}

View File

@@ -98,7 +98,7 @@
"prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^3.3.3",
"rollup-plugin-visualizer": "^6.0.0",
"svelte": "5.43.3",
"svelte": "https://pkg.pr.new/svelte@17362",
"svelte-check": "^4.1.5",
"svelte-eslint-parser": "^1.3.3",
"tailwindcss": "^4.1.7",

View File

@@ -1,3 +1,5 @@
import { preloadManager } from '$lib/managers/PreloadManager.svelte';
export interface SwipeFeedbackOptions {
/** Whether the swipe feedback is disabled */
disabled?: boolean;
@@ -17,8 +19,6 @@ export interface SwipeFeedbackOptions {
swipeThreshold?: number;
/** Current asset URL - when this changes, preview containers are reset */
currentAssetUrl?: string | null;
/** The img or video element to transform. If not provided, will query for img/video inside the node */
imageElement?: HTMLImageElement | HTMLVideoElement | null;
}
interface SwipeAnimations {
@@ -31,6 +31,8 @@ interface SwipeAnimations {
* Allows the user to drag an element left or right (horizontal only),
* and resets the position when the drag ends.
* Optionally shows preview images on the left/right during swipe.
*
* Requires exactly one element with [data-swipe-subject] attribute within the node.
*/
export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions) => {
// Animation configuration
@@ -38,9 +40,18 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
// Enable/disable scaling effect during animation
const ENABLE_SCALE_ANIMATION = false;
// Find the image element to apply custom transforms
let imgElement: HTMLImageElement | HTMLVideoElement | null =
options?.imageElement ?? node.querySelector('img') ?? node.querySelector('video');
// Find the element to apply custom transforms - must have [data-swipe-subject] attribute
const swipeSubjects = node.querySelectorAll<HTMLElement>('[data-swipe-subject]');
if (swipeSubjects.length === 0) {
throw new Error('swipeFeedback action requires exactly one element with [data-swipe-subject] attribute, found 0');
}
if (swipeSubjects.length > 1) {
throw new Error(`swipeFeedback action requires exactly one element with [data-swipe-subject] attribute, found ${swipeSubjects.length}`);
}
const imgElement: HTMLElement = swipeSubjects[0];
let isDragging = false;
let startX = 0;
@@ -437,12 +448,9 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
// Get current time before modifying animation
const currentTime = Number(activeAnimations.currentImageAnimation.currentTime) || 0;
console.log(`Committing transition from ${currentTime}ms / ${ANIMATION_DURATION_MS}ms`);
// If animation is already at or near the end, skip to finish immediately
if (currentTime >= ANIMATION_DURATION_MS - 5) {
console.log('Animation already complete, finishing immediately');
// Keep the preview visible by hiding the main image but showing the preview
imgElement.style.opacity = '0';
@@ -495,7 +503,6 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
};
const pointerUp = (event: PointerEvent) => {
console.log('up', event);
if (!isDragging || !event.isPrimary || (event.pointerType === 'mouse' && event.button !== 0)) {
return;
}
@@ -551,23 +558,34 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
return {
update(newOptions?: SwipeFeedbackOptions) {
// Update imgElement if provided
if (newOptions?.imageElement !== undefined) {
imgElement = newOptions.imageElement;
}
// Check if asset URL changed - if so, reset everything
if (newOptions?.currentAssetUrl && newOptions.currentAssetUrl !== lastAssetUrl) {
resetPreviewContainers();
lastAssetUrl = newOptions.currentAssetUrl;
}
const lastLeftPreviewUrl = options?.leftPreviewUrl;
const lastRightPreviewUrl = options?.rightPreviewUrl;
if (
lastLeftPreviewUrl &&
lastLeftPreviewUrl != newOptions?.leftPreviewUrl &&
lastLeftPreviewUrl !== newOptions?.currentAssetUrl
) {
preloadManager.cancelUrl(lastLeftPreviewUrl);
}
if (
lastRightPreviewUrl &&
lastRightPreviewUrl != newOptions?.rightPreviewUrl &&
lastRightPreviewUrl !== newOptions?.currentAssetUrl
) {
preloadManager.cancelUrl(lastRightPreviewUrl);
}
options = newOptions;
// Update or create left preview
if (options?.leftPreviewUrl) {
if (leftPreviewImg) {
// Update existing
leftPreviewImg.src = options.leftPreviewUrl;
} else if (!leftPreviewContainer) {
// Create if doesn't exist
@@ -582,6 +600,7 @@ export const swipeFeedback = (node: HTMLElement, options?: SwipeFeedbackOptions)
if (options?.rightPreviewUrl) {
if (rightPreviewImg) {
// Update existing
rightPreviewImg.src = options.rightPreviewUrl;
} else if (!rightPreviewContainer) {
// Create if doesn't exist

View File

@@ -7,13 +7,23 @@ import { thumbHashToRGBA } from 'thumbhash';
* @param param1 object containing the base64 encoded hash (base64Thumbhash: yourString)
*/
export function thumbhash(canvas: HTMLCanvasElement, { base64ThumbHash }: { base64ThumbHash: string }) {
const ctx = canvas.getContext('2d');
if (ctx) {
const { w, h, rgba } = thumbHashToRGBA(decodeBase64(base64ThumbHash));
const pixels = ctx.createImageData(w, h);
canvas.width = w;
canvas.height = h;
pixels.data.set(rgba);
ctx.putImageData(pixels, 0, 0);
}
const render = (hash: string) => {
const ctx = canvas.getContext('2d');
if (ctx) {
const { w, h, rgba } = thumbHashToRGBA(decodeBase64(hash));
const pixels = ctx.createImageData(w, h);
canvas.width = w;
canvas.height = h;
pixels.data.set(rgba);
ctx.putImageData(pixels, 0, 0);
}
};
render(base64ThumbHash);
return {
update({ base64ThumbHash: newHash }: { base64ThumbHash: string }) {
render(newHash);
},
};
}

View File

@@ -3,48 +3,63 @@ import { useZoomImageWheel } from '@zoom-image/svelte';
import { get } from 'svelte/store';
export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolean }) => {
const { createZoomImage, zoomImageState, setZoomImageState } = useZoomImageWheel();
createZoomImage(node, {
maxZoom: 10,
});
const state = get(photoZoomState);
if (state) {
setZoomImageState(state);
}
node.style.overflow = 'visible';
// Store original event handlers so we can prevent them when disabled
const wheelHandler = (event: WheelEvent) => {
let unsubscribes: (() => void)[] = [];
const createZoomAction = (newOptions?: { disabled?: boolean }) => {
options = newOptions;
if (options?.disabled) {
event.stopImmediatePropagation();
}
};
const disabledPointerDownHandler = (event: PointerEvent) => {
if (options?.disabled) {
event.stopImmediatePropagation();
}
};
// Add handlers at capture phase with higher priority for disabled state
node.addEventListener('wheel', wheelHandler, { capture: true });
node.addEventListener('pointerdown', disabledPointerDownHandler, { capture: true });
const unsubscribes = [photoZoomState.subscribe(setZoomImageState), zoomImageState.subscribe(photoZoomState.set)];
return {
update(newOptions?: { disabled?: boolean }) {
options = newOptions;
},
destroy() {
node.removeEventListener('wheel', wheelHandler, { capture: true });
node.removeEventListener('pointerdown', disabledPointerDownHandler, { capture: true });
for (const unsubscribe of unsubscribes) {
unsubscribe();
}
unsubscribes = [];
} else {
const { createZoomImage, zoomImageState, setZoomImageState } = useZoomImageWheel();
createZoomImage(node, {
maxZoom: 10,
});
const state = get(photoZoomState);
if (state) {
setZoomImageState(state);
}
node.style.overflow = 'visible';
// Store original event handlers so we can prevent them when disabled
const wheelHandler = (event: WheelEvent) => {
if (options?.disabled) {
event.stopImmediatePropagation();
}
};
const disabledPointerDownHandler = (event: PointerEvent) => {
if (options?.disabled) {
event.stopImmediatePropagation();
}
};
// Add handlers at capture phase with higher priority for disabled state
node.addEventListener('wheel', wheelHandler, { capture: true });
node.addEventListener('pointerdown', disabledPointerDownHandler, { capture: true });
unsubscribes = [
photoZoomState.subscribe(setZoomImageState),
zoomImageState.subscribe(photoZoomState.set),
() => node.removeEventListener('wheel', wheelHandler, { capture: true }),
() => node.removeEventListener('pointerdown', disabledPointerDownHandler, { capture: true }),
];
}
};
createZoomAction();
return {
update(newOptions?: { disabled?: boolean }) {
createZoomAction(newOptions);
},
destroy() {
for (const unsubscribe of unsubscribes) {
unsubscribe();
}
unsubscribes = [];
},
};
};

View File

@@ -30,7 +30,6 @@
import {
AssetJobName,
AssetTypeEnum,
getAllAlbums,
getAssetInfo,
getStack,
runAssetJobs,
@@ -97,12 +96,10 @@
stopProgress: stopSlideshowProgress,
slideshowNavigation,
slideshowState,
slideshowTransition,
} = slideshowStore;
const stackThumbnailSize = 60;
const stackSelectedThumbnailSize = 65;
let appearsInAlbums: AlbumResponseDto[] = $state([]);
let shouldPlayMotionPhoto = $state(false);
let sharedLink = getSharedLink();
let enableDetailPanel = asset.hasMetadata;
@@ -116,11 +113,17 @@
let selectedEditType: string = $state('');
let stack: StackResponseDto | null = $state(null);
let slideShowPlaying = $derived($slideshowState === SlideshowState.PlaySlideshow);
let slideShowAscending = $derived($slideshowNavigation === SlideshowNavigation.AscendingOrder);
let slideShowShuffle = $derived($slideshowNavigation === SlideshowNavigation.Shuffle);
let zoomToggle = $state(() => void 0);
let playOriginalVideo = $state($alwaysLoadOriginalVideo);
let nextSizeHint = $state<{ width: number; height: number } | null>(null);
let refreshAlbumsSignal = $state(0);
const setPlayOriginalVideo = (value: boolean) => {
playOriginalVideo = value;
};
@@ -165,26 +168,23 @@
let equirectangularTransitionName = $state<string | null>();
let detailPanelTransitionName = $state<string | null>(null);
if (viewTransitionManager.activeViewTransition) {
transitionName = 'hero';
console.log('setting name initial');
equirectangularTransitionName = 'hero';
}
let addInfoTransition;
let finished;
onMount(async () => {
onMount(() => {
addInfoTransition = () => {
detailPanelTransitionName = 'info';
transitionName = 'hero';
equirectangularTransitionName = 'hero';
console.log('transitioned');
};
eventManager.on('TransitionToAssetViewer', addInfoTransition);
eventManager.on('TransitionToTimeline', addInfoTransition);
finished = () => {
detailPanelTransitionName = null;
transitionName = null;
console.log('setting null');
};
eventManager.on('Finished', finished);
// eventManager.emit('AssetViewerLoaded');
unsubscribes.push(
websocketEvents.on('on_upload_success', (asset) => onAssetUpdate({ event: 'upload', asset })),
websocketEvents.on('on_asset_update', (asset) => onAssetUpdate({ event: 'update', asset })),
@@ -206,10 +206,6 @@
slideshowHistory.queue(toTimelineAsset(asset));
}
});
if (!sharedLink) {
await handleGetAllAlbums();
}
});
onDestroy(() => {
@@ -231,18 +227,6 @@
eventManager.off('Finished', finished!);
});
const handleGetAllAlbums = async () => {
if (authManager.isSharedLink) {
return;
}
try {
appearsInAlbums = await getAllAlbums({ assetId: asset.id });
} catch (error) {
console.error('Error getting album that asset belong to', error);
}
};
const handleOpenActivity = () => {
if ($isShowDetail) {
$isShowDetail = false;
@@ -266,33 +250,43 @@
});
};
const startTransition = async (targetTransition: string | null, targetAsset?: AssetResponseDto) => {
const startTransition = async (
types: string[],
targetTransition: string | null,
targetAsset: AssetResponseDto | null,
navigateFn: () => Promise<boolean>,
) => {
transitionName = viewTransitionManager.getTransitionName('old', targetTransition);
console.log('transitionName', transitionName);
equirectangularTransitionName = viewTransitionManager.getTransitionName('old', targetTransition);
detailPanelTransitionName = 'detail-panel';
await tick();
debugger;
viewTransitionManager.startTransition(
new Promise<void>((resolve) => {
eventManager.once('StartViewTransition', () => {
transitionName = viewTransitionManager.getTransitionName('new', targetTransition);
console.log(transitionName);
if (targetAsset && isEquirectangular(asset) && !isEquirectangular(targetAsset)) {
equirectangularTransitionName = null;
}
});
eventManager.once('AssetViewerFree', () => tick().then(resolve()));
}),
);
const navigationResult = new Promise<boolean>((navigationResolve) => {
viewTransitionManager.startTransition(
new Promise<void>((resolve) => {
eventManager.once('StartViewTransition', async () => {
transitionName = viewTransitionManager.getTransitionName('new', targetTransition);
if (targetAsset && isEquirectangular(asset) && !isEquirectangular(targetAsset)) {
equirectangularTransitionName = null;
}
await tick();
navigationResolve(await navigateFn());
});
eventManager.once('AssetViewerFree', () => tick().then(resolve));
}),
types,
);
});
return navigationResult;
};
const tracker = new InvocationTracker();
const navigateAsset = (order?: 'previous' | 'next', skipTransition: boolean = false) => {
if (!order) {
if ($slideshowState === SlideshowState.PlaySlideshow) {
order = $slideshowNavigation === SlideshowNavigation.AscendingOrder ? 'previous' : 'next';
if (slideShowPlaying) {
order = slideShowAscending ? 'previous' : 'next';
} else {
return;
}
@@ -302,49 +296,53 @@
return;
}
void tracker.invoke(async () => {
let skipped = false;
if (viewTransitionManager.skipTransitions()) {
await tick();
skipped = true;
console.log('was skipped');
}
let skipped = false;
if (viewTransitionManager.skipTransitions()) {
skipped = true;
}
void tracker.invoke(async () => {
let hasNext = false;
if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) {
console.log('$slideshowState', $slideshowState, skipTransition);
if (!skipTransition) {
await startTransition('slideshow', undefined);
}
hasNext = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next();
if (!hasNext) {
const asset = await onRandom?.();
if (asset) {
slideshowHistory.queue(asset);
hasNext = true;
if (slideShowPlaying && slideShowShuffle) {
const navigate = async () => {
let next = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next();
if (!next) {
const asset = await onRandom?.();
if (asset) {
slideshowHistory.queue(asset);
next = true;
}
}
return next;
};
// eslint-disable-next-line unicorn/prefer-ternary
if (viewTransitionManager.isSupported() && !skipped && !skipTransition) {
hasNext = await startTransition(['slideshow'], null, null, navigate);
} else {
hasNext = await navigate();
}
} else if (onNavigateToAsset) {
// only transition if the target is already preloaded, and is in a secure context
const targetAsset = order === 'previous' ? previousAsset : nextAsset;
const preloaded = await preloadManager.isPreloaded(targetAsset);
if (!skipTransition && !!targetAsset && globalThis.isSecureContext && preloaded) {
const targetTransition = $slideshowState === SlideshowState.PlaySlideshow ? null : order;
console.log('sta', $slideshowState);
await startTransition(targetTransition, targetAsset);
const navigate = async () =>
order === 'previous' ? await onNavigateToAsset(previousAsset) : await onNavigateToAsset(nextAsset);
if (viewTransitionManager.isSupported() && !skipped && !skipTransition && !!targetAsset) {
const targetTransition = slideShowPlaying ? null : order;
hasNext = await startTransition(
slideShowPlaying ? ['slideshow'] : ['viewer-nav'],
targetTransition,
targetAsset,
navigate,
);
} else {
console.log('not');
hasNext = await navigate();
}
resetZoomState();
console.log('about to');
hasNext = order === 'previous' ? await onNavigateToAsset(previousAsset) : await onNavigateToAsset(nextAsset);
console.log('done to');
} else {
hasNext = false;
}
if ($slideshowState === SlideshowState.PlaySlideshow) {
if (slideShowPlaying) {
if (hasNext) {
$restartSlideshowProgress = true;
} else {
@@ -411,7 +409,7 @@
const handleAction = async (action: Action) => {
switch (action.type) {
case AssetAction.ADD_TO_ALBUM: {
await handleGetAllAlbums();
refreshAlbumsSignal++;
break;
}
case AssetAction.REMOVE_ASSET_FROM_STACK: {
@@ -454,14 +452,6 @@
await goto(`${AppRoute.PHOTOS}/${newAssetId}`);
};
const handleAboutToNavigate = (target: { direction: 'left' | 'right'; nextWidth: number; nextHeight: number }) => {
nextSizeHint = {
width: target.nextWidth,
height: target.nextHeight,
};
console.log('setting', nextSizeHint);
};
let isFullScreen = $derived(fullscreenElement !== null);
$effect(() => {
@@ -479,7 +469,6 @@
$effect(() => {
const refresh = async () => {
await refreshStack();
await handleGetAllAlbums();
ocrManager.clear();
if (!sharedLink) {
if (previewStackedAsset) {
@@ -504,6 +493,7 @@
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
asset.id;
if (viewerKind !== 'PhotoViewer' && viewerKind !== 'ImagePanaramaViewer' && viewerKind !== 'VideoViewer') {
console.log('EMMITTTTT');
eventManager.emit('AssetViewerFree');
}
});
@@ -592,7 +582,10 @@
{/if}
{#if $slideshowState === SlideshowState.None && showNavigation && !isShowEditor && previousAsset}
<div class="my-auto column-span-1 col-start-1 row-span-full row-start-1 justify-self-start">
<div
class="my-auto column-span-1 col-start-1 row-span-full row-start-1 justify-self-start"
style:view-transition-name="exclude-leftbutton"
>
<PreviousAssetAction onPreviousAsset={() => navigateAsset('previous')} />
</div>
{/if}
@@ -608,7 +601,6 @@
{nextAsset}
{previousAsset}
{nextSizeHint}
onAboutToNavigate={handleAboutToNavigate}
onPreviousAsset={() => navigateAsset('previous', true)}
onNextAsset={() => navigateAsset('next', true)}
{sharedLink}
@@ -616,6 +608,7 @@
{:else if viewerKind === 'StackVideoViewer'}
<VideoViewer
{transitionName}
{asset}
assetId={previewStackedAsset!.id}
{nextAsset}
{previousAsset}
@@ -633,6 +626,7 @@
{:else if viewerKind === 'LiveVideoViewer'}
<VideoViewer
{transitionName}
{asset}
assetId={asset.livePhotoVideoId!}
{nextAsset}
{previousAsset}
@@ -659,15 +653,15 @@
{nextAsset}
{previousAsset}
{nextSizeHint}
onAboutToNavigate={handleAboutToNavigate}
onPreviousAsset={() => navigateAsset('previous', true)}
onNextAsset={() => navigateAsset('next', true)}
{sharedLink}
onFree={() => eventManager.emit('AssetViewerFree')}
onReady={() => eventManager.emit('AssetViewerFree')}
/>
{:else if viewerKind === 'VideoViewer'}
<VideoViewer
{transitionName}
{asset}
assetId={asset.id}
{nextAsset}
{previousAsset}
@@ -706,7 +700,10 @@
</div>
{#if $slideshowState === SlideshowState.None && showNavigation && !isShowEditor && nextAsset}
<div class="my-auto col-span-1 col-start-4 row-span-full row-start-1 justify-self-end">
<div
class="my-auto col-span-1 col-start-4 row-span-full row-start-1 justify-self-end"
style:view-transition-name="exclude-rightbutton"
>
<NextAssetAction onNextAsset={() => navigateAsset('next')} />
</div>
{/if}
@@ -719,7 +716,7 @@
class="row-start-1 row-span-4 w-[360px] overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray bg-light"
translate="yes"
>
<DetailPanel {asset} currentAlbum={album} albums={appearsInAlbums} onClose={() => ($isShowDetail = false)} />
<DetailPanel {asset} {refreshAlbumsSignal} currentAlbum={album} onClose={() => ($isShowDetail = false)} />
</div>
{/if}

View File

@@ -13,13 +13,19 @@
import { boundingBoxesArray } from '$lib/stores/people.store';
import { locale } from '$lib/stores/preferences.store';
import { preferences, user } from '$lib/stores/user.store';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
import { delay, getDimensions } from '$lib/utils/asset-utils';
import { getByteUnitString } from '$lib/utils/byte-units';
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
import { fromISODateTime, fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util';
import { getParentPath } from '$lib/utils/tree-utils';
import { AssetMediaSize, getAssetInfo, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk';
import {
AssetMediaSize,
getAllAlbums,
getAssetInfo,
type AlbumResponseDto,
type AssetResponseDto,
} from '@immich/sdk';
import { Icon, IconButton, LoadingSpinner, modalManager } from '@immich/ui';
import {
mdiCalendar,
@@ -43,12 +49,12 @@
interface Props {
asset: AssetResponseDto;
albums?: AlbumResponseDto[];
currentAlbum?: AlbumResponseDto | null;
refreshAlbumsSignal?: number;
onClose: () => void;
}
let { asset, albums = [], currentAlbum = null, onClose }: Props = $props();
let { asset, refreshAlbumsSignal = 0, currentAlbum = null, onClose }: Props = $props();
let showAssetPath = $state(false);
let showEditFaces = $state(false);
@@ -74,6 +80,17 @@
);
let previousId: string | undefined = $state();
let albums = $state<AlbumResponseDto[]>([]);
$effect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
refreshAlbumsSignal;
if (authManager.isSharedLink) {
return;
}
handlePromiseError(getAllAlbums({ assetId: asset.id }).then((response) => (albums = response)));
});
$effect(() => {
if (!previousId) {
previousId = asset.id;

View File

@@ -11,7 +11,7 @@
import { t } from 'svelte-i18n';
interface Props {
htmlElement: HTMLImageElement | HTMLVideoElement;
htmlElement: HTMLImageElement | HTMLVideoElement | undefined | null;
containerWidth: number;
containerHeight: number;
assetId: string;

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { shortcuts } from '$lib/actions/shortcut';
import { swipeFeedback } from '$lib/actions/swipe-feedback';
import { thumbhash } from '$lib/actions/thumbhash';
import { zoomImageAction } from '$lib/actions/zoom-image';
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
import OcrBoundingBox from '$lib/components/asset-viewer/ocr-bounding-box.svelte';
@@ -11,20 +12,30 @@
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import { ocrManager } from '$lib/stores/ocr.svelte';
import { boundingBoxesArray } from '$lib/stores/people.store';
import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store';
import { SlideshowLook, slideshowLookCssMapping, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { photoZoomState, resetZoomState } from '$lib/stores/zoom-image.store';
import { getAssetUrl, targetImageSize as getTargetImageSize, handlePromiseError } from '$lib/utils';
import { canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils';
import {
getAssetThumbnailUrl,
getAssetUrl,
targetImageSize as getTargetImageSize,
handlePromiseError,
} from '$lib/utils';
import { canCopyImageToClipboard, copyImageToClipboard, getDimensions } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error';
import { scaleToFit } from '$lib/utils/layout-utils';
import { getOcrBoundingBoxes } from '$lib/utils/ocr-utils';
import { getBoundingBox } from '$lib/utils/people-utils';
import { cancelImageUrl } from '$lib/utils/sw-messaging';
import { getAltText } from '$lib/utils/thumbnail-util';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import { TUNABLES } from '$lib/utils/tunables';
import { AssetMediaSize, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
import { LoadingSpinner, toastManager } from '@immich/ui';
import { onDestroy, onMount, untrack } from 'svelte';
import { t } from 'svelte-i18n';
let {
IMAGE_THUMBNAIL: { THUMBHASH_FADE_DURATION },
} = TUNABLES;
interface Props {
transitionName?: string | null | undefined;
@@ -34,19 +45,10 @@
element?: HTMLDivElement;
sharedLink?: SharedLinkResponseDto;
nextSizeHint?: { width: number; height: number } | null;
onAboutToNavigate?: ({
direction,
nextWidth,
nextHeight,
}: {
direction: 'left' | 'right';
nextWidth: number;
nextHeight: number;
}) => void;
onPreviousAsset?: (() => void) | null;
onNextAsset?: (() => void) | null;
onLoad?: (() => void) | null;
onError?: (() => void) | null;
onReady?: (() => void) | null;
onBusy?: (() => void) | null;
onFree?: (() => void) | null;
copyImage?: () => Promise<void>;
@@ -61,11 +63,10 @@
element = $bindable(),
sharedLink,
nextSizeHint,
onAboutToNavigate,
onPreviousAsset = null,
onNextAsset = null,
onLoad,
onError,
onReady,
onBusy,
onFree,
copyImage = $bindable(),
@@ -79,16 +80,32 @@
let imageError: boolean = $state(false);
let loader = $state<HTMLImageElement>();
$effect(() => {
if (loader) {
const _loader = loader;
const _src = loader.src;
const _imageLoaderUrl = imageLoaderUrl;
_loader.onload = () => {
if (_loader.src === _src && imageLoaderUrl === _imageLoaderUrl) {
onload();
}
};
_loader.onerror = () => {
if (_loader.src === _src && imageLoaderUrl === _imageLoaderUrl) {
onerror();
}
};
}
});
resetZoomState();
onDestroy(() => {
$boundingBoxesArray = [];
});
$inspect(transitionName).with(console.log.bind(null, 'transit'));
const box = $derived.by(() => {
const { width, height } = scaleToFit(naturalWidth, naturalHeight, containerWidth, containerHeight);
const { width, height } = scaledDimensions;
return {
width: width + 'px',
height: height + 'px',
@@ -97,6 +114,63 @@
};
});
const blurredSlideshow = $derived(
$slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground && asset.thumbhash,
);
const transitionLetterboxLeft = $derived(transitionName === 'hero' || blurredSlideshow ? null : 'letterbox-left');
const transitionLetterboxRight = $derived(transitionName === 'hero' || blurredSlideshow ? null : 'letterbox-right');
const transitionLetterboxTop = $derived(transitionName === 'hero' || blurredSlideshow ? null : 'letterbox-top');
const transitionLetterboxBottom = $derived(transitionName === 'hero' || blurredSlideshow ? null : 'letterbox-bottom');
// Letterbox regions (the empty space around the main box)
const letterboxLeft = $derived.by(() => {
const { width } = scaledDimensions;
const leftOffset = (containerWidth - width) / 2;
return {
width: leftOffset + 'px',
height: containerHeight + 'px',
left: '0px',
top: '0px',
};
});
const letterboxRight = $derived.by(() => {
const { width } = scaledDimensions;
const leftOffset = (containerWidth - width) / 2;
const rightOffset = leftOffset;
return {
width: rightOffset + 'px',
height: containerHeight + 'px',
left: containerWidth - rightOffset + 'px',
top: '0px',
};
});
const letterboxTop = $derived.by(() => {
const { width, height } = scaledDimensions;
const topOffset = (containerHeight - height) / 2;
const leftOffset = (containerWidth - width) / 2;
return {
width: width + 'px',
height: topOffset + 'px',
left: leftOffset + 'px',
top: '0px',
};
});
const letterboxBottom = $derived.by(() => {
const { width, height } = scaledDimensions;
const topOffset = (containerHeight - height) / 2;
const bottomOffset = topOffset;
const leftOffset = (containerWidth - width) / 2;
return {
width: width + 'px',
height: bottomOffset + 'px',
left: leftOffset + 'px',
top: containerHeight - bottomOffset + 'px',
};
});
let ocrBoxes = $derived(
ocrManager.showOverlay && $photoViewerImgElement
? getOcrBoundingBoxes(ocrManager.data, $photoZoomState, $photoViewerImgElement)
@@ -105,29 +179,6 @@
let isOcrActive = $derived(ocrManager.showOverlay);
const handlePreCommit = (direction: 'left' | 'right', nextWidth: number, nextHeight: number) => {
// Scale the preview dimensions to fit within the viewport (like object-fit: contain)
// This prevents flashing when small images are scaled up by scaleToFit
let width = nextWidth;
let height = nextHeight;
if (direction === 'right' && nextAsset?.exifInfo?.exifImageWidth && nextAsset?.exifInfo?.exifImageHeight) {
width = nextAsset.exifInfo.exifImageWidth;
height = nextAsset.exifInfo.exifImageHeight;
} else if (
direction === 'left' &&
previousAsset?.exifInfo?.exifImageWidth &&
previousAsset?.exifInfo?.exifImageHeight
) {
width = previousAsset.exifInfo.exifImageWidth;
height = previousAsset.exifInfo.exifImageHeight;
}
const box = scaleToFit(width, height, containerWidth, containerHeight);
console.log('nextSize', nextWidth, nextHeight, box);
// onAboutToNavigate?.({ direction, nextWidth: scaledWidth, nextHeight: scaledHeight });
onAboutToNavigate?.({ direction, nextWidth: box.width, nextHeight: box.height });
};
const handleSwipeCommit = (direction: 'left' | 'right') => {
if (direction === 'left' && onNextAsset) {
// Swiped left, go to next asset
@@ -196,30 +247,45 @@
}
};
let lastFreedUrl: string | undefined | null;
const notifyFree = () => {
if (lastFreedUrl !== imageLoaderUrl) {
onFree?.();
lastFreedUrl = imageLoaderUrl;
}
};
const notifyReady = () => {
onReady?.();
};
const onload = () => {
onLoad?.();
onFree?.();
imageLoaded = true;
naturalWidth = loader?.naturalWidth ?? 1;
naturalHeight = loader?.naturalHeight ?? 1;
notifyFree();
dimensions = {
width: loader?.naturalWidth ?? 1,
height: loader?.naturalHeight ?? 1,
};
originalImageLoaded = targetImageSize === AssetMediaSize.Fullsize || targetImageSize === 'original';
};
const onerror = () => {
onError?.();
onFree?.();
naturalWidth = loader?.naturalWidth ?? 1;
naturalHeight = loader?.naturalHeight ?? 1;
imageError = imageLoaded = true;
notifyFree();
dimensions = {
width: loader?.naturalWidth ?? 1,
height: loader?.naturalHeight ?? 1,
};
imageError = true;
};
onMount(() => {
notifyReady();
return () => {
if (!imageLoaded && !imageError) {
onFree?.();
notifyFree();
}
if (imageLoaderUrl) {
preloadManager.cancelPreloadUrl(imageLoaderUrl);
preloadManager.cancelUrl(imageLoaderUrl);
}
};
});
@@ -229,38 +295,56 @@
);
const previousAssetUrl = $derived(getAssetUrl({ asset: previousAsset, sharedLink }));
const nextAssetUrl = $derived(getAssetUrl({ asset: nextAsset, sharedLink }));
const thumbnailUrl = $derived(
getAssetThumbnailUrl({
id: asset.id,
size: AssetMediaSize.Thumbnail,
cacheKey: asset.thumbhash,
}),
);
let thumbnailPreloaded = $state(false);
$effect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
asset;
untrack(() => {
void preloadManager.isUrlPreloaded(thumbnailUrl).then((preloaded) => (thumbnailPreloaded = preloaded));
});
});
let containerWidth = $state(0);
let containerHeight = $state(0);
let naturalWidth = $derived(nextSizeHint?.width ?? 1);
let naturalHeight = $derived(nextSizeHint?.height ?? 1);
const exifDimensions = $derived(
asset.exifInfo?.exifImageHeight && asset.exifInfo.exifImageHeight
? (getDimensions(asset.exifInfo) as { width: number; height: number })
: null,
);
let dimensions = $derived(nextSizeHint ?? exifDimensions ?? { width: 1, height: 1 });
const scaledDimensions = $derived(scaleToFit(dimensions, containerWidth, containerHeight));
let lastUrl: string | undefined | null;
let lastPreviousUrl: string | undefined | null;
let lastNextUrl: string | undefined | null;
$effect(() => {
if (!lastUrl) {
untrack(() => onBusy?.());
}
if (lastUrl && lastUrl !== imageLoaderUrl) {
if (lastUrl !== imageLoaderUrl && imageLoaderUrl) {
untrack(() => {
const isPreviewedImage = imageLoaderUrl === lastPreviousUrl || imageLoaderUrl === lastNextUrl;
if (!isPreviewedImage) {
// It is a previewed image - prevent flicker - skip spinner but still let loader go through lifecycle
imageLoaded = false;
}
imageLoaded = false;
originalImageLoaded = false;
imageError = false;
cancelImageUrl(lastUrl);
onBusy?.();
notifyReady();
});
}
lastUrl = imageLoaderUrl;
lastPreviousUrl = previousAssetUrl;
lastNextUrl = nextAssetUrl;
});
$effect(() => {
$photoViewerImgElement = loader;
});
</script>
<svelte:document
@@ -272,12 +356,8 @@
{ shortcut: { key: 'z' }, onShortcut: zoomToggle, preventDefault: false },
]}
/>
{#if imageError}
<div class="h-full w-full">
<BrokenAsset class="text-xl h-full w-full" />
</div>
{/if}
<img bind:this={loader} style="display:none" src={imageLoaderUrl} alt="" aria-hidden="true" {onload} {onerror} />
<!-- <img bind:this={loader} style="display:none" src={imageLoaderUrl} alt="" aria-hidden="true" {onload} {onerror} /> -->
<div
bind:this={element}
class="absolute h-full w-full select-none"
@@ -285,64 +365,118 @@
bind:clientHeight={containerHeight}
use:swipeFeedback={{
disabled: isOcrActive || $photoZoomState.currentZoom > 1,
onPreCommit: handlePreCommit,
onSwipeCommit: handleSwipeCommit,
leftPreviewUrl: previousAssetUrl,
rightPreviewUrl: nextAssetUrl,
currentAssetUrl: imageLoaderUrl,
imageElement: $photoViewerImgElement,
}}
>
{#if !imageLoaded}
<div id="spinner" class="flex h-full items-center justify-center">
<LoadingSpinner />
</div>
{:else if !imageError}
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
<img
src={imageLoaderUrl}
alt=""
class="-z-1 absolute top-0 start-0 object-cover h-full w-full blur-lg"
draggable="false"
/>
{#if blurredSlideshow}
<canvas
id="test"
use:thumbhash={{ base64ThumbHash: asset.thumbhash! }}
class="-z-1 absolute top-0 left-0 start-0 h-dvh w-dvw"
></canvas>
{/if}
<div
class="absolute"
style:view-transition-name={transitionLetterboxLeft}
style:left={letterboxLeft.left}
style:top={letterboxLeft.top}
style:width={letterboxLeft.width}
style:height={letterboxLeft.height}
></div>
<div
class="absolute"
style:view-transition-name={transitionLetterboxRight}
style:left={letterboxRight.left}
style:top={letterboxRight.top}
style:width={letterboxRight.width}
style:height={letterboxRight.height}
></div>
<div
class="absolute"
style:view-transition-name={transitionLetterboxTop}
style:left={letterboxTop.left}
style:top={letterboxTop.top}
style:width={letterboxTop.width}
style:height={letterboxTop.height}
></div>
<div
class="absolute"
style:view-transition-name={transitionLetterboxBottom}
style:left={letterboxBottom.left}
style:top={letterboxBottom.top}
style:width={letterboxBottom.width}
style:height={letterboxBottom.height}
></div>
<div
style:view-transition-name={transitionName}
data-transition-name={transitionName}
class="absolute"
style:left={box.left}
style:top={box.top}
style:width={box.width}
style:height={box.height}
data-swipe-subject
>
{#if asset.thumbhash}
<canvas data-blur use:thumbhash={{ base64ThumbHash: asset.thumbhash }} class="h-full w-full absolute -z-2"
></canvas>
{#if thumbnailPreloaded}
<img src={thumbnailUrl} alt={$getAltText(toTimelineAsset(asset))} class="h-full w-full absolute -z-1" />
{/if}
{/if}
<div
use:zoomImageAction={{ disabled: isOcrActive }}
style:width={box.width}
style:height={box.height}
style:left={box.left}
style:top={box.top}
style:overflow="visible"
class="absolute"
>
<img
style:view-transition-name={transitionName}
bind:this={$photoViewerImgElement}
src={imageLoaderUrl}
alt={$getAltText(toTimelineAsset(asset))}
class="w-full h-full {$slideshowState === SlideshowState.None
? 'object-contain'
: slideshowLookCssMapping[$slideshowLook]}"
draggable="false"
/>
{#if !imageLoaded && !asset.thumbhash && !imageError}
<div id="spinner" class="absolute flex h-full items-center justify-center">
<LoadingSpinner />
</div>
{/if}
{#if imageError}
<div class="h-full w-full">
<BrokenAsset class="text-xl h-full w-full" />
</div>
{/if}
{#key imageLoaderUrl}
<div
use:zoomImageAction={{ disabled: isOcrActive }}
style:width={box.width}
style:height={box.height}
style:overflow="visible"
class="absolute"
>
<img
decoding="async"
bind:this={loader}
src={imageLoaderUrl}
alt={$getAltText(toTimelineAsset(asset))}
class={[
'w-full',
'h-full',
$slideshowState === SlideshowState.None ? 'object-contain' : slideshowLookCssMapping[$slideshowLook],
imageError && 'hidden',
]}
draggable="false"
/>
<!-- eslint-disable-next-line svelte/require-each-key -->
{#each getBoundingBox($boundingBoxesArray, $photoZoomState, $photoViewerImgElement) as boundingbox}
<div
class="absolute border-solid border-white border-3 rounded-lg"
style="top: {boundingbox.top}px; left: {boundingbox.left}px; height: {boundingbox.height}px; width: {boundingbox.width}px;"
></div>
{/each}
<!-- eslint-disable-next-line svelte/require-each-key -->
{#each getBoundingBox($boundingBoxesArray, $photoZoomState, $photoViewerImgElement) as boundingbox}
<div
class="absolute border-solid border-white border-3 rounded-lg"
style="top: {boundingbox.top}px; left: {boundingbox.left}px; height: {boundingbox.height}px; width: {boundingbox.width}px;"
></div>
{/each}
{#each ocrBoxes as ocrBox (ocrBox.id)}
<OcrBoundingBox {ocrBox} />
{/each}
</div>
{#each ocrBoxes as ocrBox (ocrBox.id)}
<OcrBoundingBox {ocrBox} />
{/each}
</div>
{/key}
{#if isFaceEditMode.value}
<FaceEditor htmlElement={$photoViewerImgElement} {containerWidth} {containerHeight} assetId={asset.id} />
{/if}
{/if}
</div>
</div>
<style>
@@ -355,4 +489,8 @@
visibility: hidden;
animation: 0s linear 0.4s forwards delayedVisibility;
}
[data-blur] {
visibility: hidden;
animation: 0s linear 0.1s forwards delayedVisibility;
}
</style>

View File

@@ -13,6 +13,7 @@
videoViewerVolume,
} from '$lib/stores/preferences.store';
import { getAssetOriginalUrl, getAssetPlaybackUrl, getAssetThumbnailUrl, getAssetUrl } from '$lib/utils';
import { getDimensions } from '$lib/utils/asset-utils';
import { scaleToFit } from '$lib/utils/layout-utils';
import { AssetMediaSize, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
import { LoadingSpinner } from '@immich/ui';
@@ -21,6 +22,7 @@
interface Props {
transitionName?: string | null;
asset: AssetResponseDto;
assetId: string;
previousAsset?: AssetResponseDto;
nextAsset?: AssetResponseDto;
@@ -29,15 +31,6 @@
loopVideo: boolean;
cacheKey: string | null;
playOriginalVideo: boolean;
onAboutToNavigate?: ({
direction,
nextWidth,
nextHeight,
}: {
direction: 'left' | 'right';
nextWidth: number;
nextHeight: number;
}) => void;
onPreviousAsset?: () => void;
onNextAsset?: () => void;
onVideoEnded?: () => void;
@@ -47,6 +40,7 @@
let {
transitionName,
asset,
assetId,
previousAsset,
nextAsset,
@@ -55,7 +49,6 @@
loopVideo,
cacheKey,
playOriginalVideo,
onAboutToNavigate,
onPreviousAsset = () => {},
onNextAsset = () => {},
onVideoEnded = () => {},
@@ -73,10 +66,14 @@
let containerWidth = $state(document.documentElement.clientWidth);
let containerHeight = $state(document.documentElement.clientHeight);
let videoHeight = $derived(nextSizeHint?.height ?? 1);
let videoWidth = $derived(nextSizeHint?.width ?? 1);
$inspect(videoWidth).with(console.log.bind(null, 'vwidth'));
console.log('next', nextSizeHint);
const exifDimensions = $derived(
asset.exifInfo?.exifImageHeight && asset.exifInfo.exifImageHeight
? (getDimensions(asset.exifInfo) as { width: number; height: number })
: null,
);
let dimensions = $derived(nextSizeHint ?? exifDimensions ?? { width: 1, height: 1 });
const scaledDimensions = $derived(scaleToFit(dimensions, containerWidth, containerHeight));
onMount(() => {
// Show video after mount to ensure fading in.
showVideo = true;
@@ -96,9 +93,10 @@
});
const handleLoadedMetadata = () => {
console.log('loaded', videoPlayer?.videoWidth);
videoWidth = videoPlayer?.videoWidth ?? 1;
videoHeight = videoPlayer?.videoHeight ?? 1;
dimensions = {
width: videoPlayer?.videoWidth ?? 1,
height: videoPlayer?.videoHeight ?? 1,
};
eventManager.emit('AssetViewerFree');
};
@@ -141,17 +139,6 @@
}
};
const handlePreCommit = (direction: 'left' | 'right', nextWidth: number, nextHeight: number) => {
const { width: scaledWidth, height: scaledHeight } = scaleToFit(
nextWidth,
nextHeight,
containerWidth,
containerHeight,
);
onAboutToNavigate?.({ direction, nextWidth: scaledWidth, nextHeight: scaledHeight });
};
$effect(() => {
if (isFaceEditMode.value) {
videoPlayer?.pause();
@@ -159,7 +146,7 @@
});
const calculateSize = () => {
const { width, height } = scaleToFit(videoWidth, videoHeight, containerWidth, containerHeight);
const { width, height } = scaledDimensions;
const size = {
width: width + 'px',
@@ -188,16 +175,14 @@
bind:clientWidth={containerWidth}
bind:clientHeight={containerHeight}
use:swipeFeedback={{
onPreCommit: handlePreCommit,
onSwipeCommit: handleSwipeCommit,
leftPreviewUrl: previousAssetUrl,
rightPreviewUrl: nextAssetUrl,
currentAssetUrl: assetFileUrl,
imageElement: videoPlayer,
}}
>
{#if castManager.isCasting}
<div class="place-content-center h-full place-items-center">
<div class="place-content-center h-full place-items-center" data-swipe-subject>
<VideoRemoteViewer
poster={getAssetThumbnailUrl({ id: assetId, size: AssetMediaSize.Preview, cacheKey })}
{onVideoStarted}
@@ -206,7 +191,7 @@
/>
</div>
{:else}
<div>
<div class="relative">
<video
style:view-transition-name={transitionName}
style:height={box.height}
@@ -231,15 +216,14 @@
bind:volume={$videoViewerVolume}
poster={getAssetThumbnailUrl({ id: assetId, size: AssetMediaSize.Preview, cacheKey })}
src={assetFileUrl}
data-swipe-subject
>
</video>
{#if isLoading}
<div class="absolute flex place-content-center place-items-center">
<div class="absolute inset-0 flex place-content-center place-items-center">
<LoadingSpinner />
</div>
{/if}
{#if isFaceEditMode.value}
<FaceEditor htmlElement={videoPlayer} {containerWidth} {containerHeight} {assetId} />
{/if}

View File

@@ -6,6 +6,7 @@
interface Props {
transitionName?: string | null;
asset: AssetResponseDto;
assetId: string;
previousAsset?: AssetResponseDto;
nextAsset?: AssetResponseDto;
@@ -15,15 +16,7 @@
cacheKey: string | null;
loopVideo: boolean;
playOriginalVideo: boolean;
onAboutToNavigate?: ({
direction,
nextWidth,
nextHeight,
}: {
direction: 'left' | 'right';
nextWidth: number;
nextHeight: number;
}) => void;
onClose?: () => void;
onPreviousAsset?: () => void;
onNextAsset?: () => void;
@@ -33,6 +26,7 @@
let {
transitionName,
asset,
assetId,
previousAsset,
nextAsset,
@@ -42,7 +36,6 @@
cacheKey,
loopVideo,
playOriginalVideo,
onAboutToNavigate,
onPreviousAsset,
onClose,
onNextAsset,
@@ -58,13 +51,13 @@
{transitionName}
{loopVideo}
{cacheKey}
{asset}
{assetId}
{nextAsset}
{sharedLink}
{nextSizeHint}
{previousAsset}
{playOriginalVideo}
{onAboutToNavigate}
{onPreviousAsset}
{onNextAsset}
{onVideoEnded}

View File

@@ -3,6 +3,7 @@
import { preloadManager } from '$lib/managers/PreloadManager.svelte';
import { Icon } from '@immich/ui';
import { mdiEyeOffOutline } from '@mdi/js';
import { untrack } from 'svelte';
import type { ActionReturn } from 'svelte/action';
import type { ClassValue } from 'svelte/elements';
@@ -54,13 +55,20 @@
onComplete?.(true);
};
$effect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
url;
untrack(() => {
preloadManager.loading(url);
});
});
function mount(elem: HTMLImageElement): ActionReturn {
if (elem.complete) {
loaded = true;
onComplete?.(false);
}
return {
destroy: () => preloadManager.cancelPreloadUrl(url),
destroy: () => preloadManager.cancelUrl(url),
};
}

View File

@@ -6,7 +6,6 @@
import { uploadAssetsStore } from '$lib/stores/upload';
import type { CommonPosition } from '$lib/utils/layout-utils';
import type { Snippet } from 'svelte';
import { flip } from 'svelte/animate';
let { isUploading } = uploadAssetsStore;
@@ -50,13 +49,14 @@
<div
data-asset-id={asset.id}
class="absolute"
data-transition-name={transitionName}
style:view-transition-name={transitionName}
style:top={position.top + 'px'}
style:left={position.left + 'px'}
style:width={position.width + 'px'}
style:height={position.height + 'px'}
animate:flip={{ duration: transitionDuration }}
>
<!-- animate:flip={{ duration: transitionDuration }} -->
{@render thumbnail({ asset, position })}
{@render customThumbnailLayout?.(asset)}
</div>

View File

@@ -64,7 +64,6 @@
if (!asset) {
return;
}
viewTransitionManager.startTransition(
new Promise<void>((resolve) => {
eventManager.once('TimelineLoaded', async ({ id }) => {
@@ -73,15 +72,18 @@
resolve();
});
}),
[],
() => {
animationTargetAssetId = null;
},
);
};
eventManager.on('TransitionToTimeline', transitionToTimelineCallback);
onDestroy(() => {
eventManager.off('TransitionToTimeline', transitionToTimelineCallback);
});
if (viewTransitionManager.isSupported()) {
eventManager.on('TransitionToTimeline', transitionToTimelineCallback);
onDestroy(() => {
eventManager.off('TransitionToTimeline', transitionToTimelineCallback);
});
}
</script>
{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)}
@@ -147,7 +149,7 @@
:global(::view-transition) {
background: black;
animation-duration: 500ms;
animation-duration: 250ms;
}
:global(::view-transition-old(*)),
@@ -166,12 +168,27 @@
}
:global(::view-transition-old(root)) {
animation: 500ms 0s fadeOut forwards;
animation: 250ms 0s fadeOut forwards;
}
:global(::view-transition-new(root)) {
animation: 500ms 0s fadeIn forwards;
animation: 250ms 0s fadeIn forwards;
}
:global(html:active-view-transition-type(slideshow)) {
:global(&::view-transition-old(root)) {
animation: 1s 0s fadeOut forwards;
}
:global(&::view-transition-new(root)) {
animation: 1s 0s fadeIn forwards;
}
}
:global(html:active-view-transition-type(viewer-nav)) {
:global(&::view-transition-old(root)) {
animation: 350ms 0s fadeOut forwards;
}
:global(&::view-transition-new(root)) {
animation: 350ms 0s fadeIn forwards;
}
}
:global(::view-transition-old(info)) {
animation: 250ms 0s flyOutRight forwards;
}
@@ -179,21 +196,56 @@
animation: 250ms 0s flyInRight forwards;
}
:global(::view-transition-group(detail-panel)) {
z-index: 1;
}
:global(::view-transition-old(detail-panel)),
:global(::view-transition-new(detail-panel)) {
z-index: 3;
animation: none;
}
:global(::view-transition-group(letterbox-left)),
:global(::view-transition-group(letterbox-right)),
:global(::view-transition-group(letterbox-top)),
:global(::view-transition-group(letterbox-bottom)) {
z-index: 4;
}
:global(::view-transition-old(letterbox-left)),
:global(::view-transition-old(letterbox-right)),
:global(::view-transition-old(letterbox-top)),
:global(::view-transition-old(letterbox-bottom)) {
background-color: black;
}
:global(::view-transition-new(letterbox-left)),
:global(::view-transition-new(letterbox-right)) {
height: 100dvh;
}
:global(::view-transition-new(letterbox-left)),
:global(::view-transition-new(letterbox-right)),
:global(::view-transition-new(letterbox-top)),
:global(::view-transition-new(letterbox-bottom)) {
background-color: black;
opacity: 1 !important;
}
:global(::view-transition-group(exclude-leftbutton)),
:global(::view-transition-group(exclude-rightbutton)),
:global(::view-transition-group(exclude)) {
animation: none;
z-index: 2;
z-index: 5;
}
:global(::view-transition-old(exclude-leftbutton)),
:global(::view-transition-old(exclude-rightbutton)),
:global(::view-transition-old(exclude)) {
visibility: hidden;
}
:global(::view-transition-new(exclude-leftbutton)),
:global(::view-transition-new(exclude-rightbutton)),
:global(::view-transition-new(exclude)) {
animation: none;
z-index: 2;
z-index: 5;
}
:global(::view-transition-old(hero)) {
@@ -206,124 +258,100 @@
}
:global(::view-transition-old(next)),
:global(::view-transition-old(next-old)) {
animation: 250ms flyOutLeft forwards;
/* transform-origin: center; */
height: 100%;
/* display: flex; */
object-fit: contain;
/* margin: auto; */
/* transform: translateY(50%); */
/* width: auto;
left: 50dvw;
top: 50dvh;
transform: translate3d(-50%, -50%, 0); */
/* object-fit: contain;
height: 100vh;
width: 100vw;
z-index: 10; */
animation: 250ms cubic-bezier(0.25, 0.46, 0.45, 0.94) flyOutLeft forwards;
overflow: hidden;
}
:global(::view-transition-new(next)),
:global(::view-transition-new(next-new)) {
animation: 250ms fadeIn forwards;
height: 100%;
/* display: flex; */
object-fit: contain;
/* transform-origin: center;
width: auto;
left: 50dvw;
top: 50dvh;
transform: translate3d(-50%, -50%, 0); */
/* height: 100%;
object-fit: contain;
height: 100vh;
width: 100vw; */
animation: 250ms cubic-bezier(0.25, 0.46, 0.45, 0.94) flyInRight forwards;
overflow: hidden;
}
:global(::view-transition-old(previous)) {
animation: 1s flyOutRight forwards;
animation: 250ms cubic-bezier(0.25, 0.46, 0.45, 0.94) flyOutRight forwards;
}
:global(::view-transition-old(previous-old)) {
animation: 250ms flyOutRight forwards;
height: 100%;
object-fit: contain;
/* transform-origin: center; */
/* height: 100%;
object-fit: contain;
height: 100vh;
width: 100vw;
z-index: 10; */
animation: 250ms cubic-bezier(0.25, 0.46, 0.45, 0.94) flyOutRight forwards;
overflow: hidden;
z-index: -1;
}
:global(::view-transition-new(previous)) {
animation: 1s flyOutRight forwards;
animation: 250ms cubic-bezier(0.25, 0.46, 0.45, 0.94) flyInLeft forwards;
}
:global(::view-transition-new(previous-new)) {
animation: 250ms fadeIn forwards;
height: 100%;
object-fit: contain;
/* transform-origin: center; */
/* height: 100%;
object-fit: contain;
height: 100vh;
width: 100vw; */
animation: 250ms cubic-bezier(0.25, 0.46, 0.45, 0.94) flyInLeft forwards;
overflow: hidden;
}
@keyframes -global-flyInLeft {
from {
/* transform: translateX(-50dvw); */
object-position: -25dvw;
opacity: 0;
/* object-position: -25dvw; */
transform: translateX(-15%);
opacity: 0.1;
filter: blur(4px);
}
50% {
opacity: 0.4;
filter: blur(2px);
}
to {
/* transform: translateX(0); */
object-position: 0px 0px;
opacity: 1;
filter: blur(0);
}
}
@keyframes -global-flyOutLeft {
from {
/* transform: translateX(0); */
object-position: 0px;
opacity: 1;
filter: blur(0);
}
50% {
opacity: 0.4;
filter: blur(2px);
}
to {
/* transform: translateX(-50dvw); */
object-position: -25dvw;
opacity: 0;
/* object-position: -25dvw; */
transform: translateX(-15%);
opacity: 0.1;
filter: blur(4px);
}
}
@keyframes -global-flyInRight {
from {
/* transform: translateX(50dvw); */
object-position: 25dvw;
opacity: 0;
/* object-position: 25dvw; */
transform: translateX(15%);
opacity: 0.1;
filter: blur(4px);
}
50% {
opacity: 0.4;
filter: blur(2px);
}
to {
/* transform: translateX(0); */
object-position: 0px;
opacity: 1;
filter: blur(0);
}
}
/* Fly out to right */
@keyframes -global-flyOutRight {
from {
/* transform: translateX(0); */
opacity: 1;
filter: blur(0);
}
50% {
opacity: 0.4;
filter: blur(2px);
}
to {
/* transform: translateX(50dvw); */
object-position: 50dvw 0px;
opacity: 0;
/* object-position: 50dvw 0px; */
transform: translateX(15%);
opacity: 0.1;
filter: blur(4px);
}
}

View File

@@ -229,9 +229,7 @@
// if the asset is not found, scroll to the top
timelineManager.scrollTo(0);
}
if (!isAssetViewerRoute(page)) {
invisible = false;
}
invisible = isAssetViewerRoute(page) ? true : false;
};
// note: only modified once in afterNavigate()
@@ -243,7 +241,6 @@
// and a new route is being navigated to. It will never be called on direct
// navigations by the browser.
beforeNavigate(({ from, to }) => {
console.log('BEFORE NAV');
timelineManager.suspendTransitions = true;
const isNavigatingToAssetViewer = isAssetViewerRoute(to);
const isNavigatingFromAssetViewer = isAssetViewerRoute(from);
@@ -257,7 +254,6 @@
// after successful navigation.
afterNavigate(({ complete }) => {
void complete.finally(async () => {
console.log('AFTER nav');
const isAssetViewerPage = isAssetViewerRoute(page);
// Set initial load state only once - if initialLoadWasAssetViewer is null, then
@@ -722,32 +718,39 @@
{asset}
{albumUsers}
{groupIndex}
onClick={(asset) => {
onClick={async (asset) => {
const callClickHandler = () => {
if (typeof onThumbnailClick === 'function') {
onThumbnailClick(asset, timelineManager, dayGroup, _onClick);
} else {
_onClick(timelineManager, dayGroup.getAssets(), dayGroup.groupTitle, asset);
}
};
if (!viewTransitionManager.isSupported()) {
callClickHandler();
return;
}
// tag target on the 'old' snapshot
toAssetViewerTransitionId = asset.id;
await tick();
eventManager.once('StartViewTransition', () => {
// remove target on the 'old' view,
// asset-viewer will tag new target element for 'new' snapshot
toAssetViewerTransitionId = null;
callClickHandler();
});
viewTransitionManager.startTransition(
new Promise((resolve) =>
new Promise<void>((resolve) => {
eventManager.once('AssetViewerFree', async () => {
toAssetViewerTransitionId = null;
await tick();
eventManager.emit('TransitionToAssetViewer');
resolve();
}),
),
});
}),
);
if (typeof onThumbnailClick === 'function') {
onThumbnailClick(asset, timelineManager, dayGroup, _onClick);
} else {
_onClick(timelineManager, dayGroup.getAssets(), dayGroup.groupTitle, asset);
}
}}
onSelect={() => {
if (isSelectionMode || assetInteraction.selectionActive) {

View File

@@ -41,18 +41,26 @@
person = null,
}: Props = $props();
const getNextAsset = async (currentAsset: AssetResponseDto) => {
const getNextAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => {
const earlierTimelineAsset = await timelineManager.getEarlierAsset(currentAsset);
if (earlierTimelineAsset) {
const asset = assetCacheManager.getAsset({ ...authManager.params, id: earlierTimelineAsset.id });
const asset = await assetCacheManager.getAsset({ ...authManager.params, id: earlierTimelineAsset.id });
if (preload) {
// also pre-cache an extra one, to pre-cache these assetInfos for the next nav after this one is complete
void getNextAsset(asset, false);
}
return asset;
}
};
const getPreviousAsset = async (currentAsset: AssetResponseDto) => {
const getPreviousAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => {
const laterTimelineAsset = await timelineManager.getLaterAsset(currentAsset);
if (laterTimelineAsset) {
const asset = assetCacheManager.getAsset({ ...authManager.params, id: laterTimelineAsset.id });
const asset = await assetCacheManager.getAsset({ ...authManager.params, id: laterTimelineAsset.id });
if (preload) {
// also pre-cache an extra one, to pre-cache these assetInfos for the next nav after this one is complete
void getPreviousAsset(asset, false);
}
return asset;
}
};

View File

@@ -1,44 +1,60 @@
import { getAssetUrl } from '$lib/utils';
import { cancelImageUrl, isImageUrlCached, preloadImageUrl } from '$lib/utils/sw-messaging';
import { cancelImageUrl, isImageUrlCached, isServiceWorkerEnabled, preloadImageUrl } from '$lib/utils/sw-messaging';
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
class PreloadManager {
#cachedImages = new Set<string>();
loading(url: string) {
if (!isServiceWorkerEnabled()) {
this.#cachedImages.add(url);
}
}
preload(asset: AssetResponseDto | undefined) {
if (!asset) {
return;
}
if (globalThis.isSecureContext) {
if (isServiceWorkerEnabled()) {
preloadImageUrl(getAssetUrl({ asset }));
return;
}
if (asset.type === AssetTypeEnum.Image) {
const img = new Image();
img.src = getAssetUrl({ asset });
const src = getAssetUrl({ asset });
if (src) {
const img = new Image();
img.src = src;
}
}
}
isPreloaded(asset: AssetResponseDto | undefined) {
if (!asset) {
return false;
return Promise.resolve(false);
}
if (globalThis.isSecureContext) {
const img = getAssetUrl({ asset });
const url = getAssetUrl({ asset });
return this.isUrlPreloaded(url);
}
return isImageUrlCached(img);
isUrlPreloaded(url: string | undefined | null) {
if (!url) {
return Promise.resolve(false);
}
return false;
if (isServiceWorkerEnabled()) {
return isImageUrlCached(url);
}
return Promise.resolve(this.#cachedImages.has(url));
}
cancel(asset: AssetResponseDto | undefined) {
if (!globalThis.isSecureContext || !asset) {
if (!isServiceWorkerEnabled() || !asset) {
return;
}
const url = getAssetUrl({ asset });
cancelImageUrl(url);
}
cancelPreloadUrl(url: string) {
if (!globalThis.isSecureContext) {
cancelUrl(url: string) {
if (!isServiceWorkerEnabled()) {
return;
}

View File

@@ -1,5 +1,8 @@
import { eventManager } from '$lib/managers/event-manager.svelte';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function traceTransitionEvents(msg: string, error?: unknown) {
// console.log(msg, error);
}
class ViewTransitionManager {
#activeViewTransition = $state<ViewTransition | null>(null);
#finishedCallbacks: (() => void)[] = [];
@@ -25,36 +28,57 @@ class ViewTransitionManager {
return this.#activeViewTransition;
}
isSupported() {
return 'startViewTransition' in document;
}
skipTransitions() {
const skippedTransitions = !!this.#activeViewTransition;
if (skippedTransitions) {
console.log('skipped!');
}
this.#activeViewTransition?.skipTransition();
this.#notifyFinished();
return skippedTransitions;
}
startTransition(domUpdateComplete: Promise<void>, finishedCallback?: () => void) {
startTransition(domUpdateComplete: Promise<unknown>, types?: string[], finishedCallback?: () => unknown) {
if (!this.isSupported()) {
throw new Error('View transition API not available');
}
if (this.#activeViewTransition) {
console.error('Can not start transition - one already active');
traceTransitionEvents('Can not start transition - one already active');
return;
}
// good time to add view-transition-name styles (if needed)
traceTransitionEvents('emit BeforeStartViewTransition');
eventManager.emit('BeforeStartViewTransition');
// next call will create the 'old' view snapshot
// eslint-disable-next-line tscompat/tscompat
const transition = document.startViewTransition(async () => {
try {
let transition: ViewTransition;
try {
// eslint-disable-next-line tscompat/tscompat
transition = document.startViewTransition({
update: async () => {
// Good time to remove any view-transition-name styles created during
// BeforeStartViewTransition, then trigger the actual view transition.
traceTransitionEvents('emit StartViewTransition');
eventManager.emit('StartViewTransition');
await domUpdateComplete;
traceTransitionEvents('awaited domUpdateComplete');
},
types,
});
} catch {
// eslint-disable-next-line tscompat/tscompat
transition = document.startViewTransition(async () => {
// Good time to remove any view-transition-name styles created during
// BeforeStartViewTransition, then trigger the actual view transition.
traceTransitionEvents('emit StartViewTransition');
eventManager.emit('StartViewTransition');
await domUpdateComplete;
} catch (error: unknown) {
console.log('exception', error);
}
});
traceTransitionEvents('awaited domUpdateComplete');
});
}
this.#activeViewTransition = transition;
this.#finishedCallbacks.push(() => {
this.#activeViewTransition = null;
@@ -67,10 +91,10 @@ class ViewTransitionManager {
// eslint-disable-next-line tscompat/tscompat
transition.updateCallbackDone
.then(() => {
console.log('update done');
traceTransitionEvents('emit UpdateCallbackDone');
eventManager.emit('UpdateCallbackDone');
})
.catch((error: unknown) => console.log('exception in update', error));
.catch((error: unknown) => traceTransitionEvents('error in UpdateCallbackDone', error));
// Both old/new snapshots are taken - pseudo elements are created, transition is
// about to start
// eslint-disable-next-line tscompat/tscompat
@@ -78,21 +102,21 @@ class ViewTransitionManager {
.then(() => eventManager.emit('Ready'))
.catch((error: unknown) => {
this.#notifyFinished();
console.log('exception in ready', error);
traceTransitionEvents('error in Ready', error);
});
// Transition is complete
// eslint-disable-next-line tscompat/tscompat
transition.finished
.then(() => {
traceTransitionEvents('emit Finished');
eventManager.emit('Finished');
console.log('finished');
})
.catch((error: unknown) => console.log('exception in finished', error));
.catch((error: unknown) => traceTransitionEvents('error in Finished', error));
// eslint-disable-next-line tscompat/tscompat
void transition.finished.then(() => this.#notifyFinished());
}
#notifyFinished() {
console.log('finishedCallbacks len', this.#finishedCallbacks.length);
for (const callback of this.#finishedCallbacks) {
callback();
}

View File

@@ -1,4 +1,4 @@
import { writable } from 'svelte/store';
export const photoViewerImgElement = writable<HTMLImageElement | null>(null);
export const photoViewerImgElement = writable<HTMLImageElement | null | undefined>(null);
export const isSelectingAllAssets = writable(false);

View File

@@ -1,7 +1,13 @@
import type { ZoomImageWheelState } from '@zoom-image/core';
import { writable } from 'svelte/store';
export const photoZoomState = writable<ZoomImageWheelState>();
export const photoZoomState = writable<ZoomImageWheelState>({
currentRotation: 0,
currentZoom: 1,
enable: true,
currentPositionX: 0,
currentPositionY: 0,
});
export const resetZoomState = () => {
photoZoomState.set({

View File

@@ -132,14 +132,14 @@ export type CommonPosition = {
};
// Scales dimensions to fit within a container (like object-fit: contain)
export const scaleToFit = (width: number, height: number, containerW: number, containerH: number) => {
const scaleX = containerW / width;
const scaleY = containerH / height;
export const scaleToFit = (dimensions: { width: number; height: number }, containerW: number, containerH: number) => {
const scaleX = containerW / dimensions.width;
const scaleY = containerH / dimensions.height;
const scale = Math.min(scaleX, scaleY);
return {
width: width * scale,
height: height * scale,
width: dimensions.width * scale,
height: dimensions.height * scale,
};
};

View File

@@ -24,11 +24,11 @@ export interface boundingBox {
export const getBoundingBox = (
faces: Faces[],
zoom: ZoomImageWheelState,
photoViewer: HTMLImageElement | null,
photoViewer: HTMLImageElement | null | undefined,
): boundingBox[] => {
const boxes: boundingBox[] = [];
if (photoViewer === null) {
if (!photoViewer) {
return boxes;
}
const clientHeight = photoViewer.clientHeight;

View File

@@ -1,13 +1,21 @@
const broadcast = new BroadcastChannel('immich');
let isLoadedReplyListeners: ((url: string, isUrlCached: boolean) => void)[] = [];
let serviceWorkerEnabled = false;
let replyListeners: ((url: string, isUrlCached: boolean) => void)[] = [];
broadcast.addEventListener('message', (event) => {
if (event.data.type == 'isImageUrlCachedReply') {
for (const listener of isLoadedReplyListeners) {
if (event.data.type === 'isImageUrlCachedReply') {
for (const listener of replyListeners) {
listener(event.data.url, event.data.isImageUrlCached);
}
} else if (event.data.type === 'isServiceWorkerEnabledReply') {
serviceWorkerEnabled = true;
}
});
broadcast.postMessage({ type: 'isServiceWorkerEnabled' });
export function isServiceWorkerEnabled() {
return serviceWorkerEnabled;
}
export function cancelImageUrl(url: string | undefined | null) {
if (!url) {
@@ -24,22 +32,21 @@ export function preloadImageUrl(url: string | undefined | null) {
}
export function isImageUrlCached(url: string) {
if (!globalThis.isSecureContext) {
if (!globalThis.isSecureContext || !serviceWorkerEnabled) {
return Promise.resolve(false);
}
return new Promise((resolve) => {
return new Promise<boolean>((resolve) => {
const listener = (urlReply: string, isUrlCached: boolean) => {
if (urlReply === url) {
cleanup(isUrlCached);
}
};
const cleanup = (isUrlCached: boolean) => {
isLoadedReplyListeners = isLoadedReplyListeners.filter((element) => element !== listener);
replyListeners = replyListeners.filter((element) => element !== listener);
resolve(isUrlCached);
};
isLoadedReplyListeners.push(listener);
replyListeners.push(listener);
broadcast.postMessage({ type: 'isImageUrlCached', url });
setTimeout(() => cleanup(false), 5000);
});
}

View File

@@ -26,6 +26,11 @@ export const installBroadcastChannelListener = () => {
void handleIsUrlCached(url);
break;
}
case 'isServiceWorkerEnabled': {
replyIsServiceWorkerEnabled();
break;
}
}
};
};
@@ -33,3 +38,7 @@ export const installBroadcastChannelListener = () => {
export const replyIsImageUrlCached = (url: string, isImageUrlCached: boolean) => {
broadcast.postMessage({ type: 'isImageUrlCachedReply', url, isImageUrlCached });
};
export const replyIsServiceWorkerEnabled = () => {
broadcast.postMessage({ type: 'isServiceWorkerEnabledReply', enabled: true });
};

View File

@@ -37,3 +37,4 @@ sw.addEventListener('install', handleInstall, { passive: true });
sw.addEventListener('activate', handleActivate, { passive: true });
sw.addEventListener('fetch', handleFetch, { passive: true });
installBroadcastChannelListener();

View File

@@ -77,6 +77,5 @@ export const handleIsUrlCached = async (url: URL) => {
const cacheKey = getCacheKey(url);
const isImageUrlCached = !!(await get(cacheKey));
console.log('cacheKey', cacheKey, isImageUrlCached);
replyIsImageUrlCached(url.pathname + url.search + url.hash, isImageUrlCached);
};