mirror of
https://github.com/immich-app/immich.git
synced 2026-05-27 10:02:31 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a89564270 | |||
| 2dd6b47714 |
@@ -840,7 +840,6 @@
|
||||
"copy_error": "Copy error",
|
||||
"copy_file_path": "Copy file path",
|
||||
"copy_image": "Copy Image",
|
||||
"copy_json": "Copy JSON",
|
||||
"copy_link": "Copy link",
|
||||
"copy_link_to_clipboard": "Copy link to clipboard",
|
||||
"copy_password": "Copy password",
|
||||
@@ -980,8 +979,6 @@
|
||||
"downloading_media": "Downloading media",
|
||||
"drag_to_reorder": "Drag to reorder",
|
||||
"drop_files_to_upload": "Drop files anywhere to upload",
|
||||
"duplicate": "Duplicate",
|
||||
"duplicate_workflow": "Duplicate workflow",
|
||||
"duplicates": "Duplicates",
|
||||
"duplicates_description": "Resolve each group by indicating which, if any, are duplicates.",
|
||||
"duration": "Duration",
|
||||
|
||||
@@ -276,8 +276,6 @@ class TimeBucketAssetResponseDto {
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'city',
|
||||
'country',
|
||||
'createdAt',
|
||||
'duration',
|
||||
'fileCreatedAt',
|
||||
|
||||
@@ -25324,8 +25324,6 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"city",
|
||||
"country",
|
||||
"createdAt",
|
||||
"duration",
|
||||
"fileCreatedAt",
|
||||
|
||||
@@ -2612,9 +2612,9 @@ export type TagUpdateDto = {
|
||||
};
|
||||
export type TimeBucketAssetResponseDto = {
|
||||
/** Array of city names extracted from EXIF GPS data */
|
||||
city: (string | null)[];
|
||||
city?: (string | null)[];
|
||||
/** Array of country names extracted from EXIF GPS data */
|
||||
country: (string | null)[];
|
||||
country?: (string | null)[];
|
||||
/** Array of UTC timestamps when each asset was originally uploaded to Immich */
|
||||
createdAt: string[];
|
||||
/** Array of video/gif durations in milliseconds (null for static images) */
|
||||
|
||||
Generated
+28
-34
@@ -609,7 +609,7 @@ importers:
|
||||
version: 10.0.1(eslint@10.4.0(jiti@2.7.0))
|
||||
'@nestjs/cli':
|
||||
specifier: ^11.0.2
|
||||
version: 11.0.21(@swc/core@1.15.33(@swc/helpers@0.5.22))(@types/node@24.12.4)(esbuild@0.28.0)(lightningcss@1.32.0)(prettier@3.8.3)
|
||||
version: 11.0.21(@swc/core@1.15.33(@swc/helpers@0.5.21))(@types/node@24.12.4)(esbuild@0.28.0)(lightningcss@1.32.0)(prettier@3.8.3)
|
||||
'@nestjs/schematics':
|
||||
specifier: ^11.0.0
|
||||
version: 11.1.0(chokidar@4.0.3)(prettier@3.8.3)(typescript@6.0.3)
|
||||
@@ -618,7 +618,7 @@ importers:
|
||||
version: 11.1.21(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21)(@nestjs/platform-express@11.1.21)
|
||||
'@swc/core':
|
||||
specifier: ^1.4.14
|
||||
version: 1.15.33(@swc/helpers@0.5.22)
|
||||
version: 1.15.33(@swc/helpers@0.5.21)
|
||||
'@types/archiver':
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
@@ -738,7 +738,7 @@ importers:
|
||||
version: 8.59.4(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3)
|
||||
unplugin-swc:
|
||||
specifier: ^1.4.5
|
||||
version: 1.5.9(@swc/core@1.15.33(@swc/helpers@0.5.22))(rollup@4.60.4)
|
||||
version: 1.5.9(@swc/core@1.15.33(@swc/helpers@0.5.21))(rollup@4.60.4)
|
||||
vite-tsconfig-paths:
|
||||
specifier: ^6.0.0
|
||||
version: 6.1.1(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0))
|
||||
@@ -758,8 +758,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../packages/sdk
|
||||
'@immich/ui':
|
||||
specifier: ^0.79.2
|
||||
version: 0.79.2(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))
|
||||
specifier: ^0.79.0
|
||||
version: 0.79.0(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))
|
||||
'@mapbox/mapbox-gl-rtl-text':
|
||||
specifier: 0.4.0
|
||||
version: 0.4.0
|
||||
@@ -1691,10 +1691,6 @@ packages:
|
||||
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime@7.29.7':
|
||||
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.28.6':
|
||||
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -3208,8 +3204,8 @@ packages:
|
||||
resolution: {integrity: sha512-O1SJ+BbeFVsUTF4af1MfagJZM+lPgLjI8lQ3SZNjpo8SGJReSbUl2ii03OKuGni/G0yp2GnRLpOTNSHYGtVrcg==}
|
||||
hasBin: true
|
||||
|
||||
'@immich/ui@0.79.2':
|
||||
resolution: {integrity: sha512-tnpYhYHrjrFJK18QglRMzPUtHv6q5V6tW38HiAraQJBv7MCg+yaJDrdF8omM2L5F311FGlv1PZLJYvmR4e49PA==}
|
||||
'@immich/ui@0.79.0':
|
||||
resolution: {integrity: sha512-UEQZrP8CTc4Kth1xCV8/6Xmk1P51GQKISC7vKqcrM0BO0fxCaNwJK8Ocn6R8baVqH52JYfPb1yQR9bweBnCjXw==}
|
||||
peerDependencies:
|
||||
'@sveltejs/kit': ^2.13.0
|
||||
svelte: ^5.0.0
|
||||
@@ -4986,8 +4982,8 @@ packages:
|
||||
'@swc/counter@0.1.3':
|
||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
||||
|
||||
'@swc/helpers@0.5.22':
|
||||
resolution: {integrity: sha512-/e2Ly3Docn9kYByap6TV4oquJ3wQuz3c+kC74riqtkwU9CwTMeuj6t2rW+bRr4pyOx/CYQM4wr0RgaKQwGEz0A==}
|
||||
'@swc/helpers@0.5.21':
|
||||
resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==}
|
||||
|
||||
'@swc/types@0.1.26':
|
||||
resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==}
|
||||
@@ -13791,8 +13787,6 @@ snapshots:
|
||||
|
||||
'@babel/runtime@7.29.2': {}
|
||||
|
||||
'@babel/runtime@7.29.7': {}
|
||||
|
||||
'@babel/template@7.28.6':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
@@ -15885,7 +15879,7 @@ snapshots:
|
||||
pg-connection-string: 2.13.0
|
||||
postgres: 3.4.9
|
||||
|
||||
'@immich/ui@0.79.2(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))':
|
||||
'@immich/ui@0.79.0(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.55.8(@typescript-eslint/types@8.59.4))(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))(typescript@6.0.3)(vite@8.0.13(@types/node@24.12.4)(esbuild@0.28.0)(jiti@2.7.0)(sass@1.99.0)(terser@5.47.1)(tsx@4.22.3)(yaml@2.9.0)))(svelte@5.55.8(@typescript-eslint/types@8.59.4))':
|
||||
dependencies:
|
||||
'@internationalized/date': 3.12.1
|
||||
'@mdi/js': 7.4.47
|
||||
@@ -16041,7 +16035,7 @@ snapshots:
|
||||
|
||||
'@internationalized/date@3.12.1':
|
||||
dependencies:
|
||||
'@swc/helpers': 0.5.22
|
||||
'@swc/helpers': 0.5.21
|
||||
|
||||
'@ioredis/commands@1.5.1': {}
|
||||
|
||||
@@ -16444,7 +16438,7 @@ snapshots:
|
||||
bullmq: 5.76.10
|
||||
tslib: 2.8.1
|
||||
|
||||
'@nestjs/cli@11.0.21(@swc/core@1.15.33(@swc/helpers@0.5.22))(@types/node@24.12.4)(esbuild@0.28.0)(lightningcss@1.32.0)(prettier@3.8.3)':
|
||||
'@nestjs/cli@11.0.21(@swc/core@1.15.33(@swc/helpers@0.5.21))(@types/node@24.12.4)(esbuild@0.28.0)(lightningcss@1.32.0)(prettier@3.8.3)':
|
||||
dependencies:
|
||||
'@angular-devkit/core': 19.2.24(chokidar@4.0.3)
|
||||
'@angular-devkit/schematics': 19.2.24(chokidar@4.0.3)
|
||||
@@ -16455,17 +16449,17 @@ snapshots:
|
||||
chokidar: 4.0.3
|
||||
cli-table3: 0.6.5
|
||||
commander: 4.1.1
|
||||
fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0))
|
||||
fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.21))(esbuild@0.28.0)(lightningcss@1.32.0))
|
||||
glob: 13.0.6
|
||||
node-emoji: 1.11.0
|
||||
ora: 5.4.1
|
||||
tsconfig-paths: 4.2.0
|
||||
tsconfig-paths-webpack-plugin: 4.2.0
|
||||
typescript: 5.9.3
|
||||
webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)
|
||||
webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.21))(esbuild@0.28.0)(lightningcss@1.32.0)
|
||||
webpack-node-externals: 3.0.0
|
||||
optionalDependencies:
|
||||
'@swc/core': 1.15.33(@swc/helpers@0.5.22)
|
||||
'@swc/core': 1.15.33(@swc/helpers@0.5.21)
|
||||
transitivePeerDependencies:
|
||||
- '@minify-html/node'
|
||||
- '@swc/css'
|
||||
@@ -17444,7 +17438,7 @@ snapshots:
|
||||
|
||||
'@slorber/react-helmet-async@1.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.7
|
||||
'@babel/runtime': 7.29.2
|
||||
invariant: 2.2.4
|
||||
prop-types: 15.8.1
|
||||
react: 19.2.6
|
||||
@@ -17653,7 +17647,7 @@ snapshots:
|
||||
'@swc/core-win32-x64-msvc@1.15.33':
|
||||
optional: true
|
||||
|
||||
'@swc/core@1.15.33(@swc/helpers@0.5.22)':
|
||||
'@swc/core@1.15.33(@swc/helpers@0.5.21)':
|
||||
dependencies:
|
||||
'@swc/counter': 0.1.3
|
||||
'@swc/types': 0.1.26
|
||||
@@ -17670,11 +17664,11 @@ snapshots:
|
||||
'@swc/core-win32-arm64-msvc': 1.15.33
|
||||
'@swc/core-win32-ia32-msvc': 1.15.33
|
||||
'@swc/core-win32-x64-msvc': 1.15.33
|
||||
'@swc/helpers': 0.5.22
|
||||
'@swc/helpers': 0.5.21
|
||||
|
||||
'@swc/counter@0.1.3': {}
|
||||
|
||||
'@swc/helpers@0.5.22':
|
||||
'@swc/helpers@0.5.21':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
@@ -21090,7 +21084,7 @@ snapshots:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)):
|
||||
fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.21))(esbuild@0.28.0)(lightningcss@1.32.0)):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
chalk: 4.1.2
|
||||
@@ -21105,7 +21099,7 @@ snapshots:
|
||||
semver: 7.8.0
|
||||
tapable: 2.3.3
|
||||
typescript: 5.9.3
|
||||
webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)
|
||||
webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.21))(esbuild@0.28.0)(lightningcss@1.32.0)
|
||||
|
||||
form-data-encoder@2.1.4: {}
|
||||
|
||||
@@ -25767,15 +25761,15 @@ snapshots:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
terser-webpack-plugin@5.6.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)):
|
||||
terser-webpack-plugin@5.6.0(@swc/core@1.15.33(@swc/helpers@0.5.21))(esbuild@0.28.0)(lightningcss@1.32.0)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.21))(esbuild@0.28.0)(lightningcss@1.32.0)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.3.3
|
||||
terser: 5.47.1
|
||||
webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)
|
||||
webpack: 5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.21))(esbuild@0.28.0)(lightningcss@1.32.0)
|
||||
optionalDependencies:
|
||||
'@swc/core': 1.15.33(@swc/helpers@0.5.22)
|
||||
'@swc/core': 1.15.33(@swc/helpers@0.5.21)
|
||||
esbuild: 0.28.0
|
||||
lightningcss: 1.32.0
|
||||
|
||||
@@ -26182,10 +26176,10 @@ snapshots:
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
unplugin-swc@1.5.9(@swc/core@1.15.33(@swc/helpers@0.5.22))(rollup@4.60.4):
|
||||
unplugin-swc@1.5.9(@swc/core@1.15.33(@swc/helpers@0.5.21))(rollup@4.60.4):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.4)
|
||||
'@swc/core': 1.15.33(@swc/helpers@0.5.22)
|
||||
'@swc/core': 1.15.33(@swc/helpers@0.5.21)
|
||||
load-tsconfig: 0.2.5
|
||||
unplugin: 2.3.11
|
||||
transitivePeerDependencies:
|
||||
@@ -26584,7 +26578,7 @@ snapshots:
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
||||
webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0):
|
||||
webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.21))(esbuild@0.28.0)(lightningcss@1.32.0):
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.7
|
||||
'@types/estree': 1.0.9
|
||||
@@ -26608,7 +26602,7 @@ snapshots:
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 4.3.3
|
||||
tapable: 2.3.3
|
||||
terser-webpack-plugin: 5.6.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.22))(esbuild@0.28.0)(lightningcss@1.32.0))
|
||||
terser-webpack-plugin: 5.6.0(@swc/core@1.15.33(@swc/helpers@0.5.21))(esbuild@0.28.0)(lightningcss@1.32.0)(webpack@5.106.0(@swc/core@1.15.33(@swc/helpers@0.5.21))(esbuild@0.28.0)(lightningcss@1.32.0))
|
||||
watchpack: 2.5.1
|
||||
webpack-sources: 3.4.1
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -107,8 +107,8 @@ const TimeBucketAssetResponseSchema = z
|
||||
livePhotoVideoId: z
|
||||
.array(z.string().nullable())
|
||||
.describe('Array of live photo video asset IDs (null for non-live photos)'),
|
||||
city: z.array(z.string().nullable()).describe('Array of city names extracted from EXIF GPS data'),
|
||||
country: z.array(z.string().nullable()).describe('Array of country names extracted from EXIF GPS data'),
|
||||
city: z.array(z.string().nullable()).optional().describe('Array of city names extracted from EXIF GPS data'),
|
||||
country: z.array(z.string().nullable()).optional().describe('Array of country names extracted from EXIF GPS data'),
|
||||
latitude: z
|
||||
.array(z.number().nullable())
|
||||
.optional()
|
||||
|
||||
@@ -384,8 +384,6 @@ with
|
||||
asset."fileCreatedAt" at time zone 'utc' as "fileCreatedAt",
|
||||
asset."createdAt" at time zone 'utc' as "createdAt",
|
||||
encode("asset"."thumbhash", 'base64') as "thumbhash",
|
||||
"asset_exif"."city",
|
||||
"asset_exif"."country",
|
||||
"asset_exif"."projectionType",
|
||||
coalesce(
|
||||
case
|
||||
@@ -398,6 +396,8 @@ with
|
||||
end,
|
||||
1
|
||||
) as "ratio",
|
||||
"asset_exif"."city",
|
||||
"asset_exif"."country",
|
||||
"stack"
|
||||
from
|
||||
"asset"
|
||||
@@ -432,8 +432,6 @@ with
|
||||
),
|
||||
"agg" as (
|
||||
select
|
||||
coalesce(array_agg("city"), '{}') as "city",
|
||||
coalesce(array_agg("country"), '{}') as "country",
|
||||
coalesce(array_agg("duration"), '{}') as "duration",
|
||||
coalesce(array_agg("id"), '{}') as "id",
|
||||
coalesce(array_agg("visibility"), '{}') as "visibility",
|
||||
@@ -449,6 +447,8 @@ with
|
||||
coalesce(array_agg("ratio"), '{}') as "ratio",
|
||||
coalesce(array_agg("status"), '{}') as "status",
|
||||
coalesce(array_agg("thumbhash"), '{}') as "thumbhash",
|
||||
coalesce(array_agg("city"), '{}') as "city",
|
||||
coalesce(array_agg("country"), '{}') as "country",
|
||||
coalesce(json_agg("stack"), '[]') as "stack"
|
||||
from
|
||||
"cte"
|
||||
|
||||
@@ -134,7 +134,7 @@ from
|
||||
"cte"
|
||||
where
|
||||
"cte"."distance" <= $4
|
||||
commit
|
||||
rollback
|
||||
|
||||
-- SearchRepository.searchPlaces
|
||||
select
|
||||
|
||||
@@ -786,8 +786,6 @@ export class AssetRepository {
|
||||
sql`asset."fileCreatedAt" at time zone 'utc'`.as('fileCreatedAt'),
|
||||
sql`asset."createdAt" at time zone 'utc'`.as('createdAt'),
|
||||
eb.fn('encode', ['asset.thumbhash', sql.lit('base64')]).as('thumbhash'),
|
||||
'asset_exif.city',
|
||||
'asset_exif.country',
|
||||
'asset_exif.projectionType',
|
||||
eb.fn
|
||||
.coalesce(
|
||||
@@ -801,6 +799,9 @@ export class AssetRepository {
|
||||
)
|
||||
.as('ratio'),
|
||||
])
|
||||
.$if(!auth.sharedLink || auth.sharedLink.showExif, (qb) =>
|
||||
qb.select(['asset_exif.city', 'asset_exif.country']),
|
||||
)
|
||||
.$if(!!options.withCoordinates, (qb) => qb.select(['asset_exif.latitude', 'asset_exif.longitude']))
|
||||
.where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||
.$if(options.visibility == undefined, withDefaultVisibility)
|
||||
@@ -875,8 +876,6 @@ export class AssetRepository {
|
||||
qb
|
||||
.selectFrom('cte')
|
||||
.select((eb) => [
|
||||
eb.fn.coalesce(eb.fn('array_agg', ['city']), sql.lit('{}')).as('city'),
|
||||
eb.fn.coalesce(eb.fn('array_agg', ['country']), sql.lit('{}')).as('country'),
|
||||
eb.fn.coalesce(eb.fn('array_agg', ['duration']), sql.lit('{}')).as('duration'),
|
||||
eb.fn.coalesce(eb.fn('array_agg', ['id']), sql.lit('{}')).as('id'),
|
||||
eb.fn.coalesce(eb.fn('array_agg', ['visibility']), sql.lit('{}')).as('visibility'),
|
||||
@@ -894,6 +893,12 @@ export class AssetRepository {
|
||||
eb.fn.coalesce(eb.fn('array_agg', ['status']), sql.lit('{}')).as('status'),
|
||||
eb.fn.coalesce(eb.fn('array_agg', ['thumbhash']), sql.lit('{}')).as('thumbhash'),
|
||||
])
|
||||
.$if(!auth.sharedLink || auth.sharedLink.showExif, (qb) =>
|
||||
qb.select((eb) => [
|
||||
eb.fn.coalesce(eb.fn('array_agg', ['city']), sql.lit('{}')).as('city'),
|
||||
eb.fn.coalesce(eb.fn('array_agg', ['country']), sql.lit('{}')).as('country'),
|
||||
]),
|
||||
)
|
||||
.$if(!!options.withCoordinates, (qb) =>
|
||||
qb.select((eb) => [
|
||||
eb.fn.coalesce(eb.fn('array_agg', ['latitude']), sql.lit('{}')).as('latitude'),
|
||||
|
||||
@@ -66,6 +66,10 @@ export class TimelineService extends BaseService {
|
||||
await this.requireAccess({ auth, permission: Permission.TagRead, ids: [dto.tagId] });
|
||||
}
|
||||
|
||||
if (auth.sharedLink && !auth.sharedLink.showExif) {
|
||||
dto.withCoordinates = false;
|
||||
}
|
||||
|
||||
if (dto.withPartners) {
|
||||
const requestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined;
|
||||
const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Kysely } from 'kysely';
|
||||
import { AssetVisibility } from 'src/enum';
|
||||
import { AssetVisibility, SharedLinkType } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
||||
import { DB } from 'src/schema';
|
||||
import { TimelineService } from 'src/services/timeline.service';
|
||||
import { newMediumService } from 'test/medium.factory';
|
||||
@@ -207,4 +208,32 @@ describe(TimelineService.name, () => {
|
||||
expect(response2).toEqual(expect.objectContaining({ id: [asset2.id, asset1.id], isFavorite: [true, false] }));
|
||||
});
|
||||
});
|
||||
|
||||
it('should strip geodata metadata if shared link without exif', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset } = await ctx.newAsset({
|
||||
ownerId: user.id,
|
||||
localDateTime: new Date('1970-02-12'),
|
||||
deletedAt: new Date(),
|
||||
});
|
||||
const { album } = await ctx.newAlbum({ ownerId: user.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
|
||||
const { id: sharedLinkId } = await sharedLinkRepo.create({
|
||||
allowUpload: false,
|
||||
key: Buffer.from('123'),
|
||||
type: SharedLinkType.Album,
|
||||
userId: user.id,
|
||||
albumId: album.id,
|
||||
});
|
||||
|
||||
await ctx.newExif({ assetId: asset.id, city: 'Austin', country: 'USA' });
|
||||
const auth = factory.auth({ sharedLink: { id: sharedLinkId, showExif: false } });
|
||||
const rawResponse = await sut.getTimeBucket(auth, { albumId: album.id, timeBucket: '1970-02-01', isTrashed: true });
|
||||
const response = JSON.parse(rawResponse);
|
||||
expect(response).not.toEqual(expect.objectContaining({ city: expect.any(Array), country: expect.any(Array) }));
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
"@formatjs/icu-messageformat-parser": "^3.0.0",
|
||||
"@immich/justified-layout-wasm": "^0.4.3",
|
||||
"@immich/sdk": "workspace:*",
|
||||
"@immich/ui": "^0.79.2",
|
||||
"@immich/ui": "^0.79.0",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.4.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@noble/hashes": "^2.2.0",
|
||||
|
||||
@@ -149,29 +149,35 @@
|
||||
return { width: 1, height: 1 };
|
||||
});
|
||||
|
||||
const { insetInlineStart, top, rasterWidth, rasterHeight, rasterScale } = $derived.by(() => {
|
||||
const scaleFn = objectFit === 'cover' ? scaleToCover : scaleToFit;
|
||||
const { width, height } = scaleFn(imageDimensions, container);
|
||||
if (maxRasterPixels === 0) {
|
||||
const { insetInlineStart, top, displayWidth, displayHeight, rasterWidth, rasterHeight, rasterScale } = $derived.by(
|
||||
() => {
|
||||
const scaleFn = objectFit === 'cover' ? scaleToCover : scaleToFit;
|
||||
const { width, height } = scaleFn(imageDimensions, container);
|
||||
if (maxRasterPixels === 0) {
|
||||
return {
|
||||
insetInlineStart: (container.width - width) / 2 + 'px',
|
||||
top: (container.height - height) / 2 + 'px',
|
||||
displayWidth: width + 'px',
|
||||
displayHeight: height + 'px',
|
||||
rasterWidth: width + 'px',
|
||||
rasterHeight: height + 'px',
|
||||
rasterScale: 1,
|
||||
};
|
||||
}
|
||||
const nativeRatio = imageDimensions.width / width;
|
||||
const budgetRatio = Math.sqrt(maxRasterPixels / Math.max(width * height, 1));
|
||||
const rasterRatio = Math.max(1, Math.min(nativeRatio, budgetRatio));
|
||||
return {
|
||||
insetInlineStart: (container.width - width) / 2 + 'px',
|
||||
top: (container.height - height) / 2 + 'px',
|
||||
rasterWidth: width + 'px',
|
||||
rasterHeight: height + 'px',
|
||||
rasterScale: 1,
|
||||
displayWidth: width + 'px',
|
||||
displayHeight: height + 'px',
|
||||
rasterWidth: width * rasterRatio + 'px',
|
||||
rasterHeight: height * rasterRatio + 'px',
|
||||
rasterScale: 1 / rasterRatio,
|
||||
};
|
||||
}
|
||||
const nativeRatio = imageDimensions.width / width;
|
||||
const budgetRatio = Math.sqrt(maxRasterPixels / Math.max(width * height, 1));
|
||||
const rasterRatio = Math.max(1, Math.min(nativeRatio, budgetRatio));
|
||||
return {
|
||||
insetInlineStart: (container.width - width) / 2 + 'px',
|
||||
top: (container.height - height) / 2 + 'px',
|
||||
rasterWidth: width * rasterRatio + 'px',
|
||||
rasterHeight: height * rasterRatio + 'px',
|
||||
rasterScale: 1 / rasterRatio,
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const { status } = $derived(adaptiveImageLoader);
|
||||
const alt = $derived(status.urls.preview ? $getAltText(toTimelineAsset(asset)) : '');
|
||||
@@ -216,69 +222,78 @@
|
||||
{@render backdrop?.()}
|
||||
|
||||
<div
|
||||
class="pointer-events-none absolute"
|
||||
class="pointer-events-none absolute overflow-hidden"
|
||||
style:inset-inline-start={insetInlineStart}
|
||||
style:top
|
||||
style:width={rasterWidth}
|
||||
style:height={rasterHeight}
|
||||
style:transform="scale({rasterScale})"
|
||||
style:transform-origin="0 0"
|
||||
style:will-change={maxRasterPixels > 0 ? 'transform' : undefined}
|
||||
style:width={displayWidth}
|
||||
style:height={displayHeight}
|
||||
>
|
||||
{#if show.alphaBackground}
|
||||
<AlphaBackground />
|
||||
{/if}
|
||||
|
||||
{#if show.thumbhash}
|
||||
{#if asset.thumbhash}
|
||||
<!-- Thumbhash / spinner layer -->
|
||||
<Thumbhash base64ThumbHash={asset.thumbhash} class="absolute size-full" />
|
||||
{:else if show.spinner}
|
||||
<DelayedLoadingSpinner />
|
||||
<div
|
||||
style:width={rasterWidth}
|
||||
style:height={rasterHeight}
|
||||
style:transform="scale({rasterScale})"
|
||||
style:transform-origin="0 0"
|
||||
style:will-change={maxRasterPixels > 0 ? 'transform' : undefined}
|
||||
>
|
||||
{#if show.alphaBackground}
|
||||
<AlphaBackground />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if show.thumbnail}
|
||||
<ImageLayer
|
||||
{adaptiveImageLoader}
|
||||
width={rasterWidth}
|
||||
height={rasterHeight}
|
||||
quality="thumbnail"
|
||||
src={status.urls.thumbnail}
|
||||
alt=""
|
||||
role="presentation"
|
||||
bind:ref={thumbnailElement}
|
||||
/>
|
||||
{/if}
|
||||
{#if show.thumbhash}
|
||||
{#if asset.thumbhash}
|
||||
<!-- Thumbhash / spinner layer -->
|
||||
<Thumbhash base64ThumbHash={asset.thumbhash} class="absolute size-full" />
|
||||
{:else if show.spinner}
|
||||
<DelayedLoadingSpinner />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if show.brokenAsset}
|
||||
<BrokenAsset class="absolute size-full text-xl" />
|
||||
{/if}
|
||||
{#if show.thumbnail}
|
||||
<ImageLayer
|
||||
{adaptiveImageLoader}
|
||||
width={rasterWidth}
|
||||
height={rasterHeight}
|
||||
quality="thumbnail"
|
||||
src={status.urls.thumbnail}
|
||||
alt=""
|
||||
role="presentation"
|
||||
bind:ref={thumbnailElement}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if show.preview}
|
||||
<ImageLayer
|
||||
{adaptiveImageLoader}
|
||||
{alt}
|
||||
width={rasterWidth}
|
||||
height={rasterHeight}
|
||||
{overlays}
|
||||
quality="preview"
|
||||
src={status.urls.preview}
|
||||
bind:ref={previewElement}
|
||||
/>
|
||||
{/if}
|
||||
{#if show.brokenAsset}
|
||||
<BrokenAsset class="absolute size-full text-xl" />
|
||||
{/if}
|
||||
|
||||
{#if show.original}
|
||||
<ImageLayer
|
||||
{adaptiveImageLoader}
|
||||
{alt}
|
||||
width={rasterWidth}
|
||||
height={rasterHeight}
|
||||
{overlays}
|
||||
quality="original"
|
||||
src={status.urls.original}
|
||||
bind:ref={originalElement}
|
||||
/>
|
||||
{#if show.preview}
|
||||
<ImageLayer
|
||||
{adaptiveImageLoader}
|
||||
{alt}
|
||||
width={rasterWidth}
|
||||
height={rasterHeight}
|
||||
quality="preview"
|
||||
src={status.urls.preview}
|
||||
bind:ref={previewElement}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if show.original}
|
||||
<ImageLayer
|
||||
{adaptiveImageLoader}
|
||||
{alt}
|
||||
width={rasterWidth}
|
||||
height={rasterHeight}
|
||||
quality="original"
|
||||
src={status.urls.original}
|
||||
bind:ref={originalElement}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if overlays}
|
||||
<div class="pointer-events-none absolute inset-0">
|
||||
{@render overlays()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Image from '$lib/components/Image.svelte';
|
||||
import type { AdaptiveImageLoader, ImageQuality } from '$lib/utils/adaptive-image-loader.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
type Props = {
|
||||
adaptiveImageLoader: AdaptiveImageLoader;
|
||||
@@ -12,20 +11,9 @@
|
||||
ref?: HTMLImageElement;
|
||||
width: string;
|
||||
height: string;
|
||||
overlays?: Snippet;
|
||||
};
|
||||
|
||||
let {
|
||||
adaptiveImageLoader,
|
||||
quality,
|
||||
src,
|
||||
alt = '',
|
||||
role,
|
||||
ref = $bindable(),
|
||||
width,
|
||||
height,
|
||||
overlays,
|
||||
}: Props = $props();
|
||||
let { adaptiveImageLoader, quality, src, alt = '', role, ref = $bindable(), width, height }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#key adaptiveImageLoader}
|
||||
@@ -42,6 +30,5 @@
|
||||
draggable={false}
|
||||
data-testid={quality}
|
||||
/>
|
||||
{@render overlays?.()}
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
@@ -179,8 +179,8 @@ export class TimelineMonth {
|
||||
);
|
||||
|
||||
const timelineAsset: TimelineAsset = {
|
||||
city: bucketAssets.city[i],
|
||||
country: bucketAssets.country[i],
|
||||
city: bucketAssets.city?.[i] ?? null,
|
||||
country: bucketAssets.country?.[i] ?? null,
|
||||
duration: bucketAssets.duration[i],
|
||||
id: bucketAssets.id[i],
|
||||
visibility: bucketAssets.visibility[i],
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { Route } from '$lib/route';
|
||||
import { handleCreateWorkflow } from '$lib/services/workflow.service';
|
||||
import { WorkflowTrigger, type WorkflowResponseDto } from '@immich/sdk';
|
||||
import { Field, FormModal, Input, Textarea, VStack } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
workflow: WorkflowResponseDto;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const { workflow, onClose }: Props = $props();
|
||||
|
||||
let name = $state(workflow.name ?? '');
|
||||
let description = $state(workflow.description ?? '');
|
||||
let trigger = $state<WorkflowTrigger>(workflow.trigger);
|
||||
|
||||
const onSubmit = async () => {
|
||||
const response = await handleCreateWorkflow({
|
||||
name,
|
||||
description,
|
||||
trigger,
|
||||
steps: workflow.steps,
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
if (response) {
|
||||
await goto(Route.viewWorkflow({ id: response.id }));
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<FormModal
|
||||
title={$t('duplicate_workflow')}
|
||||
{onClose}
|
||||
{onSubmit}
|
||||
disabled={!name || !trigger}
|
||||
size="medium"
|
||||
submitText={$t('create')}
|
||||
>
|
||||
<VStack gap={4}>
|
||||
<Field label={$t('name')} required>
|
||||
<Input placeholder={$t('workflow_name')} bind:value={name} />
|
||||
</Field>
|
||||
|
||||
<Field label={$t('description')}>
|
||||
<Textarea grow placeholder={$t('workflow_description')} bind:value={description} />
|
||||
</Field>
|
||||
</VStack>
|
||||
</FormModal>
|
||||
@@ -42,7 +42,7 @@ import AssetAddToAlbumModal from '$lib/modals/AssetAddToAlbumModal.svelte';
|
||||
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { getAssetMediaUrl, getSharedLink, sleep } from '$lib/utils';
|
||||
import { downloadUrl } from '$lib/utils';
|
||||
import { downloadUrl } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ import { toastManager, type ActionItem } from '@immich/ui';
|
||||
import { mdiContentCopy, mdiDownload, mdiUpload } from '@mdi/js';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { copyToClipboard, downloadJson } from '$lib/utils';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { downloadBlob } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
|
||||
@@ -17,7 +19,7 @@ export const getSystemConfigActions = (
|
||||
title: $t('copy_to_clipboard'),
|
||||
description: $t('admin.copy_config_to_clipboard_description'),
|
||||
icon: mdiContentCopy,
|
||||
onAction: () => copyToClipboard(config),
|
||||
onAction: () => handleCopyToClipboard(config),
|
||||
shortcuts: { shift: true, key: 'c' },
|
||||
};
|
||||
|
||||
@@ -25,7 +27,7 @@ export const getSystemConfigActions = (
|
||||
title: $t('export_as_json'),
|
||||
description: $t('admin.export_config_as_json_description'),
|
||||
icon: mdiDownload,
|
||||
onAction: () => downloadJson(config, 'immich-config.json'),
|
||||
onAction: () => handleDownloadConfig(config),
|
||||
shortcuts: [
|
||||
{ shift: true, key: 's' },
|
||||
{ shift: true, key: 'd' },
|
||||
@@ -63,6 +65,31 @@ export const handleSystemConfigSave = async (update: Partial<SystemConfigDto>) =
|
||||
}
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/questions/16167581/sort-object-properties-and-json-stringify/43636793#43636793
|
||||
const jsonReplacer = (_key: string, value: unknown) =>
|
||||
value instanceof Object && !Array.isArray(value)
|
||||
? Object.keys(value)
|
||||
.sort()
|
||||
// eslint-disable-next-line unicorn/no-array-reduce
|
||||
.reduce((sorted: { [key: string]: unknown }, key) => {
|
||||
sorted[key] = (value as { [key: string]: unknown })[key];
|
||||
return sorted;
|
||||
}, {})
|
||||
: value;
|
||||
|
||||
export const handleCopyToClipboard = async (config: SystemConfigDto) => {
|
||||
await copyToClipboard(JSON.stringify(config, jsonReplacer, 2));
|
||||
};
|
||||
|
||||
export const handleDownloadConfig = (config: SystemConfigDto) => {
|
||||
const blob = new Blob([JSON.stringify(config, jsonReplacer, 2)], { type: 'application/json' });
|
||||
const downloadKey = 'immich-config.json';
|
||||
downloadManager.add(downloadKey, blob.size);
|
||||
downloadManager.update(downloadKey, blob.size);
|
||||
downloadBlob(blob, downloadKey);
|
||||
setTimeout(() => downloadManager.clear(downloadKey), 5000);
|
||||
};
|
||||
|
||||
export const handleUploadConfig = () => {
|
||||
const input = globalThis.document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
|
||||
@@ -10,25 +10,12 @@ import {
|
||||
type WorkflowUpdateDto,
|
||||
} from '@immich/sdk';
|
||||
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||
import {
|
||||
mdiCodeJson,
|
||||
mdiContentCopy,
|
||||
mdiContentDuplicate,
|
||||
mdiDeleteOutline,
|
||||
mdiDownload,
|
||||
mdiFileDocumentMultipleOutline,
|
||||
mdiPause,
|
||||
mdiPencil,
|
||||
mdiPlay,
|
||||
mdiPlus,
|
||||
} from '@mdi/js';
|
||||
import { mdiCodeJson, mdiDelete, mdiFileDocumentMultipleOutline, mdiPause, mdiPencil, mdiPlay, mdiPlus } from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import WorkflowDuplicateModal from '$lib/modals/WorkflowDuplicateModal.svelte';
|
||||
import WorkflowTemplatePicker from '$lib/modals/WorkflowTemplatePicker.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { copyToClipboard, downloadJson } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
|
||||
@@ -60,50 +47,10 @@ export const getWorkflowActions = ($t: MessageFormatter, workflow: WorkflowRespo
|
||||
const ToggleEnabled: ActionItem = {
|
||||
title: workflow.enabled ? $t('disable') : $t('enable'),
|
||||
icon: workflow.enabled ? mdiPause : mdiPlay,
|
||||
color: workflow.enabled ? 'danger' : 'primary',
|
||||
onAction: () => handleUpdateWorkflow(workflow.id, { enabled: !workflow.enabled }),
|
||||
};
|
||||
|
||||
const CopyJson: ActionItem = {
|
||||
title: $t('copy_json'),
|
||||
icon: mdiContentCopy,
|
||||
onAction: () =>
|
||||
copyToClipboard(
|
||||
JSON.stringify(
|
||||
{
|
||||
name: workflow.name,
|
||||
description: workflow.description,
|
||||
enabled: workflow.enabled,
|
||||
trigger: workflow.trigger,
|
||||
steps: workflow.steps,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
const Download: ActionItem = {
|
||||
title: $t('download'),
|
||||
icon: mdiDownload,
|
||||
onAction: () =>
|
||||
downloadJson(
|
||||
{
|
||||
name: workflow.name,
|
||||
description: workflow.description,
|
||||
enabled: workflow.enabled,
|
||||
trigger: workflow.trigger,
|
||||
steps: workflow.steps,
|
||||
},
|
||||
'workflow.json',
|
||||
),
|
||||
};
|
||||
|
||||
const Duplicate: ActionItem = {
|
||||
title: $t('duplicate'),
|
||||
icon: mdiContentDuplicate,
|
||||
onAction: async () => modalManager.show(WorkflowDuplicateModal, { workflow }),
|
||||
};
|
||||
|
||||
const Edit: ActionItem = {
|
||||
title: $t('edit'),
|
||||
icon: mdiPencil,
|
||||
@@ -112,12 +59,14 @@ export const getWorkflowActions = ($t: MessageFormatter, workflow: WorkflowRespo
|
||||
|
||||
const Delete: ActionItem = {
|
||||
title: $t('delete'),
|
||||
icon: mdiDeleteOutline,
|
||||
icon: mdiDelete,
|
||||
color: 'danger',
|
||||
onAction: () => handleDeleteWorkflow(workflow),
|
||||
onAction: async () => {
|
||||
await handleDeleteWorkflow(workflow);
|
||||
},
|
||||
};
|
||||
|
||||
return { CopyJson, Download, Duplicate, ToggleEnabled, Edit, Delete };
|
||||
return { ToggleEnabled, Edit, Delete };
|
||||
};
|
||||
|
||||
export const getWorkflowShowSchemaAction = (
|
||||
@@ -136,10 +85,10 @@ export const handleCreateWorkflow = async (dto: WorkflowCreateDto) => {
|
||||
try {
|
||||
const response = await createWorkflow({ workflowCreateDto: dto });
|
||||
eventManager.emit('WorkflowCreate', response);
|
||||
toastManager.success();
|
||||
return response;
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_create'));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+2
-39
@@ -24,7 +24,6 @@ import { init, register, t } from 'svelte-i18n';
|
||||
import { derived, get } from 'svelte/store';
|
||||
import { defaultLang, locales } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
||||
import { alwaysLoadOriginalFile, lang } from '$lib/stores/preferences.store';
|
||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@@ -250,53 +249,17 @@ export const getProfileImageUrl = (user: UserResponseDto) =>
|
||||
export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: string) =>
|
||||
createUrl(getPeopleThumbnailPath(person.id), { updatedAt: updatedAt ?? person.updatedAt });
|
||||
|
||||
export const copyToClipboard = async (secret: string | unknown) => {
|
||||
export const copyToClipboard = async (secret: string) => {
|
||||
const $t = get(t);
|
||||
|
||||
try {
|
||||
const value = typeof secret === 'string' ? secret : JSON.stringify(secret, jsonReplacer, 2);
|
||||
await navigator.clipboard.writeText(value);
|
||||
await navigator.clipboard.writeText(secret);
|
||||
toastManager.info($t('copied_to_clipboard'));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_copy_to_clipboard'));
|
||||
}
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/questions/16167581/sort-object-properties-and-json-stringify/43636793#43636793
|
||||
const jsonReplacer = (_key: string, value: unknown) =>
|
||||
value instanceof Object && !Array.isArray(value)
|
||||
? Object.keys(value)
|
||||
.sort()
|
||||
// eslint-disable-next-line unicorn/no-array-reduce
|
||||
.reduce((sorted: { [key: string]: unknown }, key) => {
|
||||
sorted[key] = (value as { [key: string]: unknown })[key];
|
||||
return sorted;
|
||||
}, {})
|
||||
: value;
|
||||
|
||||
export const downloadUrl = (url: string, filename: string) => {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = filename;
|
||||
|
||||
document.body.append(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
export const downloadBlob = (data: Blob, filename: string) => downloadUrl(URL.createObjectURL(data), filename);
|
||||
|
||||
export const downloadJson = (data: unknown, filename: string) => {
|
||||
const blob = new Blob([JSON.stringify(data, jsonReplacer, 2)], { type: 'application/json' });
|
||||
const downloadKey = filename;
|
||||
downloadManager.add(downloadKey, blob.size);
|
||||
downloadManager.update(downloadKey, blob.size);
|
||||
downloadBlob(blob, downloadKey);
|
||||
setTimeout(() => downloadManager.clear(downloadKey), 5000);
|
||||
};
|
||||
|
||||
export const oauth = {
|
||||
isCallback: (location: Location) => {
|
||||
const search = location.search;
|
||||
|
||||
@@ -26,7 +26,7 @@ import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { downloadBlob, downloadRequest, withError } from '$lib/utils';
|
||||
import { downloadRequest, withError } from '$lib/utils';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
@@ -73,6 +73,32 @@ export const removeTag = async ({
|
||||
return assetIds;
|
||||
};
|
||||
|
||||
export const downloadBlob = (data: Blob, filename: string) => {
|
||||
const url = URL.createObjectURL(data);
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = filename;
|
||||
|
||||
document.body.append(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
export const downloadUrl = (url: string, filename: string) => {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = filename;
|
||||
|
||||
document.body.append(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
export const downloadArchive = async (fileName: string, options: Omit<DownloadInfoDto, 'archiveSize'>) => {
|
||||
const archiveSize = authManager.authenticated ? authManager.preferences.download.archiveSize : undefined;
|
||||
const dto = { ...options, archiveSize };
|
||||
|
||||
@@ -16,11 +16,12 @@
|
||||
CardTitle,
|
||||
CodeBlock,
|
||||
Container,
|
||||
ContextMenuButton,
|
||||
Icon,
|
||||
IconButton,
|
||||
MenuItemType,
|
||||
menuManager,
|
||||
} from '@immich/ui';
|
||||
import { mdiClose, mdiFlashOutline } from '@mdi/js';
|
||||
import { mdiClose, mdiDotsVertical, mdiFlashOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import type { PageData } from './$types';
|
||||
@@ -35,7 +36,7 @@
|
||||
|
||||
const expandedIds = new SvelteSet<string>();
|
||||
|
||||
const onToggleExpand = (id: string) => {
|
||||
const toggleExpanded = (id: string) => {
|
||||
if (expandedIds.has(id)) {
|
||||
expandedIds.delete(id);
|
||||
} else {
|
||||
@@ -43,6 +44,21 @@
|
||||
}
|
||||
};
|
||||
|
||||
const showWorkflowMenu = (event: MouseEvent, workflow: WorkflowResponseDto) => {
|
||||
const { ToggleEnabled, Edit, Delete } = getWorkflowActions($t, workflow);
|
||||
void menuManager.show({
|
||||
target: event.currentTarget as HTMLElement,
|
||||
position: 'top-left',
|
||||
items: [
|
||||
ToggleEnabled,
|
||||
Edit,
|
||||
getWorkflowShowSchemaAction($t, expandedIds.has(workflow.id), () => toggleExpanded(workflow.id)),
|
||||
MenuItemType.Divider,
|
||||
Delete,
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const { Create, UseTemplate } = $derived(getWorkflowsActions($t));
|
||||
|
||||
const onWorkflowCreate = async (response: WorkflowResponseDto) => {
|
||||
@@ -75,8 +91,6 @@
|
||||
{:else}
|
||||
<div class="my-6 flex flex-col gap-3">
|
||||
{#each workflows as workflow (workflow.id)}
|
||||
{@const { ToggleEnabled, Duplicate, Edit, Delete } = getWorkflowActions($t, workflow)}
|
||||
|
||||
<Card class="group shadow-none transition-colors hover:border-primary">
|
||||
<CardHeader>
|
||||
<a
|
||||
@@ -114,16 +128,17 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ContextMenuButton
|
||||
position="top-left"
|
||||
items={[
|
||||
ToggleEnabled,
|
||||
Edit,
|
||||
Duplicate,
|
||||
getWorkflowShowSchemaAction($t, expandedIds.has(workflow.id), () => onToggleExpand(workflow.id)),
|
||||
MenuItemType.Divider,
|
||||
Delete,
|
||||
]}
|
||||
<IconButton
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
icon={mdiDotsVertical}
|
||||
aria-label={$t('menu')}
|
||||
onclick={(event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
showWorkflowMenu(event, workflow);
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
|
||||
@@ -137,7 +152,7 @@
|
||||
fullWidth
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
onclick={() => onToggleExpand(workflow.id)}
|
||||
onclick={() => toggleExpanded(workflow.id)}
|
||||
>
|
||||
{$t('close')}
|
||||
</Button>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import WorkflowEditStepModal from '$lib/modals/WorkflowEditStepModal.svelte';
|
||||
import WorkflowTriggerPicker from '$lib/modals/WorkflowTriggerPicker.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getWorkflowActions, handleUpdateWorkflow } from '$lib/services/workflow.service';
|
||||
import { handleUpdateWorkflow } from '$lib/services/workflow.service';
|
||||
import { getTriggerDescription, getTriggerName } from '$lib/utils/workflow';
|
||||
import type { WorkflowResponseDto, WorkflowStepDto, WorkflowUpdateDto } from '@immich/sdk';
|
||||
import {
|
||||
@@ -83,7 +83,7 @@
|
||||
let dropTargetIndex = $state<number | null>(null);
|
||||
|
||||
const workflowSummary = $derived({ name, description, trigger, steps });
|
||||
const workflowJsonContent = $derived<WorkflowJsonContent>({ name, description, enabled, trigger, steps });
|
||||
const workflowJsonContent = $derived<WorkflowJsonContent>({ description, enabled, name, steps, trigger });
|
||||
|
||||
const hasChanges = $derived(
|
||||
enabled !== savedWorkflow.enabled ||
|
||||
@@ -217,12 +217,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const onWorkflowDelete = async (response: WorkflowResponseDto) => {
|
||||
if (id === response.id) {
|
||||
await goto(Route.workflows());
|
||||
}
|
||||
};
|
||||
|
||||
const confirmNavigation = async () => {
|
||||
if (!hasChanges) {
|
||||
return true;
|
||||
@@ -279,73 +273,60 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const { Download, Duplicate, CopyJson, Delete } = $derived(
|
||||
getWorkflowActions($t, { ...savedWorkflow, name, description, enabled, trigger, steps }),
|
||||
);
|
||||
</script>
|
||||
|
||||
<OnEvents {onWorkflowUpdate} {onWorkflowDelete} />
|
||||
<OnEvents {onWorkflowUpdate} />
|
||||
|
||||
<AppShell class="">
|
||||
<AppShellBar>
|
||||
<ActionBar
|
||||
shape="round"
|
||||
static
|
||||
{onClose}
|
||||
translations={{ close: $t('back') }}
|
||||
closeIcon={mdiArrowLeft}
|
||||
actions={[Duplicate, CopyJson, Download, Delete].map((item) => ({ ...item, color: undefined }))}
|
||||
>
|
||||
<ActionBar static {onClose} translations={{ close: $t('back') }} closeIcon={mdiArrowLeft}>
|
||||
<ControlBarHeader>
|
||||
<ControlBarTitle>{data.workflow.name}</ControlBarTitle>
|
||||
<ControlBarDescription>{data.workflow.description}</ControlBarDescription>
|
||||
</ControlBarHeader>
|
||||
<ControlBarContent class="flex items-center justify-end gap-6">
|
||||
{#if hasChanges}
|
||||
<div class="flex gap-1 rounded-full border border-light-200 bg-light p-1" role="group">
|
||||
<Button
|
||||
variant="filled"
|
||||
variant={editMode === 'visual' ? 'filled' : 'ghost'}
|
||||
color={editMode === 'visual' ? 'primary' : 'secondary'}
|
||||
size="small"
|
||||
color="primary"
|
||||
leadingIcon={mdiContentSave}
|
||||
disabled={!hasChanges || isSaving}
|
||||
loading={isSaving}
|
||||
onclick={saveWorkflow}
|
||||
leadingIcon={mdiFormatListBulletedSquare}
|
||||
aria-pressed={editMode === 'visual'}
|
||||
onclick={() => (editMode = 'visual')}
|
||||
shape="round"
|
||||
>
|
||||
{$t('save')}
|
||||
{$t('visual')}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
variant={editMode === 'json' ? 'filled' : 'ghost'}
|
||||
color={editMode === 'json' ? 'primary' : 'secondary'}
|
||||
size="small"
|
||||
leadingIcon={mdiCodeJson}
|
||||
aria-pressed={editMode === 'json'}
|
||||
onclick={() => (editMode = 'json')}
|
||||
shape="round"
|
||||
>
|
||||
JSON
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="filled"
|
||||
size="small"
|
||||
color="primary"
|
||||
leadingIcon={mdiContentSave}
|
||||
disabled={!hasChanges || isSaving}
|
||||
loading={isSaving}
|
||||
onclick={saveWorkflow}
|
||||
>
|
||||
{$t('save')}
|
||||
</Button>
|
||||
</ControlBarContent>
|
||||
</ActionBar>
|
||||
</AppShellBar>
|
||||
|
||||
<Container size="medium" class="pt-8 pb-24" center>
|
||||
<VStack gap={4}>
|
||||
<div class="flex gap-1 rounded-full border border-light-200 bg-light p-1" role="group">
|
||||
<Button
|
||||
variant={editMode === 'visual' ? 'filled' : 'ghost'}
|
||||
color={editMode === 'visual' ? 'primary' : 'secondary'}
|
||||
size="small"
|
||||
leadingIcon={mdiFormatListBulletedSquare}
|
||||
aria-pressed={editMode === 'visual'}
|
||||
onclick={() => (editMode = 'visual')}
|
||||
shape="round"
|
||||
>
|
||||
{$t('visual')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={editMode === 'json' ? 'filled' : 'ghost'}
|
||||
color={editMode === 'json' ? 'primary' : 'secondary'}
|
||||
size="small"
|
||||
leadingIcon={mdiCodeJson}
|
||||
aria-pressed={editMode === 'json'}
|
||||
onclick={() => (editMode = 'json')}
|
||||
shape="round"
|
||||
>
|
||||
JSON
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if editMode === 'visual'}
|
||||
<Card class="shadow-none" expandable>
|
||||
<CardHeader>
|
||||
@@ -373,8 +354,9 @@
|
||||
bind:value={() => name ?? '', (value) => (name = value || null)}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={$t('description')}>
|
||||
<Field label={$t('description')} for="workflow-description">
|
||||
<Textarea
|
||||
id="workflow-description"
|
||||
grow
|
||||
placeholder={$t('workflow_description')}
|
||||
bind:value={() => description ?? '', (value) => (description = value || null)}
|
||||
|
||||
@@ -76,8 +76,8 @@ export const toResponseDto = (...timelineAsset: TimelineAsset[]) => {
|
||||
};
|
||||
for (const asset of timelineAsset) {
|
||||
const fileCreatedAt = fromTimelinePlainDateTime(asset.fileCreatedAt).toISO();
|
||||
bucketAssets.city.push(asset.city);
|
||||
bucketAssets.country.push(asset.country);
|
||||
bucketAssets.city?.push(asset.city);
|
||||
bucketAssets.country?.push(asset.country);
|
||||
bucketAssets.duration.push(asset.duration!);
|
||||
bucketAssets.id.push(asset.id);
|
||||
bucketAssets.visibility.push(asset.visibility);
|
||||
|
||||
Reference in New Issue
Block a user