Compare commits

...

52 Commits

Author SHA1 Message Date
Min Idzelis 9806e94f6f wip 2025-04-30 03:50:13 +00:00
Jason Rasmussen 2e8a286540 refactor: smart search queue (#17977) 2025-04-29 17:44:28 -04:00
Jason Rasmussen 038a82c4f1 refactor: theme manager (#17976) 2025-04-29 17:44:09 -04:00
renovate[bot] 2c2dd01bf0 fix(deps): update machine-learning (#17951) 2025-04-29 20:02:58 +00:00
Ben ac73e163df chore(mobile): translate toast messages (#17964) 2025-04-29 14:26:41 -05:00
Jason Rasmussen d89e88bb3f feat: configure token endpoint auth method (#17968) 2025-04-29 15:17:48 -04:00
Thomas 3ce353393a chore(server): don't insert embeddings if the model has changed (#17885)
* chore(server): don't insert embeddings if the model has changed

We're moving away from the heuristic of waiting for queues to complete. The job
which inserts embeddings can simply check if the model has changed before
inserting, rather than attempting to lock the queue.

* more robust dim size update

* use check constraint

* index command cleanup

* add create statement

* update medium test, create appropriate extension

* new line

* set dimension size when running on all assets

* why does it want braces smh

* take 2

---------

Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
2025-04-29 14:23:01 -04:00
Min Idzelis 0e4cf9ac57 feat(web): responsive date group header height (#17944)
* feat: responsive date group header height

* update tests

* feat(web): improve perf when changing mobile orientation (#17945)

fix: improve perf when changing mobile orientation
2025-04-29 13:59:06 -04:00
Min Idzelis 07290580a6 feat: improve semantic nav/main tags (#17800)
feat: nav/main elements

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-04-29 13:51:39 -04:00
AverageHelper d9ce74b896 chore: add security.txt (#17952)
* feat: Create .well-known/security.txt

* feat: Add another security.txt for the main website

* fix: deploy hidden files

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-04-29 13:48:06 -04:00
Jason Rasmussen 4c0f79b162 fix: use lint:p in checkall script (#17969) 2025-04-29 17:34:36 +00:00
renovate[bot] 9851d24628 chore(deps): update docker.io/valkey/valkey:8-bookworm docker digest to c855f98 (#17948)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 12:08:50 +01:00
renovate[bot] fe6cbd93b1 chore(deps): pin dependencies (#17947)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 12:08:40 +01:00
renovate[bot] df20788088 chore(deps): update grafana/grafana docker tag to v11.6.1 (#17955)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 12:08:08 +01:00
renovate[bot] 3d042cc7f1 fix(deps): update typescript-projects (#17961)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 13:00:37 +02:00
renovate[bot] 85446c5862 chore(deps): update redis:6.2-alpine docker digest to 3211c33 (#17950)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 10:09:25 +00:00
renovate[bot] fb52ac0f5b chore(deps): update node.js to v22.15.0 (#17956)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 12:08:32 +02:00
Eli Gao 48bcbee6ed feat(server): JXL previews from DNG 1.7+ (#17861)
* feat(server): JXL previews from RAW

* refactor(server): use var name assumedExtractedFormat for clarity

* test(server): fix existing media.extract() returning JPEG

* chore(openapi): regen

* style(server): lint

* fix(server): ignore undefined decode orientation

* fix(server): correct orientation assignment in media decode options

* test(server): unit tests of JXL-encoded DNG

* refactor(server): return buffer and format from mediaRepository.extract()

* chore(open-api): regen

* refactor

---------

Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
2025-04-28 18:18:46 -04:00
Daniel Dietzler f621f8ef2c refactor: more job queries (#17745) 2025-04-29 00:03:20 +02:00
Jason Rasmussen 7f69abbf0d refactor: app init event (#17937) 2025-04-28 14:48:33 -04:00
Jason Rasmussen 895b2bf5cd refactor: download manager (#17935) 2025-04-28 14:21:24 -04:00
Jason Rasmussen f64e6f5dc3 refactor: auth login event (#17934) 2025-04-28 14:13:14 -04:00
Luke Towers 64e738f79d feat(web): move duplicates controls above preview of duplicate images (#17837)
Move duplicates controls above preview of duplicate images
2025-04-28 16:10:40 +00:00
Daniel Dietzler a17390a422 refactor: move managers to new folder (#17929) 2025-04-28 16:56:04 +02:00
Jason Rasmussen 1b5fc9c665 feat: notifications (#17701)
* feat: notifications

* UI works

* chore: pr feedback

* initial fetch and clear notification upon logging out

* fix: merge

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-04-28 10:36:14 -04:00
Yaros 23717ce981 feat(mobile): save grid size on gesture resize (#17891) 2025-04-28 09:23:33 -05:00
Min Idzelis 2fd05e8447 feat: preload and cancel images with a service worker (#16893)
* feat: Service Worker to preload/cancel images and other resources

* Remove caddy configuration, localhost is secure if port-forwarded

* fix e2e tests

* Cache/return the app.html for all web entry points

* Only handle preload/cancel

* fix e2e

* fix e2e

* e2e-2

* that'll do it

* format

* fix test

* lint

* refactor common code to conditionals

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-04-28 14:23:05 +00:00
Min Idzelis c664d99a34 refactor: vscode - format/organize on save (#17928) 2025-04-28 10:11:19 -04:00
Andreas Tollkötter 21c7d70336 feat(mobile): Capitalize month names in asset grid (#17898)
* capitalize month titles

* capitalize day titles as well
2025-04-28 13:56:36 +00:00
Jason Rasmussen ad272333db refactor: user avatar color (#17753) 2025-04-28 08:54:51 -05:00
Zack Pollard 460d594791 feat: api response compression (#17878) 2025-04-28 08:54:11 -05:00
Jason Rasmussen e6c575c33e feat: rtl (#17860) 2025-04-28 08:53:53 -05:00
Andreas Tollkötter 85ac0512a6 fix(web): Make date-time formatting follow locale (#17899)
* fixed missing $locale parameter to .toLocaleString

* Remove unused types and functions in timeline-util

* remove unused export

* re-enable export because it is needed for tests

* format
2025-04-28 08:53:26 -05:00
Alex 205260d31c chore: post release tasks (#17895) 2025-04-27 23:02:03 -05:00
Alex 3858973be5 chore(mobile): translation (#17920) 2025-04-27 23:00:40 -05:00
github-actions 02994883fe chore: version v1.132.3 2025-04-25 19:44:05 +00:00
Alex a1f8150c30 fix: Authelia OAuth code verifier value contains invalid characters (#17886)
* fix(mobile): Authelia OAuth code verifier value contains invalid characters

* Refactor

* Refactoring with Jason

* Refactoring with Jason
2025-04-25 19:39:14 +00:00
Yaros d85ef19bfc fix(mobile): revert get location on app start (#17882) 2025-04-25 10:38:30 -05:00
Jason Rasmussen d0014bdf94 refactor: event manager (#17862)
* refactor: event manager

* refactor: event manager
2025-04-25 08:36:31 -04:00
Martin Mikita e822e3eca9 docs: update MapTiler name (#17863) 2025-04-25 08:57:44 +00:00
Alex 644defa4a1 chore: post release tasks (#17867) 2025-04-25 04:14:40 +00:00
Matthew Momjian 1fe3c7b9b3 fix(docs): priorities (Capitalization) (#17866)
priorities
2025-04-25 04:07:42 +00:00
github-actions 0d60be3d87 chore: version v1.132.2 2025-04-25 03:07:06 +00:00
Alex 765da7b182 fix(mobile): mobile migration logic (#17865)
* fix(mobile): mobile migration logic

* add exception

* remove unused comment

* finalize
2025-04-25 00:16:54 +00:00
shenlong b037158028 fix(mobile): auto trash using MANAGE_MEDIA (#17828)
fix: auto trash using MANAGE_MEDIA

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-04-24 19:09:50 -05:00
Daimolean a03902f174 fix(docs): incorrect date sorting (#17858) 2025-04-24 19:40:52 -04:00
Jason Rasmussen 1d610ad9cb refactor: database connection parsing (#17852) 2025-04-24 12:58:29 -04:00
Min Idzelis dab4870fed fix: flappy e2e test (#17832)
* fix: flappy e2e test

* lint
2025-04-23 23:30:13 -04:00
github-actions 37f5e6e2cb chore: version v1.132.1 2025-04-23 21:43:47 +00:00
Alex 57d622bc43 chore: post release tasks (#17816) 2025-04-23 16:41:08 -05:00
Alex c167e46ec7 chore: revert #16732 (#17819)
* chore: revert #16732

* lint
2025-04-23 16:40:59 -05:00
Mert 6ce8a1deeb fix(server): bump sharp (#17818)
* bump sharp

* test linking

* link in prod image too

* force global

* keep unnecessary libraries

* override sharp version

* revert dockerfile changes

* add node-gyp and napi

* dev dependency
2025-04-23 17:08:29 -04:00
341 changed files with 8403 additions and 3153 deletions
+1 -1
View File
@@ -96,7 +96,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }} type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
with: with:
file: cli/Dockerfile file: cli/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
+3 -3
View File
@@ -50,7 +50,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3 uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -63,7 +63,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@45775bd8235c68ba998cffa5171334d58593da47 # v3 uses: github/codeql-action/autobuild@28deaeda66b76a05916b6923827895f2b14ab387 # v3
# ️ Command-line programs to run using the OS shell. # ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -76,6 +76,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3 uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'
+4 -4
View File
@@ -205,7 +205,7 @@ jobs:
- name: Build and push image - name: Build and push image
id: build id: build
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
with: with:
context: ${{ env.context }} context: ${{ env.context }}
file: ${{ env.file }} file: ${{ env.file }}
@@ -266,7 +266,7 @@ jobs:
- build_and_push_ml - build_and_push_ml
steps: steps:
- name: Download digests - name: Download digests
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with: with:
path: ${{ runner.temp }}/digests path: ${{ runner.temp }}/digests
pattern: ml-digests-${{ matrix.device }}-* pattern: ml-digests-${{ matrix.device }}-*
@@ -407,7 +407,7 @@ jobs:
- name: Build and push image - name: Build and push image
id: build id: build
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
with: with:
context: ${{ env.context }} context: ${{ env.context }}
file: ${{ env.file }} file: ${{ env.file }}
@@ -454,7 +454,7 @@ jobs:
- build_and_push_server - build_and_push_server
steps: steps:
- name: Download digests - name: Download digests
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with: with:
path: ${{ runner.temp }}/digests path: ${{ runner.temp }}/digests
pattern: server-digests-* pattern: server-digests-*
+1
View File
@@ -72,4 +72,5 @@ jobs:
with: with:
name: docs-build-output name: docs-build-output
path: docs/build/ path: docs/build/
include-hidden-files: true
retention-days: 1 retention-days: 1
+1 -1
View File
@@ -95,7 +95,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Download APK - name: Download APK
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with: with:
name: release-apk-signed name: release-apk-signed
+3 -3
View File
@@ -105,12 +105,12 @@ jobs:
actions: read actions: read
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: with:
persist-credentials: false persist-credentials: false
- name: Install the latest version of uv - name: Install the latest version of uv
uses: astral-sh/setup-uv@v5 uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
- name: Run zizmor 🌈 - name: Run zizmor 🌈
run: uvx zizmor --format=sarif . > results.sarif run: uvx zizmor --format=sarif . > results.sarif
@@ -118,7 +118,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file - name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v3 uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3
with: with:
sarif_file: results.sarif sarif_file: results.sarif
category: zizmor category: zizmor
+1 -1
View File
@@ -461,7 +461,7 @@ jobs:
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
# TODO: add caching when supported (https://github.com/actions/setup-python/pull/818) # TODO: add caching when supported (https://github.com/actions/setup-python/pull/818)
# with: # with:
# python-version: 3.11 # python-version: 3.11
+48 -30
View File
@@ -1,45 +1,63 @@
{ {
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[css]": { "[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2, "editor.formatOnSave": true,
"editor.formatOnSave": true
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode",
"editor.tabSize": 2 "editor.tabSize": 2
}, },
"svelte.enable-ts-plugin": true,
"eslint.validate": [
"javascript",
"svelte"
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"[dart]": { "[dart]": {
"editor.defaultFormatter": "Dart-Code.dart-code",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.selectionHighlight": false, "editor.selectionHighlight": false,
"editor.suggest.snippetsPreventQuickSuggestions": false, "editor.suggest.snippetsPreventQuickSuggestions": false,
"editor.suggestSelection": "first", "editor.suggestSelection": "first",
"editor.tabCompletion": "onlySnippets", "editor.tabCompletion": "onlySnippets",
"editor.wordBasedSuggestions": "off", "editor.wordBasedSuggestions": "off"
"editor.defaultFormatter": "Dart-Code.dart-code"
}, },
"cSpell.words": [ "[javascript]": {
"immich" "editor.codeActionsOnSave": {
], "source.organizeImports": "explicit",
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"[svelte]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "svelte.svelte-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"[typescript]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.tabSize": 2
},
"cSpell.words": ["immich"],
"editor.formatOnSave": true,
"eslint.validate": ["javascript", "svelte"],
"explorer.fileNesting.enabled": true, "explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": { "explorer.fileNesting.patterns": {
"*.ts": "${capture}.spec.ts,${capture}.mock.ts", "*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart" "*.ts": "${capture}.spec.ts,${capture}.mock.ts"
} },
"svelte.enable-ts-plugin": true,
"typescript.preferences.importModuleSpecifier": "non-relative"
} }
+3
View File
@@ -17,6 +17,9 @@ e2e:
prod: prod:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
prod-down:
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
prod-scale: prod-scale:
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:22.14.0-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS core FROM node:22.15.0-alpine3.20@sha256:686b8892b69879ef5bfd6047589666933508f9a5451c67320df3070ba0e9807b AS core
WORKDIR /usr/src/open-api/typescript-sdk WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
+124 -136
View File
@@ -1,12 +1,12 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.62", "version": "2.2.65",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.62", "version": "2.2.65",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"chokidar": "^4.0.3", "chokidar": "^4.0.3",
@@ -54,7 +54,7 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.132.0", "version": "1.132.3",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
@@ -647,9 +647,9 @@
} }
}, },
"node_modules/@eslint/core": { "node_modules/@eslint/core": {
"version": "0.12.0", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
"integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -697,9 +697,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.24.0", "version": "9.25.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz",
"integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", "integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -730,19 +730,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1380,17 +1367,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.30.1", "version": "8.31.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.0.tgz",
"integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==", "integrity": "sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.30.1", "@typescript-eslint/scope-manager": "8.31.0",
"@typescript-eslint/type-utils": "8.30.1", "@typescript-eslint/type-utils": "8.31.0",
"@typescript-eslint/utils": "8.30.1", "@typescript-eslint/utils": "8.31.0",
"@typescript-eslint/visitor-keys": "8.30.1", "@typescript-eslint/visitor-keys": "8.31.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -1410,16 +1397,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.30.1", "version": "8.31.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.0.tgz",
"integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", "integrity": "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.30.1", "@typescript-eslint/scope-manager": "8.31.0",
"@typescript-eslint/types": "8.30.1", "@typescript-eslint/types": "8.31.0",
"@typescript-eslint/typescript-estree": "8.30.1", "@typescript-eslint/typescript-estree": "8.31.0",
"@typescript-eslint/visitor-keys": "8.30.1", "@typescript-eslint/visitor-keys": "8.31.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1435,14 +1422,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.30.1", "version": "8.31.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.0.tgz",
"integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", "integrity": "sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.30.1", "@typescript-eslint/types": "8.31.0",
"@typescript-eslint/visitor-keys": "8.30.1" "@typescript-eslint/visitor-keys": "8.31.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1453,14 +1440,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.30.1", "version": "8.31.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.0.tgz",
"integrity": "sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==", "integrity": "sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "8.30.1", "@typescript-eslint/typescript-estree": "8.31.0",
"@typescript-eslint/utils": "8.30.1", "@typescript-eslint/utils": "8.31.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^2.0.1" "ts-api-utils": "^2.0.1"
}, },
@@ -1477,9 +1464,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.30.1", "version": "8.31.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz",
"integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", "integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1491,14 +1478,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.30.1", "version": "8.31.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.0.tgz",
"integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", "integrity": "sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.30.1", "@typescript-eslint/types": "8.31.0",
"@typescript-eslint/visitor-keys": "8.30.1", "@typescript-eslint/visitor-keys": "8.31.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -1544,16 +1531,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.30.1", "version": "8.31.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.0.tgz",
"integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", "integrity": "sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.30.1", "@typescript-eslint/scope-manager": "8.31.0",
"@typescript-eslint/types": "8.30.1", "@typescript-eslint/types": "8.31.0",
"@typescript-eslint/typescript-estree": "8.30.1" "@typescript-eslint/typescript-estree": "8.31.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1568,13 +1555,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.30.1", "version": "8.31.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.0.tgz",
"integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", "integrity": "sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.30.1", "@typescript-eslint/types": "8.31.0",
"eslint-visitor-keys": "^4.2.0" "eslint-visitor-keys": "^4.2.0"
}, },
"engines": { "engines": {
@@ -1586,9 +1573,9 @@
} }
}, },
"node_modules/@vitest/coverage-v8": { "node_modules/@vitest/coverage-v8": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.2.tgz",
"integrity": "sha512-MgV6D2dhpD6Hp/uroUoAIvFqA8AuvXEFBC2eepG3WFc1pxTfdk1LEqqkWoWhjz+rytoqrnUUCdf6Lzco3iHkLQ==", "integrity": "sha512-XDdaDOeaTMAMYW7N63AqoK32sYUWbXnTkC6tEbVcu3RlU1bB9of32T+PGf8KZvxqLNqeXhafDFqCkwpf2+dyaQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1601,7 +1588,7 @@
"istanbul-reports": "^3.1.7", "istanbul-reports": "^3.1.7",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"magicast": "^0.3.5", "magicast": "^0.3.5",
"std-env": "^3.8.1", "std-env": "^3.9.0",
"test-exclude": "^7.0.1", "test-exclude": "^7.0.1",
"tinyrainbow": "^2.0.0" "tinyrainbow": "^2.0.0"
}, },
@@ -1609,8 +1596,8 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
}, },
"peerDependencies": { "peerDependencies": {
"@vitest/browser": "3.1.1", "@vitest/browser": "3.1.2",
"vitest": "3.1.1" "vitest": "3.1.2"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@vitest/browser": { "@vitest/browser": {
@@ -1619,14 +1606,14 @@
} }
}, },
"node_modules/@vitest/expect": { "node_modules/@vitest/expect": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.2.tgz",
"integrity": "sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==", "integrity": "sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/spy": "3.1.1", "@vitest/spy": "3.1.2",
"@vitest/utils": "3.1.1", "@vitest/utils": "3.1.2",
"chai": "^5.2.0", "chai": "^5.2.0",
"tinyrainbow": "^2.0.0" "tinyrainbow": "^2.0.0"
}, },
@@ -1635,13 +1622,13 @@
} }
}, },
"node_modules/@vitest/mocker": { "node_modules/@vitest/mocker": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.2.tgz",
"integrity": "sha512-bmpJJm7Y7i9BBELlLuuM1J1Q6EQ6K5Ye4wcyOpOMXMcePYKSIYlpcrCm4l/O6ja4VJA5G2aMJiuZkZdnxlC3SA==", "integrity": "sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/spy": "3.1.1", "@vitest/spy": "3.1.2",
"estree-walker": "^3.0.3", "estree-walker": "^3.0.3",
"magic-string": "^0.30.17" "magic-string": "^0.30.17"
}, },
@@ -1662,9 +1649,9 @@
} }
}, },
"node_modules/@vitest/pretty-format": { "node_modules/@vitest/pretty-format": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz",
"integrity": "sha512-dg0CIzNx+hMMYfNmSqJlLSXEmnNhMswcn3sXO7Tpldr0LiGmg3eXdLLhwkv2ZqgHb/d5xg5F7ezNFRA1fA13yA==", "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1675,13 +1662,13 @@
} }
}, },
"node_modules/@vitest/runner": { "node_modules/@vitest/runner": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.2.tgz",
"integrity": "sha512-X/d46qzJuEDO8ueyjtKfxffiXraPRfmYasoC4i5+mlLEJ10UvPb0XH5M9C3gWuxd7BAQhpK42cJgJtq53YnWVA==", "integrity": "sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/utils": "3.1.1", "@vitest/utils": "3.1.2",
"pathe": "^2.0.3" "pathe": "^2.0.3"
}, },
"funding": { "funding": {
@@ -1689,13 +1676,13 @@
} }
}, },
"node_modules/@vitest/snapshot": { "node_modules/@vitest/snapshot": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.2.tgz",
"integrity": "sha512-bByMwaVWe/+1WDf9exFxWWgAixelSdiwo2p33tpqIlM14vW7PRV5ppayVXtfycqze4Qhtwag5sVhX400MLBOOw==", "integrity": "sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/pretty-format": "3.1.1", "@vitest/pretty-format": "3.1.2",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"pathe": "^2.0.3" "pathe": "^2.0.3"
}, },
@@ -1704,9 +1691,9 @@
} }
}, },
"node_modules/@vitest/spy": { "node_modules/@vitest/spy": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.2.tgz",
"integrity": "sha512-+EmrUOOXbKzLkTDwlsc/xrwOlPDXyVk3Z6P6K4oiCndxz7YLpp/0R0UsWVOKT0IXWjjBJuSMk6D27qipaupcvQ==", "integrity": "sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1717,13 +1704,13 @@
} }
}, },
"node_modules/@vitest/utils": { "node_modules/@vitest/utils": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.2.tgz",
"integrity": "sha512-1XIjflyaU2k3HMArJ50bwSh3wKWPD6Q47wz/NUSmRV0zNywPc4w79ARjg/i/aNINHwA+mIALhUVqD9/aUvZNgg==", "integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/pretty-format": "3.1.1", "@vitest/pretty-format": "3.1.2",
"loupe": "^3.1.3", "loupe": "^3.1.3",
"tinyrainbow": "^2.0.0" "tinyrainbow": "^2.0.0"
}, },
@@ -2183,9 +2170,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "1.6.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
"integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -2254,20 +2241,20 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.24.0", "version": "9.25.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz",
"integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.20.0", "@eslint/config-array": "^0.20.0",
"@eslint/config-helpers": "^0.2.0", "@eslint/config-helpers": "^0.2.1",
"@eslint/core": "^0.12.0", "@eslint/core": "^0.13.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.24.0", "@eslint/js": "9.25.1",
"@eslint/plugin-kit": "^0.2.7", "@eslint/plugin-kit": "^0.2.8",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2", "@humanwhocodes/retry": "^0.4.2",
@@ -4197,15 +4184,15 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.30.1", "version": "8.31.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.30.1.tgz", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.0.tgz",
"integrity": "sha512-D7lC0kcehVH7Mb26MRQi64LMyRJsj3dToJxM1+JVTl53DQSV5/7oUGWQLcKl1C1KnoVHxMMU2FNQMffr7F3Row==", "integrity": "sha512-u+93F0sB0An8WEAPtwxVhFby573E8ckdjwUUQUj9QA4v8JAvgtoDdIyYR3XFwFHq2W1KJ1AurwJCO+w+Y1ixyQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.30.1", "@typescript-eslint/eslint-plugin": "8.31.0",
"@typescript-eslint/parser": "8.30.1", "@typescript-eslint/parser": "8.31.0",
"@typescript-eslint/utils": "8.30.1" "@typescript-eslint/utils": "8.31.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4292,18 +4279,18 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.3.2", "version": "6.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.3.tgz",
"integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", "integrity": "sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.3", "fdir": "^6.4.4",
"picomatch": "^4.0.2", "picomatch": "^4.0.2",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"rollup": "^4.34.9", "rollup": "^4.34.9",
"tinyglobby": "^0.2.12" "tinyglobby": "^0.2.13"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@@ -4367,9 +4354,9 @@
} }
}, },
"node_modules/vite-node": { "node_modules/vite-node": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.1.tgz", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.2.tgz",
"integrity": "sha512-V+IxPAE2FvXpTCHXyNem0M+gWm6J7eRyWPR6vYoG/Gl+IscNOjXzztUhimQgTxaAoUoj40Qqimaa0NLIOOAH4w==", "integrity": "sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4438,31 +4425,32 @@
} }
}, },
"node_modules/vitest": { "node_modules/vitest": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.1.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.2.tgz",
"integrity": "sha512-kiZc/IYmKICeBAZr9DQ5rT7/6bD9G7uqQEki4fxazi1jdVl2mWGzedtBs5s6llz59yQhVb7FFY2MbHzHCnT79Q==", "integrity": "sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vitest/expect": "3.1.1", "@vitest/expect": "3.1.2",
"@vitest/mocker": "3.1.1", "@vitest/mocker": "3.1.2",
"@vitest/pretty-format": "^3.1.1", "@vitest/pretty-format": "^3.1.2",
"@vitest/runner": "3.1.1", "@vitest/runner": "3.1.2",
"@vitest/snapshot": "3.1.1", "@vitest/snapshot": "3.1.2",
"@vitest/spy": "3.1.1", "@vitest/spy": "3.1.2",
"@vitest/utils": "3.1.1", "@vitest/utils": "3.1.2",
"chai": "^5.2.0", "chai": "^5.2.0",
"debug": "^4.4.0", "debug": "^4.4.0",
"expect-type": "^1.2.0", "expect-type": "^1.2.1",
"magic-string": "^0.30.17", "magic-string": "^0.30.17",
"pathe": "^2.0.3", "pathe": "^2.0.3",
"std-env": "^3.8.1", "std-env": "^3.9.0",
"tinybench": "^2.9.0", "tinybench": "^2.9.0",
"tinyexec": "^0.3.2", "tinyexec": "^0.3.2",
"tinyglobby": "^0.2.13",
"tinypool": "^1.0.2", "tinypool": "^1.0.2",
"tinyrainbow": "^2.0.0", "tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0", "vite": "^5.0.0 || ^6.0.0",
"vite-node": "3.1.1", "vite-node": "3.1.2",
"why-is-node-running": "^2.3.0" "why-is-node-running": "^2.3.0"
}, },
"bin": { "bin": {
@@ -4478,8 +4466,8 @@
"@edge-runtime/vm": "*", "@edge-runtime/vm": "*",
"@types/debug": "^4.1.12", "@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.1.1", "@vitest/browser": "3.1.2",
"@vitest/ui": "3.1.1", "@vitest/ui": "3.1.2",
"happy-dom": "*", "happy-dom": "*",
"jsdom": "*" "jsdom": "*"
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.62", "version": "2.2.65",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"type": "module", "type": "module",
"exports": "./dist/index.js", "exports": "./dist/index.js",
+1 -1
View File
@@ -116,7 +116,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:42cba146593a5ea9a622002c1b7cba5da7be248650cbb64ecb9c6c33d29794b1 image: docker.io/valkey/valkey:8-bookworm@sha256:c855f98e09d558a0d7cc1a4e56473231206a4c54c0114ada9c485b47aeb92ec8
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
+2 -2
View File
@@ -56,7 +56,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:42cba146593a5ea9a622002c1b7cba5da7be248650cbb64ecb9c6c33d29794b1 image: docker.io/valkey/valkey:8-bookworm@sha256:c855f98e09d558a0d7cc1a4e56473231206a4c54c0114ada9c485b47aeb92ec8
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
@@ -102,7 +102,7 @@ services:
command: [ './run.sh', '-disable-reporting' ] command: [ './run.sh', '-disable-reporting' ]
ports: ports:
- 3000:3000 - 3000:3000
image: grafana/grafana:11.6.0-ubuntu@sha256:fd8fa48213c624e1a95122f1d93abbf1cf1cbe85fc73212c1e599dbd76c63ff8 image: grafana/grafana:11.6.1-ubuntu@sha256:6fc273288470ef499dd3c6b36aeade093170d4f608f864c5dd3a7fabeae77b50
volumes: volumes:
- grafana-data:/var/lib/grafana - grafana-data:/var/lib/grafana
+1 -1
View File
@@ -49,7 +49,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:42cba146593a5ea9a622002c1b7cba5da7be248650cbb64ecb9c6c33d29794b1 image: docker.io/valkey/valkey:8-bookworm@sha256:c855f98e09d558a0d7cc1a4e56473231206a4c54c0114ada9c485b47aeb92ec8
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
+5 -5
View File
@@ -14,14 +14,14 @@ online generators you can use.
2. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.) 2. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.)
3. Save your selections. Reload the map, and enjoy your custom map style! 3. Save your selections. Reload the map, and enjoy your custom map style!
## Use Maptiler to build a custom style ## Use MapTiler to build a custom style
Customizing the map style can be done easily using Maptiler, if you do not want to write an entire JSON document by hand. Customizing the map style can be done easily using MapTiler, if you do not want to write an entire JSON document by hand.
1. Create a free account at https://cloud.maptiler.com 1. Create a free account at https://cloud.maptiler.com
2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there. 2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there.
3. The **editor** interface is self-explanatory. You can change colors, remove visible layers, or add optional layers (e.g., administrative, topo, hydro, etc.) in the composer. 3. The **editor** interface is self-explanatory. You can change colors, remove visible layers, or add optional layers (e.g., administrative, topo, hydro, etc.) in the composer.
4. Once you have your map composed, click on **Save** at the top right. Give it a unique name to save it to your account. 4. Once you have your map composed, click on **Save** at the top right. Give it a unique name to save it to your account.
5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. Maptiler will present an interactive side-by-side map with the original and your changes prior to publication.<br/>![Maptiler Publication Settings](img/immich_map_styles_publish.webp) 5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. MapTiler will present an interactive side-by-side map with the original and your changes prior to publication.<br/>![MapTiler Publication Settings](img/immich_map_styles_publish.webp)
6. Maptiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay. 6. MapTiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay.
7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to Maptiler. 7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to MapTiler.
+1 -1
View File
@@ -1,7 +1,7 @@
# Database Queries # Database Queries
:::danger :::danger
Keep in mind that mucking around in the database might set the moon on fire. Avoid modifying the database directly when possible, and always have current backups. Keep in mind that mucking around in the database might set the Moon on fire. Avoid modifying the database directly when possible, and always have current backups.
::: :::
:::tip :::tip
+7 -7
View File
@@ -252,6 +252,13 @@ const milestones: Item[] = [
description: 'Browse your photos and videos in their folder structure inside the mobile app', description: 'Browse your photos and videos in their folder structure inside the mobile app',
release: 'v1.130.0', release: 'v1.130.0',
}), }),
{
icon: mdiStar,
iconColor: 'gold',
title: '60,000 Stars',
description: 'Reached 60K Stars on GitHub!',
getDateLabel: withLanguage(new Date(2025, 2, 4)),
},
withRelease({ withRelease({
icon: mdiTagFaces, icon: mdiTagFaces,
iconColor: 'teal', iconColor: 'teal',
@@ -260,13 +267,6 @@ const milestones: Item[] = [
'Manually tag or remove faces in photos and videos, even when automatic detection misses or misidentifies them.', 'Manually tag or remove faces in photos and videos, even when automatic detection misses or misidentifies them.',
release: 'v1.127.0', release: 'v1.127.0',
}), }),
{
icon: mdiStar,
iconColor: 'gold',
title: '60,000 Stars',
description: 'Reached 60K Stars on GitHub!',
getDateLabel: withLanguage(new Date(2025, 2, 4)),
},
withRelease({ withRelease({
icon: mdiLinkEdit, icon: mdiLinkEdit,
iconColor: 'crimson', iconColor: 'crimson',
+5
View File
@@ -0,0 +1,5 @@
Policy: https://github.com/immich-app/immich/blob/main/SECURITY.md
Contact: mailto:security@immich.app
Preferred-Languages: en
Expires: 2026-05-01T23:59:00.000Z
Canonical: https://immich.app/.well-known/security.txt
+12
View File
@@ -1,4 +1,16 @@
[ [
{
"label": "v1.132.3",
"url": "https://v1.132.3.archive.immich.app"
},
{
"label": "v1.132.2",
"url": "https://v1.132.2.archive.immich.app"
},
{
"label": "v1.132.1",
"url": "https://v1.132.1.archive.immich.app"
},
{ {
"label": "v1.132.0", "label": "v1.132.0",
"url": "https://v1.132.0.archive.immich.app" "url": "https://v1.132.0.archive.immich.app"
+1 -1
View File
@@ -34,7 +34,7 @@ services:
- 2285:2285 - 2285:2285
redis: redis:
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8 image: redis:6.2-alpine@sha256:3211c33a618c457e5d241922c975dbc4f446d0bdb2dc75694f5573ef8e2d01fa
database: database:
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
+408 -344
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.132.0", "version": "1.132.3",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
+1 -1
View File
@@ -142,7 +142,7 @@ describe(`/oauth`, () => {
it(`should throw an error if the state mismatches`, async () => { it(`should throw an error if the state mismatches`, async () => {
const callbackParams = await loginWithOAuth('oauth-auto-register'); const callbackParams = await loginWithOAuth('oauth-auto-register');
const { state } = await loginWithOAuth('oauth-auto-register'); const { state } = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app) const { status } = await request(app)
.post('/oauth/callback') .post('/oauth/callback')
.send({ ...callbackParams, state }); .send({ ...callbackParams, state });
expect(status).toBeGreaterThanOrEqual(400); expect(status).toBeGreaterThanOrEqual(400);
+13 -13
View File
@@ -215,6 +215,19 @@ describe('/admin/users', () => {
const user = await getMyUser({ headers: asBearerAuth(token.accessToken) }); const user = await getMyUser({ headers: asBearerAuth(token.accessToken) });
expect(user).toMatchObject({ email: nonAdmin.userEmail }); expect(user).toMatchObject({ email: nonAdmin.userEmail });
}); });
it('should update the avatar color', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}`)
.send({ avatarColor: 'orange' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatarColor: 'orange' });
const after = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatarColor: 'orange' });
});
}); });
describe('PUT /admin/users/:id/preferences', () => { describe('PUT /admin/users/:id/preferences', () => {
@@ -240,19 +253,6 @@ describe('/admin/users', () => {
expect(after).toMatchObject({ memories: { enabled: false } }); expect(after).toMatchObject({ memories: { enabled: false } });
}); });
it('should update the avatar color', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}/preferences`)
.send({ avatar: { color: 'orange' } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatar: { color: 'orange' } });
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatar: { color: 'orange' } });
});
it('should update download archive size', async () => { it('should update download archive size', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}/preferences`) .put(`/admin/users/${admin.userId}/preferences`)
+13 -13
View File
@@ -139,6 +139,19 @@ describe('/users', () => {
profileChangedAt: expect.anything(), profileChangedAt: expect.anything(),
}); });
}); });
it('should update avatar color', async () => {
const { status, body } = await request(app)
.put(`/users/me`)
.send({ avatarColor: 'blue' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatarColor: 'blue' });
const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatarColor: 'blue' });
});
}); });
describe('PUT /users/me/preferences', () => { describe('PUT /users/me/preferences', () => {
@@ -158,19 +171,6 @@ describe('/users', () => {
expect(after).toMatchObject({ memories: { enabled: false } }); expect(after).toMatchObject({ memories: { enabled: false } });
}); });
it('should update avatar color', async () => {
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ avatar: { color: 'blue' } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatar: { color: 'blue' } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatar: { color: 'blue' } });
});
it('should require an integer for download archive size', async () => { it('should require an integer for download archive size', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/users/me/preferences`) .put(`/users/me/preferences`)
+3 -3
View File
@@ -25,7 +25,7 @@ test.describe('Registration', () => {
// login // login
await expect(page).toHaveTitle(/Login/); await expect(page).toHaveTitle(/Login/);
await page.goto('/auth/login'); await page.goto('/auth/login?autoLaunch=0');
await page.getByLabel('Email').fill('admin@immich.app'); await page.getByLabel('Email').fill('admin@immich.app');
await page.getByLabel('Password').fill('password'); await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: 'Login' }).click();
@@ -59,7 +59,7 @@ test.describe('Registration', () => {
await context.clearCookies(); await context.clearCookies();
// login // login
await page.goto('/auth/login'); await page.goto('/auth/login?autoLaunch=0');
await page.getByLabel('Email').fill('user@immich.cloud'); await page.getByLabel('Email').fill('user@immich.cloud');
await page.getByLabel('Password').fill('password'); await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: 'Login' }).click();
@@ -72,7 +72,7 @@ test.describe('Registration', () => {
await page.getByRole('button', { name: 'Change password' }).click(); await page.getByRole('button', { name: 'Change password' }).click();
// login with new password // login with new password
await expect(page).toHaveURL('/auth/login'); await expect(page).toHaveURL('/auth/login?autoLaunch=0');
await page.getByLabel('Email').fill('user@immich.cloud'); await page.getByLabel('Email').fill('user@immich.cloud');
await page.getByLabel('Password').fill('new-password'); await page.getByLabel('Password').fill('new-password');
await page.getByRole('button', { name: 'Login' }).click(); await page.getByRole('button', { name: 'Login' }).click();
@@ -21,23 +21,9 @@ test.describe('Photo Viewer', () => {
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
// before each test, login as user // before each test, login as user
await utils.setAuthCookies(context, admin.accessToken); await utils.setAuthCookies(context, admin.accessToken);
await page.goto('/photos');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
}); });
test('initially shows a loading spinner', async ({ page }) => {
await page.route(`/api/assets/${asset.id}/thumbnail**`, async (route) => {
// slow down the request for thumbnail, so spinner has chance to show up
await new Promise((f) => setTimeout(f, 2000));
await route.continue();
});
await page.goto(`/photos/${asset.id}`);
await page.waitForLoadState('load');
// this is the spinner
await page.waitForSelector('svg[role=status]');
await expect(page.getByTestId('loading-spinner')).toBeVisible();
});
test('loads original photo when zoomed', async ({ page }) => { test('loads original photo when zoomed', async ({ page }) => {
await page.goto(`/photos/${asset.id}`); await page.goto(`/photos/${asset.id}`);
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail'); await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail');
@@ -55,7 +55,6 @@ test.describe('Shared Links', () => {
await page.goto(`/share/${sharedLink.key}`); await page.goto(`/share/${sharedLink.key}`);
await page.getByRole('heading', { name: 'Test Album' }).waitFor(); await page.getByRole('heading', { name: 'Test Album' }).waitFor();
await page.getByRole('button', { name: 'Download' }).click(); await page.getByRole('button', { name: 'Download' }).click();
await page.getByText('DOWNLOADING', { exact: true }).waitFor();
await page.waitForEvent('download'); await page.waitForEvent('download');
}); });
+11 -7
View File
@@ -192,26 +192,22 @@
"oauth_auto_register": "Auto register", "oauth_auto_register": "Auto register",
"oauth_auto_register_description": "Automatically register new users after signing in with OAuth", "oauth_auto_register_description": "Automatically register new users after signing in with OAuth",
"oauth_button_text": "Button text", "oauth_button_text": "Button text",
"oauth_client_id": "Client ID", "oauth_client_secret_description": "Required if PKCE (Proof Key for Code Exchange) is not supported by the OAuth provider",
"oauth_client_secret": "Client Secret",
"oauth_enable_description": "Login with OAuth", "oauth_enable_description": "Login with OAuth",
"oauth_issuer_url": "Issuer URL",
"oauth_mobile_redirect_uri": "Mobile redirect URI", "oauth_mobile_redirect_uri": "Mobile redirect URI",
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override", "oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like '{callback}'", "oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like '{callback}'",
"oauth_profile_signing_algorithm": "Profile signing algorithm",
"oauth_profile_signing_algorithm_description": "Algorithm used to sign the user profile.",
"oauth_scope": "Scope",
"oauth_settings": "OAuth", "oauth_settings": "OAuth",
"oauth_settings_description": "Manage OAuth login settings", "oauth_settings_description": "Manage OAuth login settings",
"oauth_settings_more_details": "For more details about this feature, refer to the <link>docs</link>.", "oauth_settings_more_details": "For more details about this feature, refer to the <link>docs</link>.",
"oauth_signing_algorithm": "Signing algorithm",
"oauth_storage_label_claim": "Storage label claim", "oauth_storage_label_claim": "Storage label claim",
"oauth_storage_label_claim_description": "Automatically set the user's storage label to the value of this claim.", "oauth_storage_label_claim_description": "Automatically set the user's storage label to the value of this claim.",
"oauth_storage_quota_claim": "Storage quota claim", "oauth_storage_quota_claim": "Storage quota claim",
"oauth_storage_quota_claim_description": "Automatically set the user's storage quota to the value of this claim.", "oauth_storage_quota_claim_description": "Automatically set the user's storage quota to the value of this claim.",
"oauth_storage_quota_default": "Default storage quota (GiB)", "oauth_storage_quota_default": "Default storage quota (GiB)",
"oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota).", "oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota).",
"oauth_timeout": "Request Timeout",
"oauth_timeout_description": "Timeout for requests in milliseconds",
"offline_paths": "Offline Paths", "offline_paths": "Offline Paths",
"offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.", "offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.",
"password_enable_description": "Login with email and password", "password_enable_description": "Login with email and password",
@@ -853,10 +849,12 @@
"failed_to_keep_this_delete_others": "Failed to keep this asset and delete the other assets", "failed_to_keep_this_delete_others": "Failed to keep this asset and delete the other assets",
"failed_to_load_asset": "Failed to load asset", "failed_to_load_asset": "Failed to load asset",
"failed_to_load_assets": "Failed to load assets", "failed_to_load_assets": "Failed to load assets",
"failed_to_load_notifications": "Failed to load notifications",
"failed_to_load_people": "Failed to load people", "failed_to_load_people": "Failed to load people",
"failed_to_remove_product_key": "Failed to remove product key", "failed_to_remove_product_key": "Failed to remove product key",
"failed_to_stack_assets": "Failed to stack assets", "failed_to_stack_assets": "Failed to stack assets",
"failed_to_unstack_assets": "Failed to un-stack assets", "failed_to_unstack_assets": "Failed to un-stack assets",
"failed_to_update_notification_status": "Failed to update notification status",
"import_path_already_exists": "This import path already exists.", "import_path_already_exists": "This import path already exists.",
"incorrect_email_or_password": "Incorrect email or password", "incorrect_email_or_password": "Incorrect email or password",
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation", "paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation",
@@ -1199,6 +1197,9 @@
"map_settings_only_show_favorites": "Show Favorite Only", "map_settings_only_show_favorites": "Show Favorite Only",
"map_settings_theme_settings": "Map Theme", "map_settings_theme_settings": "Map Theme",
"map_zoom_to_see_photos": "Zoom out to see photos", "map_zoom_to_see_photos": "Zoom out to see photos",
"mark_as_read": "Mark as read",
"mark_all_as_read": "Mark all as read",
"marked_all_as_read": "Marked all as read",
"matches": "Matches", "matches": "Matches",
"media_type": "Media type", "media_type": "Media type",
"memories": "Memories", "memories": "Memories",
@@ -1225,6 +1226,8 @@
"month": "Month", "month": "Month",
"monthly_title_text_date_format": "MMMM y", "monthly_title_text_date_format": "MMMM y",
"more": "More", "more": "More",
"moved_to_archive": "Moved {count, plural, one {# asset} other {# assets}} to archive",
"moved_to_library": "Moved {count, plural, one {# asset} other {# assets}} to library",
"moved_to_trash": "Moved to trash", "moved_to_trash": "Moved to trash",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
@@ -1260,6 +1263,7 @@
"no_places": "No places", "no_places": "No places",
"no_results": "No results", "no_results": "No results",
"no_results_description": "Try a synonym or more general keyword", "no_results_description": "Try a synonym or more general keyword",
"no_notifications": "No notifications",
"no_shared_albums_message": "Create an album to share photos and videos with people in your network", "no_shared_albums_message": "Create an album to share photos and videos with people in your network",
"not_in_any_album": "Not in any album", "not_in_any_album": "Not in any album",
"not_selected": "Not selected", "not_selected": "Not selected",
+4 -4
View File
@@ -1,6 +1,6 @@
ARG DEVICE=cpu ARG DEVICE=cpu
FROM python:3.11-bookworm@sha256:a3e280261e448b95d49423532ccd6e5329c39d171c10df1457891ff7c5e2301b AS builder-cpu FROM python:3.11-bookworm@sha256:ab60e444e04215a62671149f24c59cc2893b49cb5dad26f9d139077a86be760e AS builder-cpu
FROM builder-cpu AS builder-openvino FROM builder-cpu AS builder-openvino
@@ -54,7 +54,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
RUN apt-get update && apt-get install -y --no-install-recommends g++ RUN apt-get update && apt-get install -y --no-install-recommends g++
COPY --from=ghcr.io/astral-sh/uv:latest@sha256:db305ce8edc1c2df4988b9d23471465d90d599cc55571e6501421c173a33bb0b /uv /uvx /bin/ COPY --from=ghcr.io/astral-sh/uv:latest@sha256:4a6c9444b126bd325fba904bff796bf91fb777bf6148d60109c4cb1de2ffc497 /uv /uvx /bin/
RUN --mount=type=cache,target=/root/.cache/uv \ RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
@@ -63,11 +63,11 @@ RUN if [ "$DEVICE" = "rocm" ]; then \
uv pip install /opt/onnxruntime_rocm-*.whl; \ uv pip install /opt/onnxruntime_rocm-*.whl; \
fi fi
FROM python:3.11-slim-bookworm@sha256:82c07f2f6e35255b92eb16f38dbd22679d5e8fb523064138d7c6468e7bf0c15b AS prod-cpu FROM python:3.11-slim-bookworm@sha256:97ef3198ec8c78690587167bb6a4905d00ffe053900687bdae93ad667e507cbb AS prod-cpu
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2
FROM python:3.11-slim-bookworm@sha256:82c07f2f6e35255b92eb16f38dbd22679d5e8fb523064138d7c6468e7bf0c15b AS prod-openvino FROM python:3.11-slim-bookworm@sha256:97ef3198ec8c78690587167bb6a4905d00ffe053900687bdae93ad667e507cbb AS prod-openvino
RUN apt-get update && \ RUN apt-get update && \
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
+116 -26
View File
@@ -69,6 +69,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/cd/d6d9bb1dadf73e7af02d18225cbd2c93f8552e13130484f1c8dcfece292b/anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee", size = 85481, upload_time = "2023-12-16T17:06:55.989Z" }, { url = "https://files.pythonhosted.org/packages/bf/cd/d6d9bb1dadf73e7af02d18225cbd2c93f8552e13130484f1c8dcfece292b/anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee", size = 85481, upload_time = "2023-12-16T17:06:55.989Z" },
] ]
[[package]]
name = "bidict"
version = "0.23.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload_time = "2024-02-18T19:09:05.748Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload_time = "2024-02-18T19:09:04.156Z" },
]
[[package]] [[package]]
name = "black" name = "black"
version = "25.1.0" version = "25.1.0"
@@ -1200,15 +1209,16 @@ wheels = [
[[package]] [[package]]
name = "locust" name = "locust"
version = "2.35.0" version = "2.36.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "configargparse" }, { name = "configargparse" },
{ name = "flask" }, { name = "flask" },
{ name = "flask-cors" }, { name = "flask-cors" },
{ name = "flask-login" }, { name = "flask-login" },
{ name = "gevent", marker = "python_full_version != '3.13.*'" }, { name = "gevent" },
{ name = "geventhttpclient" }, { name = "geventhttpclient" },
{ name = "locust-cloud" },
{ name = "msgpack" }, { name = "msgpack" },
{ name = "psutil" }, { name = "psutil" },
{ name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "pywin32", marker = "sys_platform == 'win32'" },
@@ -1219,9 +1229,25 @@ dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" },
{ name = "werkzeug" }, { name = "werkzeug" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/79/21/d5aeeee74173d73d7d8d392e307ec24d8281fca69a2bf1f19199bd84c498/locust-2.35.0.tar.gz", hash = "sha256:97f83e591646ca3227644cfb6d4fa590e9a3e3d791ab18b216ca98be235b9b24", size = 2240690, upload_time = "2025-04-16T12:10:25.037Z" } sdist = { url = "https://files.pythonhosted.org/packages/6d/90/55d4fbc8911e5e6ec4072caaca9e8b7b2b11279435c0d1330c9966b0c898/locust-2.36.2.tar.gz", hash = "sha256:604aff7535f5a83b7f666d32373b2dc74ad260c7c3d1dc274f4c82844be72eb6", size = 2251110, upload_time = "2025-04-25T14:03:35.919Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/15/9e92757c08af3f0c0168ab6480315ed6374396d635f70055df5c42cfa672/locust-2.35.0-py3-none-any.whl", hash = "sha256:fb9e0ec25c5db3ed6a3c6d48e7236d7c2c370b0ddae102e9badcb2d3d101abde", size = 2258054, upload_time = "2025-04-16T12:10:22.608Z" }, { url = "https://files.pythonhosted.org/packages/ab/f5/99dab104be69122eee3513dcdc6e0b32d59ca1f4cfd8715470c5f3aa7643/locust-2.36.2-py3-none-any.whl", hash = "sha256:74239f493f44035b25a87a0665deadf41d213b3dcd45774398e511dec15e26eb", size = 2267937, upload_time = "2025-04-25T14:03:33.671Z" },
]
[[package]]
name = "locust-cloud"
version = "1.20.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "configargparse" },
{ name = "gevent" },
{ name = "platformdirs" },
{ name = "python-socketio", extra = ["client"] },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/94/47/1ec2478f3d4e526fb8d667b01a75b22093b2e66aea665b5369dd656ceec9/locust_cloud-1.20.7.tar.gz", hash = "sha256:24c16b767adffab51b97f489bcf142e16e2439354fb4296ecbb3e87ad20e220a", size = 448622, upload_time = "2025-04-28T11:01:49.381Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/07/62b5b174c77d4281235405f1ffd439f12a877e434e007a24a5299c461e39/locust_cloud-1.20.7-py3-none-any.whl", hash = "sha256:f38214e77993d0ee87114dafa857e1689789ed4bfe4ae57c2b9dc754674f08bc", size = 406619, upload_time = "2025-04-28T11:01:43.135Z" },
] ]
[[package]] [[package]]
@@ -1722,11 +1748,11 @@ wheels = [
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
version = "4.1.0" version = "4.3.7"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/62/d1/7feaaacb1a3faeba96c06e6c5091f90695cc0f94b7e8e1a3a3fe2b33ff9a/platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420", size = 19760, upload_time = "2023-12-04T15:32:15.925Z" } sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload_time = "2025-03-19T20:36:10.989Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/be/53/42fe5eab4a09d251a76d0043e018172db324a23fcdac70f77a551c11f618/platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", size = 17420, upload_time = "2023-12-04T15:32:13.795Z" }, { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload_time = "2025-03-19T20:36:09.038Z" },
] ]
[[package]] [[package]]
@@ -2005,6 +2031,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/44/2f/62ea1c8b593f4e093cc1a7768f0d46112107e790c3e478532329e434f00b/python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a", size = 19482, upload_time = "2023-02-24T06:46:36.009Z" }, { url = "https://files.pythonhosted.org/packages/44/2f/62ea1c8b593f4e093cc1a7768f0d46112107e790c3e478532329e434f00b/python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a", size = 19482, upload_time = "2023-02-24T06:46:36.009Z" },
] ]
[[package]]
name = "python-engineio"
version = "4.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "simple-websocket" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f7/e1/eee1129544b7f78fa2afa9fa0fce153cdcb21015b9b331d1b8adf90f45cb/python_engineio-4.12.0.tar.gz", hash = "sha256:f42a36a868d7063aa10ddccf6bd6117a169b6bd00d7ca53999772093b62014f9", size = 91503, upload_time = "2025-04-12T15:30:23.905Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/f7/0aeea75424c47633c1d98557a2323be23bed31fa950f00161b34a5150d06/python_engineio-4.12.0-py3-none-any.whl", hash = "sha256:a0c47c129c39777e8ebc6d18011efd50db2144e4e8f08983acae8a3614626535", size = 59319, upload_time = "2025-04-12T15:30:22.325Z" },
]
[[package]] [[package]]
name = "python-multipart" name = "python-multipart"
version = "0.0.20" version = "0.0.20"
@@ -2014,6 +2052,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload_time = "2024-12-16T19:45:44.423Z" }, { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload_time = "2024-12-16T19:45:44.423Z" },
] ]
[[package]]
name = "python-socketio"
version = "5.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bidict" },
{ name = "python-engineio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/21/1a/396d50ccf06ee539fa758ce5623b59a9cb27637fc4b2dc07ed08bf495e77/python_socketio-5.13.0.tar.gz", hash = "sha256:ac4e19a0302ae812e23b712ec8b6427ca0521f7c582d6abb096e36e24a263029", size = 121125, upload_time = "2025-04-12T15:46:59.933Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/32/b4fb8585d1be0f68bde7e110dffbcf354915f77ad8c778563f0ad9655c02/python_socketio-5.13.0-py3-none-any.whl", hash = "sha256:51f68d6499f2df8524668c24bcec13ba1414117cfb3a90115c559b601ab10caf", size = 77800, upload_time = "2025-04-12T15:46:58.412Z" },
]
[package.optional-dependencies]
client = [
{ name = "requests" },
{ name = "websocket-client" },
]
[[package]] [[package]]
name = "pywin32" name = "pywin32"
version = "306" version = "306"
@@ -2226,27 +2283,27 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.11.6" version = "0.11.7"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d9/11/bcef6784c7e5d200b8a1f5c2ddf53e5da0efec37e6e5a44d163fb97e04ba/ruff-0.11.6.tar.gz", hash = "sha256:bec8bcc3ac228a45ccc811e45f7eb61b950dbf4cf31a67fa89352574b01c7d79", size = 4010053, upload_time = "2025-04-17T13:35:53.905Z" } sdist = { url = "https://files.pythonhosted.org/packages/5b/89/6f9c9674818ac2e9cc2f2b35b704b7768656e6b7c139064fc7ba8fbc99f1/ruff-0.11.7.tar.gz", hash = "sha256:655089ad3224070736dc32844fde783454f8558e71f501cb207485fe4eee23d4", size = 4054861, upload_time = "2025-04-24T18:49:37.007Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/1f/8848b625100ebcc8740c8bac5b5dd8ba97dd4ee210970e98832092c1635b/ruff-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:d84dcbe74cf9356d1bdb4a78cf74fd47c740bf7bdeb7529068f69b08272239a1", size = 10248105, upload_time = "2025-04-17T13:35:14.758Z" }, { url = "https://files.pythonhosted.org/packages/b4/ec/21927cb906c5614b786d1621dba405e3d44f6e473872e6df5d1a6bca0455/ruff-0.11.7-py3-none-linux_armv6l.whl", hash = "sha256:d29e909d9a8d02f928d72ab7837b5cbc450a5bdf578ab9ebee3263d0a525091c", size = 10245403, upload_time = "2025-04-24T18:48:40.459Z" },
{ url = "https://files.pythonhosted.org/packages/e0/47/c44036e70c6cc11e6ee24399c2a1e1f1e99be5152bd7dff0190e4b325b76/ruff-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9bc583628e1096148011a5d51ff3c836f51899e61112e03e5f2b1573a9b726de", size = 11001494, upload_time = "2025-04-17T13:35:18.444Z" }, { url = "https://files.pythonhosted.org/packages/e2/af/fec85b6c2c725bcb062a354dd7cbc1eed53c33ff3aa665165871c9c16ddf/ruff-0.11.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dd1fb86b168ae349fb01dd497d83537b2c5541fe0626e70c786427dd8363aaee", size = 11007166, upload_time = "2025-04-24T18:48:44.742Z" },
{ url = "https://files.pythonhosted.org/packages/ed/5b/170444061650202d84d316e8f112de02d092bff71fafe060d3542f5bc5df/ruff-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2959049faeb5ba5e3b378709e9d1bf0cab06528b306b9dd6ebd2a312127964a", size = 10352151, upload_time = "2025-04-17T13:35:20.563Z" }, { url = "https://files.pythonhosted.org/packages/31/9a/2d0d260a58e81f388800343a45898fd8df73c608b8261c370058b675319a/ruff-0.11.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d3d7d2e140a6fbbc09033bce65bd7ea29d6a0adeb90b8430262fbacd58c38ada", size = 10378076, upload_time = "2025-04-24T18:48:47.918Z" },
{ url = "https://files.pythonhosted.org/packages/ff/91/f02839fb3787c678e112c8865f2c3e87cfe1744dcc96ff9fc56cfb97dda2/ruff-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c5d4e30d9d0de7fedbfb3e9e20d134b73a30c1e74b596f40f0629d5c28a193", size = 10541951, upload_time = "2025-04-17T13:35:22.522Z" }, { url = "https://files.pythonhosted.org/packages/c2/c4/9b09b45051404d2e7dd6d9dbcbabaa5ab0093f9febcae664876a77b9ad53/ruff-0.11.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4809df77de390a1c2077d9b7945d82f44b95d19ceccf0c287c56e4dc9b91ca64", size = 10557138, upload_time = "2025-04-24T18:48:51.707Z" },
{ url = "https://files.pythonhosted.org/packages/9e/f3/c09933306096ff7a08abede3cc2534d6fcf5529ccd26504c16bf363989b5/ruff-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4b9a4e1439f7d0a091c6763a100cef8fbdc10d68593df6f3cfa5abdd9246e", size = 10079195, upload_time = "2025-04-17T13:35:24.485Z" }, { url = "https://files.pythonhosted.org/packages/5e/5e/f62a1b6669870a591ed7db771c332fabb30f83c967f376b05e7c91bccd14/ruff-0.11.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3a0c2e169e6b545f8e2dba185eabbd9db4f08880032e75aa0e285a6d3f48201", size = 10095726, upload_time = "2025-04-24T18:48:54.243Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/a87f8933fccbc0d8c653cfbf44bedda69c9582ba09210a309c066794e2ee/ruff-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5edf270223dd622218256569636dc3e708c2cb989242262fe378609eccf1308", size = 11698918, upload_time = "2025-04-17T13:35:26.504Z" }, { url = "https://files.pythonhosted.org/packages/45/59/a7aa8e716f4cbe07c3500a391e58c52caf665bb242bf8be42c62adef649c/ruff-0.11.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49b888200a320dd96a68e86736cf531d6afba03e4f6cf098401406a257fcf3d6", size = 11672265, upload_time = "2025-04-24T18:48:57.639Z" },
{ url = "https://files.pythonhosted.org/packages/52/7d/8eac0bd083ea8a0b55b7e4628428203441ca68cd55e0b67c135a4bc6e309/ruff-0.11.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f55844e818206a9dd31ff27f91385afb538067e2dc0beb05f82c293ab84f7d55", size = 12319426, upload_time = "2025-04-17T13:35:28.452Z" }, { url = "https://files.pythonhosted.org/packages/dd/e3/101a8b707481f37aca5f0fcc3e42932fa38b51add87bfbd8e41ab14adb24/ruff-0.11.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2b19cdb9cf7dae00d5ee2e7c013540cdc3b31c4f281f1dacb5a799d610e90db4", size = 12331418, upload_time = "2025-04-24T18:49:00.697Z" },
{ url = "https://files.pythonhosted.org/packages/c2/dc/d0c17d875662d0c86fadcf4ca014ab2001f867621b793d5d7eef01b9dcce/ruff-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d8f782286c5ff562e4e00344f954b9320026d8e3fae2ba9e6948443fafd9ffc", size = 11791012, upload_time = "2025-04-17T13:35:30.455Z" }, { url = "https://files.pythonhosted.org/packages/dd/71/037f76cbe712f5cbc7b852e4916cd3cf32301a30351818d32ab71580d1c0/ruff-0.11.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64e0ee994c9e326b43539d133a36a455dbaab477bc84fe7bfbd528abe2f05c1e", size = 11794506, upload_time = "2025-04-24T18:49:03.545Z" },
{ url = "https://files.pythonhosted.org/packages/f9/f3/81a1aea17f1065449a72509fc7ccc3659cf93148b136ff2a8291c4bc3ef1/ruff-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01c63ba219514271cee955cd0adc26a4083df1956d57847978383b0e50ffd7d2", size = 13949947, upload_time = "2025-04-17T13:35:33.133Z" }, { url = "https://files.pythonhosted.org/packages/ca/de/e450b6bab1fc60ef263ef8fcda077fb4977601184877dce1c59109356084/ruff-0.11.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bad82052311479a5865f52c76ecee5d468a58ba44fb23ee15079f17dd4c8fd63", size = 13939084, upload_time = "2025-04-24T18:49:07.159Z" },
{ url = "https://files.pythonhosted.org/packages/61/9f/a3e34de425a668284e7024ee6fd41f452f6fa9d817f1f3495b46e5e3a407/ruff-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15adac20ef2ca296dd3d8e2bedc6202ea6de81c091a74661c3666e5c4c223ff6", size = 11471753, upload_time = "2025-04-17T13:35:35.416Z" }, { url = "https://files.pythonhosted.org/packages/0e/2c/1e364cc92970075d7d04c69c928430b23e43a433f044474f57e425cbed37/ruff-0.11.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7940665e74e7b65d427b82bffc1e46710ec7f30d58b4b2d5016e3f0321436502", size = 11450441, upload_time = "2025-04-24T18:49:11.41Z" },
{ url = "https://files.pythonhosted.org/packages/df/c5/4a57a86d12542c0f6e2744f262257b2aa5a3783098ec14e40f3e4b3a354a/ruff-0.11.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4dd6b09e98144ad7aec026f5588e493c65057d1b387dd937d7787baa531d9bc2", size = 10417121, upload_time = "2025-04-17T13:35:38.224Z" }, { url = "https://files.pythonhosted.org/packages/9d/7d/1b048eb460517ff9accd78bca0fa6ae61df2b276010538e586f834f5e402/ruff-0.11.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:169027e31c52c0e36c44ae9a9c7db35e505fee0b39f8d9fca7274a6305295a92", size = 10441060, upload_time = "2025-04-24T18:49:14.184Z" },
{ url = "https://files.pythonhosted.org/packages/58/3f/a3b4346dff07ef5b862e2ba06d98fcbf71f66f04cf01d375e871382b5e4b/ruff-0.11.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:45b2e1d6c0eed89c248d024ea95074d0e09988d8e7b1dad8d3ab9a67017a5b03", size = 10073829, upload_time = "2025-04-17T13:35:40.255Z" }, { url = "https://files.pythonhosted.org/packages/3a/57/8dc6ccfd8380e5ca3d13ff7591e8ba46a3b330323515a4996b991b10bd5d/ruff-0.11.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:305b93f9798aee582e91e34437810439acb28b5fc1fee6b8205c78c806845a94", size = 10058689, upload_time = "2025-04-24T18:49:17.559Z" },
{ url = "https://files.pythonhosted.org/packages/93/cc/7ed02e0b86a649216b845b3ac66ed55d8aa86f5898c5f1691797f408fcb9/ruff-0.11.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd40de4115b2ec4850302f1a1d8067f42e70b4990b68838ccb9ccd9f110c5e8b", size = 11076108, upload_time = "2025-04-17T13:35:42.559Z" }, { url = "https://files.pythonhosted.org/packages/23/bf/20487561ed72654147817885559ba2aa705272d8b5dee7654d3ef2dbf912/ruff-0.11.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a681db041ef55550c371f9cd52a3cf17a0da4c75d6bd691092dfc38170ebc4b6", size = 11073703, upload_time = "2025-04-24T18:49:20.247Z" },
{ url = "https://files.pythonhosted.org/packages/39/5e/5b09840fef0eff1a6fa1dea6296c07d09c17cb6fb94ed5593aa591b50460/ruff-0.11.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:77cda2dfbac1ab73aef5e514c4cbfc4ec1fbef4b84a44c736cc26f61b3814cd9", size = 11512366, upload_time = "2025-04-17T13:35:45.702Z" }, { url = "https://files.pythonhosted.org/packages/9d/27/04f2db95f4ef73dccedd0c21daf9991cc3b7f29901a4362057b132075aa4/ruff-0.11.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:07f1496ad00a4a139f4de220b0c97da6d4c85e0e4aa9b2624167b7d4d44fd6b6", size = 11532822, upload_time = "2025-04-24T18:49:23.765Z" },
{ url = "https://files.pythonhosted.org/packages/6f/4c/1cd5a84a412d3626335ae69f5f9de2bb554eea0faf46deb1f0cb48534042/ruff-0.11.6-py3-none-win32.whl", hash = "sha256:5151a871554be3036cd6e51d0ec6eef56334d74dfe1702de717a995ee3d5b287", size = 10485900, upload_time = "2025-04-17T13:35:47.695Z" }, { url = "https://files.pythonhosted.org/packages/e1/72/43b123e4db52144c8add336581de52185097545981ff6e9e58a21861c250/ruff-0.11.7-py3-none-win32.whl", hash = "sha256:f25dfb853ad217e6e5f1924ae8a5b3f6709051a13e9dad18690de6c8ff299e26", size = 10362436, upload_time = "2025-04-24T18:49:27.377Z" },
{ url = "https://files.pythonhosted.org/packages/42/46/8997872bc44d43df986491c18d4418f1caff03bc47b7f381261d62c23442/ruff-0.11.6-py3-none-win_amd64.whl", hash = "sha256:cce85721d09c51f3b782c331b0abd07e9d7d5f775840379c640606d3159cae0e", size = 11558592, upload_time = "2025-04-17T13:35:49.837Z" }, { url = "https://files.pythonhosted.org/packages/c5/a0/3e58cd76fdee53d5c8ce7a56d84540833f924ccdf2c7d657cb009e604d82/ruff-0.11.7-py3-none-win_amd64.whl", hash = "sha256:0a931d85959ceb77e92aea4bbedfded0a31534ce191252721128f77e5ae1f98a", size = 11566676, upload_time = "2025-04-24T18:49:30.938Z" },
{ url = "https://files.pythonhosted.org/packages/d7/6a/65fecd51a9ca19e1477c3879a7fda24f8904174d1275b419422ac00f6eee/ruff-0.11.6-py3-none-win_arm64.whl", hash = "sha256:3567ba0d07fb170b1b48d944715e3294b77f5b7679e8ba258199a250383ccb79", size = 10682766, upload_time = "2025-04-17T13:35:52.014Z" }, { url = "https://files.pythonhosted.org/packages/68/ca/69d7c7752bce162d1516e5592b1cc6b6668e9328c0d270609ddbeeadd7cf/ruff-0.11.7-py3-none-win_arm64.whl", hash = "sha256:778c1e5d6f9e91034142dfd06110534ca13220bfaad5c3735f6cb844654f6177", size = 10677936, upload_time = "2025-04-24T18:49:34.392Z" },
] ]
[[package]] [[package]]
@@ -2349,6 +2406,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/15/88e46eb9387e905704b69849618e699dc2f54407d8953cc4ec4b8b46528d/setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc", size = 931070, upload_time = "2024-07-09T16:07:58.829Z" }, { url = "https://files.pythonhosted.org/packages/ef/15/88e46eb9387e905704b69849618e699dc2f54407d8953cc4ec4b8b46528d/setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc", size = 931070, upload_time = "2024-07-09T16:07:58.829Z" },
] ]
[[package]]
name = "simple-websocket"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wsproto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload_time = "2024-10-10T22:39:31.412Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload_time = "2024-10-10T22:39:29.645Z" },
]
[[package]] [[package]]
name = "six" name = "six"
version = "1.16.0" version = "1.16.0"
@@ -2652,6 +2721,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload_time = "2024-01-06T02:10:55.763Z" }, { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload_time = "2024-01-06T02:10:55.763Z" },
] ]
[[package]]
name = "websocket-client"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload_time = "2024-04-23T22:16:16.976Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload_time = "2024-04-23T22:16:14.422Z" },
]
[[package]] [[package]]
name = "websockets" name = "websockets"
version = "12.0" version = "12.0"
@@ -2711,6 +2789,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/6e/e792999e816d19d7fcbfa94c730936750036d65656a76a5a688b57a656c4/werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8", size = 227274, upload_time = "2024-05-05T23:10:29.567Z" }, { url = "https://files.pythonhosted.org/packages/9d/6e/e792999e816d19d7fcbfa94c730936750036d65656a76a5a688b57a656c4/werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8", size = 227274, upload_time = "2024-05-05T23:10:29.567Z" },
] ]
[[package]]
name = "wsproto"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload_time = "2022-08-23T19:58:21.447Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload_time = "2022-08-23T19:58:19.96Z" },
]
[[package]] [[package]]
name = "zope-event" name = "zope-event"
version = "5.0" version = "5.0"
@@ -6,7 +6,6 @@
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.MANAGE_MEDIA" /> <uses-permission android:name="android.permission.MANAGE_MEDIA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
@@ -1,17 +1,17 @@
package app.alextran.immich package app.alextran.immich
import android.app.Activity
import android.content.ContentResolver import android.content.ContentResolver
import android.content.ContentUris import android.content.ContentUris
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
@@ -23,6 +23,7 @@ import io.flutter.plugin.common.PluginRegistry
import java.security.MessageDigest import java.security.MessageDigest
import java.io.FileInputStream import java.io.FileInputStream
import kotlinx.coroutines.* import kotlinx.coroutines.*
import androidx.core.net.toUri
/** /**
* Android plugin for Dart `BackgroundService` and file trash operations * Android plugin for Dart `BackgroundService` and file trash operations
@@ -33,7 +34,8 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
private var fileTrashChannel: MethodChannel? = null private var fileTrashChannel: MethodChannel? = null
private var context: Context? = null private var context: Context? = null
private var pendingResult: Result? = null private var pendingResult: Result? = null
private val PERMISSION_REQUEST_CODE = 1001 private val permissionRequestCode = 1001
private val trashRequestCode = 1002
private var activityBinding: ActivityPluginBinding? = null private var activityBinding: ActivityPluginBinding? = null
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
@@ -138,36 +140,35 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
// File Trash methods moved from MainActivity // File Trash methods moved from MainActivity
"moveToTrash" -> { "moveToTrash" -> {
val fileName = call.argument<String>("fileName") val mediaUrls = call.argument<List<String>>("mediaUrls")
if (fileName != null) { if (mediaUrls != null) {
if (hasManageStoragePermission()) { if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) {
val success = moveToTrash(fileName) moveToTrash(mediaUrls, result)
result.success(success)
} else { } else {
result.error("PERMISSION_DENIED", "Storage permission required", null) result.error("PERMISSION_DENIED", "Media permission required", null)
} }
} else { } else {
result.error("INVALID_NAME", "The file name is not specified.", null) result.error("INVALID_NAME", "The mediaUrls is not specified.", null)
} }
} }
"restoreFromTrash" -> { "restoreFromTrash" -> {
val fileName = call.argument<String>("fileName") val fileName = call.argument<String>("fileName")
if (fileName != null) { val type = call.argument<Int>("type")
if (hasManageStoragePermission()) { if (fileName != null && type != null) {
val success = untrashImage(fileName) if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) {
result.success(success) restoreFromTrash(fileName, type, result)
} else { } else {
result.error("PERMISSION_DENIED", "Storage permission required", null) result.error("PERMISSION_DENIED", "Media permission required", null)
} }
} else { } else {
result.error("INVALID_NAME", "The file name is not specified.", null) result.error("INVALID_NAME", "The file name is not specified.", null)
} }
} }
"requestManageStoragePermission" -> { "requestManageMediaPermission" -> {
if (!hasManageStoragePermission()) { if (!hasManageMediaPermission()) {
requestManageStoragePermission(result) requestManageMediaPermission(result)
} else { } else {
Log.e("Manage storage permission", "Permission already granted") Log.e("Manage storage permission", "Permission already granted")
result.success(true) result.success(true)
@@ -178,100 +179,98 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
} }
} }
// File Trash methods moved from MainActivity private fun hasManageMediaPermission(): Boolean {
private fun hasManageStoragePermission(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { MediaStore.canManageMedia(context!!);
Environment.isExternalStorageManager() } else {
} else { false
true
} }
} }
private fun requestManageStoragePermission(result: Result) { private fun requestManageMediaPermission(result: Result) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
pendingResult = result // Store the result callback pendingResult = result // Store the result callback
val activity = activityBinding?.activity ?: return val activity = activityBinding?.activity ?: return
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA)
intent.data = Uri.parse("package:${activity.packageName}") intent.data = "package:${activity.packageName}".toUri()
activity.startActivityForResult(intent, PERMISSION_REQUEST_CODE) activity.startActivityForResult(intent, permissionRequestCode)
} else { } else {
result.success(true) result.success(false)
} }
} }
private fun moveToTrash(fileName: String): Boolean { @RequiresApi(Build.VERSION_CODES.R)
val contentResolver = context?.contentResolver ?: return false private fun moveToTrash(mediaUrls: List<String>, result: Result) {
val uri = getFileUri(fileName) val urisToTrash = mediaUrls.map { it.toUri() }
if (urisToTrash.isEmpty()) {
result.error("INVALID_ARGS", "No valid URIs provided", null)
return
}
toggleTrash(urisToTrash, true, result);
}
@RequiresApi(Build.VERSION_CODES.R)
private fun restoreFromTrash(name: String, type: Int, result: Result) {
val uri = getTrashedFileUri(name, type)
if (uri == null) {
Log.e("TrashError", "Asset Uri cannot be found obtained")
result.error("TrashError", "Asset Uri cannot be found obtained", null)
return
}
Log.e("FILE_URI", uri.toString()) Log.e("FILE_URI", uri.toString())
return uri?.let { moveToTrash(it) } ?: false uri.let { toggleTrash(listOf(it), false, result) }
} }
private fun moveToTrash(contentUri: Uri): Boolean { @RequiresApi(Build.VERSION_CODES.R)
val contentResolver = context?.contentResolver ?: return false private fun toggleTrash(contentUris: List<Uri>, isTrashed: Boolean, result: Result) {
return try { val activity = activityBinding?.activity
val values = ContentValues().apply { val contentResolver = context?.contentResolver
put(MediaStore.MediaColumns.IS_TRASHED, 1) // Move to trash if (activity == null || contentResolver == null) {
result.error("TrashError", "Activity or ContentResolver not available", null)
return
} }
val updated = contentResolver.update(contentUri, values, null, null)
updated > 0 try {
} catch (e: Exception) { val pendingIntent = MediaStore.createTrashRequest(contentResolver, contentUris, isTrashed)
Log.e("TrashError", "Error moving to trash", e) pendingResult = result // Store for onActivityResult
false activity.startIntentSenderForResult(
pendingIntent.intentSender,
trashRequestCode,
null, 0, 0, 0
)
} catch (e: Exception) {
Log.e("TrashError", "Error creating or starting trash request", e)
result.error("TrashError", "Error creating or starting trash request", null)
} }
} }
private fun getFileUri(fileName: String): Uri? { @RequiresApi(Build.VERSION_CODES.R)
private fun getTrashedFileUri(fileName: String, type: Int): Uri? {
val contentResolver = context?.contentResolver ?: return null val contentResolver = context?.contentResolver ?: return null
val contentUri = MediaStore.Files.getContentUri("external") val queryUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
val projection = arrayOf(MediaStore.Images.Media._ID)
val selection = "${MediaStore.Images.Media.DISPLAY_NAME} = ?"
val selectionArgs = arrayOf(fileName)
var fileUri: Uri? = null
contentResolver.query(contentUri, projection, selection, selectionArgs, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
fileUri = ContentUris.withAppendedId(contentUri, id)
}
}
return fileUri
}
private fun untrashImage(name: String): Boolean {
val contentResolver = context?.contentResolver ?: return false
val uri = getTrashedFileUri(contentResolver, name)
Log.e("FILE_URI", uri.toString())
return uri?.let { untrashImage(it) } ?: false
}
private fun untrashImage(contentUri: Uri): Boolean {
val contentResolver = context?.contentResolver ?: return false
return try {
val values = ContentValues().apply {
put(MediaStore.MediaColumns.IS_TRASHED, 0) // Restore file
}
val updated = contentResolver.update(contentUri, values, null, null)
updated > 0
} catch (e: Exception) {
Log.e("TrashError", "Error restoring file", e)
false
}
}
private fun getTrashedFileUri(contentResolver: ContentResolver, fileName: String): Uri? {
val contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
val projection = arrayOf(MediaStore.Files.FileColumns._ID) val projection = arrayOf(MediaStore.Files.FileColumns._ID)
val queryArgs = Bundle().apply { val queryArgs = Bundle().apply {
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?") putString(
ContentResolver.QUERY_ARG_SQL_SELECTION,
"${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?"
)
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName)) putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName))
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY) putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
} }
contentResolver.query(contentUri, projection, queryArgs, null)?.use { cursor -> contentResolver.query(queryUri, projection, queryArgs, null)?.use { cursor ->
if (cursor.moveToFirst()) { if (cursor.moveToFirst()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)) val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
// same order as AssetType from dart
val contentUri = when (type) {
1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
else -> queryUri
}
return ContentUris.withAppendedId(contentUri, id) return ContentUris.withAppendedId(contentUri, id)
} }
} }
@@ -301,12 +300,19 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
// ActivityResultListener implementation // ActivityResultListener implementation
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == PERMISSION_REQUEST_CODE) { if (requestCode == permissionRequestCode) {
val granted = hasManageStoragePermission() val granted = hasManageMediaPermission()
pendingResult?.success(granted) pendingResult?.success(granted)
pendingResult = null pendingResult = null
return true return true
} }
if (requestCode == trashRequestCode) {
val approved = resultCode == Activity.RESULT_OK
pendingResult?.success(approved)
pendingResult = null
return true
}
return false return false
} }
} }
+2 -2
View File
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 194, "android.injected.version.code" => 197,
"android.injected.version.name" => "1.132.0", "android.injected.version.name" => "1.132.3",
} }
) )
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
+11 -6
View File
@@ -261,9 +261,11 @@
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1; CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100; LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
}; };
FAC6F88F2D287C890078CB2F = { FAC6F88F2D287C890078CB2F = {
CreatedOnToolsVersion = 16.0; CreatedOnToolsVersion = 16.0;
ProvisioningStyle = Automatic;
}; };
}; };
}; };
@@ -541,7 +543,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 201; CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -685,7 +687,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 201; CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -715,7 +717,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 201; CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -748,7 +750,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 201; CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -769,6 +771,7 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.vdebug.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@@ -791,7 +794,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 201; CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -811,6 +814,7 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -831,7 +835,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 201; CURRENT_PROJECT_VERSION = 205;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -851,6 +855,7 @@
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
+2 -2
View File
@@ -78,7 +78,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.131.3</string> <string>1.132.3</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@@ -93,7 +93,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>201</string> <string>205</string>
<key>FLTEnableImpeller</key> <key>FLTEnableImpeller</key>
<true/> <true/>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
+4 -1
View File
@@ -18,8 +18,11 @@ default_platform(:ios)
platform :ios do platform :ios do
desc "iOS Release" desc "iOS Release"
lane :release do lane :release do
enable_automatic_code_signing(
path: "./Runner.xcodeproj",
)
increment_version_number( increment_version_number(
version_number: "1.132.0" version_number: "1.132.3"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,
@@ -1,5 +1,5 @@
abstract interface class ILocalFilesManager { abstract interface class ILocalFilesManager {
Future<bool> moveToTrash(String fileName); Future<bool> moveToTrash(List<String> mediaUrls);
Future<bool> restoreFromTrash(String fileName); Future<bool> restoreFromTrash(String fileName, int type);
Future<bool> requestManageStoragePermission(); Future<bool> requestManageMediaPermission();
} }
+40 -57
View File
@@ -1,7 +1,6 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
@@ -13,7 +12,6 @@ import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:immich_mobile/utils/map_utils.dart';
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
import 'package:immich_mobile/widgets/common/user_avatar.dart'; import 'package:immich_mobile/widgets/common/user_avatar.dart';
@@ -357,66 +355,51 @@ class PlacesCollectionCard extends StatelessWidget {
final widthFactor = isTablet ? 0.25 : 0.5; final widthFactor = isTablet ? 0.25 : 0.5;
final size = context.width * widthFactor - 20.0; final size = context.width * widthFactor - 20.0;
return FutureBuilder<(Position?, LocationPermission?)>( return GestureDetector(
future: MapUtils.checkPermAndGetLocation( onTap: () => context.pushRoute(
context: context, PlacesCollectionRoute(
silent: true, currentLocation: null,
),
), ),
builder: (context, snapshot) { child: Column(
var position = snapshot.data?.$1; crossAxisAlignment: CrossAxisAlignment.start,
return GestureDetector( children: [
onTap: () => context.pushRoute( SizedBox(
PlacesCollectionRoute( height: size,
currentLocation: position != null width: size,
? LatLng(position.latitude, position.longitude) child: DecoratedBox(
: null, decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(20)),
color:
context.colorScheme.secondaryContainer.withAlpha(100),
),
child: IgnorePointer(
child: MapThumbnail(
zoom: 8,
centre: const LatLng(
21.44950,
-157.91959,
),
showAttribution: false,
themeMode: context.isDarkTheme
? ThemeMode.dark
: ThemeMode.light,
),
),
), ),
), ),
child: Column( Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.all(8.0),
children: [ child: Text(
SizedBox( 'places'.tr(),
height: size, style: context.textTheme.titleSmall?.copyWith(
width: size, color: context.colorScheme.onSurface,
child: DecoratedBox( fontWeight: FontWeight.w500,
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(20)),
color: context.colorScheme.secondaryContainer
.withAlpha(100),
),
child: IgnorePointer(
child: snapshot.connectionState ==
ConnectionState.waiting
? const Center(child: CircularProgressIndicator())
: MapThumbnail(
zoom: 8,
centre: LatLng(
position?.latitude ?? 21.44950,
position?.longitude ?? -157.91959,
),
showAttribution: false,
themeMode: context.isDarkTheme
? ThemeMode.dark
: ThemeMode.light,
),
),
),
), ),
Padding( ),
padding: const EdgeInsets.all(8.0),
child: Text(
'places'.tr(),
style: context.textTheme.titleSmall?.copyWith(
color: context.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
],
), ),
); ],
}, ),
); );
}, },
); );
@@ -44,7 +44,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
} }
}), }),
child: const Text( child: const Text(
'grant_permission', 'continue',
).tr(), ).tr(),
), ),
], ],
@@ -3,21 +3,23 @@ import 'package:immich_mobile/interfaces/local_files_manager.interface.dart';
import 'package:immich_mobile/utils/local_files_manager.dart'; import 'package:immich_mobile/utils/local_files_manager.dart';
final localFilesManagerRepositoryProvider = final localFilesManagerRepositoryProvider =
Provider((ref) => LocalFilesManagerRepository()); Provider((ref) => const LocalFilesManagerRepository());
class LocalFilesManagerRepository implements ILocalFilesManager { class LocalFilesManagerRepository implements ILocalFilesManager {
const LocalFilesManagerRepository();
@override @override
Future<bool> moveToTrash(String fileName) async { Future<bool> moveToTrash(List<String> mediaUrls) async {
return await LocalFilesManager.moveToTrash(fileName); return await LocalFilesManager.moveToTrash(mediaUrls);
} }
@override @override
Future<bool> restoreFromTrash(String fileName) async { Future<bool> restoreFromTrash(String fileName, int type) async {
return await LocalFilesManager.restoreFromTrash(fileName); return await LocalFilesManager.restoreFromTrash(fileName, type);
} }
@override @override
Future<bool> requestManageStoragePermission() async { Future<bool> requestManageMediaPermission() async {
return await LocalFilesManager.requestManageStoragePermission(); return await LocalFilesManager.requestManageMediaPermission();
} }
} }
+25 -6
View File
@@ -255,9 +255,12 @@ class SyncService {
.where((asset) => idsToDelete.contains(asset.remoteId)) .where((asset) => idsToDelete.contains(asset.remoteId))
.toList(); .toList();
for (var asset in matchedAssets) { final mediaUrls = await Future.wait(
_localFilesManager.moveToTrash(asset.fileName); matchedAssets
} .map((asset) => asset.local?.getMediaUrl() ?? Future.value(null)),
);
await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
} }
/// Deletes remote-only assets, updates merged assets to be local-only /// Deletes remote-only assets, updates merged assets to be local-only
@@ -819,13 +822,29 @@ class SyncService {
} }
Future<void> _toggleTrashStatusForAssets(List<Asset> assetsList) async { Future<void> _toggleTrashStatusForAssets(List<Asset> assetsList) async {
for (var asset in assetsList) { final trashMediaUrls = <String>[];
for (final asset in assetsList) {
if (asset.isTrashed) { if (asset.isTrashed) {
_localFilesManager.moveToTrash(asset.fileName); final mediaUrl = await asset.local?.getMediaUrl();
if (mediaUrl == null) {
_log.warning(
"Failed to get media URL for asset ${asset.name} while moving to trash",
);
continue;
}
trashMediaUrls.add(mediaUrl);
} else { } else {
_localFilesManager.restoreFromTrash(asset.fileName); await _localFilesManager.restoreFromTrash(
asset.fileName,
asset.type.index,
);
} }
} }
if (trashMediaUrls.isNotEmpty) {
await _localFilesManager.moveToTrash(trashMediaUrls);
}
} }
/// Inserts or updates the assets in the database with their ExifInfo (if any) /// Inserts or updates the assets in the database with their ExifInfo (if any)
+19 -20
View File
@@ -1,38 +1,37 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
class LocalFilesManager { abstract final class LocalFilesManager {
static final Logger _logger = Logger('LocalFilesManager');
static const MethodChannel _channel = MethodChannel('file_trash'); static const MethodChannel _channel = MethodChannel('file_trash');
static Future<bool> moveToTrash(String fileName) async { static Future<bool> moveToTrash(List<String> mediaUrls) async {
try { try {
final bool success = return await _channel
await _channel.invokeMethod('moveToTrash', {'fileName': fileName}); .invokeMethod('moveToTrash', {'mediaUrls': mediaUrls});
return success; } catch (e, s) {
} on PlatformException catch (e) { _logger.warning('Error moving file to trash', e, s);
debugPrint('Error moving to trash: ${e.message}');
return false; return false;
} }
} }
static Future<bool> restoreFromTrash(String fileName) async { static Future<bool> restoreFromTrash(String fileName, int type) async {
try { try {
final bool success = await _channel return await _channel.invokeMethod(
.invokeMethod('restoreFromTrash', {'fileName': fileName}); 'restoreFromTrash',
return success; {'fileName': fileName, 'type': type},
} on PlatformException catch (e) { );
debugPrint('Error restoring file: ${e.message}'); } catch (e, s) {
_logger.warning('Error restore file from trash', e, s);
return false; return false;
} }
} }
static Future<bool> requestManageStoragePermission() async { static Future<bool> requestManageMediaPermission() async {
try { try {
final bool success = return await _channel.invokeMethod('requestManageMediaPermission');
await _channel.invokeMethod('requestManageStoragePermission'); } catch (e, s) {
return success; _logger.warning('Error requesting manage media permission', e, s);
} on PlatformException catch (e) {
debugPrint('Error requesting permission: ${e.message}');
return false; return false;
} }
} }
+59 -14
View File
@@ -3,7 +3,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/widgets.dart'; import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/android_device_asset.entity.dart'; import 'package:immich_mobile/entities/android_device_asset.entity.dart';
@@ -17,6 +17,8 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
// ignore: import_rule_photo_manager
import 'package:photo_manager/photo_manager.dart';
const int targetVersion = 10; const int targetVersion = 10;
@@ -69,14 +71,45 @@ Future<void> _migrateDeviceAsset(Isar db) async {
: (await db.iOSDeviceAssets.where().findAll()) : (await db.iOSDeviceAssets.where().findAll())
.map((i) => _DeviceAsset(assetId: i.id, hash: i.hash)) .map((i) => _DeviceAsset(assetId: i.id, hash: i.hash))
.toList(); .toList();
final localAssets = (await db.assets
.where() final PermissionState ps = await PhotoManager.requestPermissionExtend();
.anyOf(ids, (query, id) => query.localIdEqualTo(id.assetId)) if (!ps.hasAccess) {
.findAll()) if (kDebugMode) {
.map((a) => _DeviceAsset(assetId: a.localId!, dateTime: a.fileModifiedAt)) debugPrint(
.toList(); "[MIGRATION] Photo library permission not granted. Skipping device asset migration.",
debugPrint("Device Asset Ids length - ${ids.length}"); );
debugPrint("Local Asset Ids length - ${localAssets.length}"); }
return;
}
List<_DeviceAsset> localAssets = [];
final List<AssetPathEntity> paths =
await PhotoManager.getAssetPathList(onlyAll: true);
if (paths.isEmpty) {
localAssets = (await db.assets
.where()
.anyOf(ids, (query, id) => query.localIdEqualTo(id.assetId))
.findAll())
.map(
(a) => _DeviceAsset(assetId: a.localId!, dateTime: a.fileModifiedAt),
)
.toList();
} else {
final AssetPathEntity albumWithAll = paths.first;
final int assetCount = await albumWithAll.assetCountAsync;
final List<AssetEntity> allDeviceAssets =
await albumWithAll.getAssetListRange(start: 0, end: assetCount);
localAssets = allDeviceAssets
.map((a) => _DeviceAsset(assetId: a.id, dateTime: a.modifiedDateTime))
.toList();
}
debugPrint("[MIGRATION] Device Asset Ids length - ${ids.length}");
debugPrint("[MIGRATION] Local Asset Ids length - ${localAssets.length}");
ids.sort((a, b) => a.assetId.compareTo(b.assetId)); ids.sort((a, b) => a.assetId.compareTo(b.assetId));
localAssets.sort((a, b) => a.assetId.compareTo(b.assetId)); localAssets.sort((a, b) => a.assetId.compareTo(b.assetId));
final List<DeviceAssetEntity> toAdd = []; final List<DeviceAssetEntity> toAdd = [];
@@ -95,15 +128,27 @@ Future<void> _migrateDeviceAsset(Isar db) async {
return false; return false;
}, },
onlyFirst: (deviceAsset) { onlyFirst: (deviceAsset) {
debugPrint( if (kDebugMode) {
'DeviceAsset not found in local assets: ${deviceAsset.assetId}', debugPrint(
); '[MIGRATION] Local asset not found in DeviceAsset: ${deviceAsset.assetId}',
);
}
}, },
onlySecond: (asset) { onlySecond: (asset) {
debugPrint('Local asset not found in DeviceAsset: ${asset.assetId}'); if (kDebugMode) {
debugPrint(
'[MIGRATION] Local asset not found in DeviceAsset: ${asset.assetId}',
);
}
}, },
); );
debugPrint("Total number of device assets migrated - ${toAdd.length}");
if (kDebugMode) {
debugPrint(
"[MIGRATION] Total number of device assets migrated - ${toAdd.length}",
);
}
await db.writeTxn(() async { await db.writeTxn(() async {
await db.deviceAssetEntitys.putAll(toAdd); await db.deviceAssetEntitys.putAll(toAdd);
}); });
+5 -3
View File
@@ -8,6 +8,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/services/asset.service.dart'; import 'package:immich_mobile/services/asset.service.dart';
import 'package:immich_mobile/services/share.service.dart'; import 'package:immich_mobile/services/share.service.dart';
import 'package:immich_mobile/utils/translation.dart';
import 'package:immich_mobile/widgets/common/date_time_picker.dart'; import 'package:immich_mobile/widgets/common/date_time_picker.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/location_picker.dart'; import 'package:immich_mobile/widgets/common/location_picker.dart';
@@ -57,12 +58,13 @@ Future<void> handleArchiveAssets(
.read(assetProvider.notifier) .read(assetProvider.notifier)
.toggleArchive(selection, shouldArchive); .toggleArchive(selection, shouldArchive);
final assetOrAssets = selection.length > 1 ? 'assets' : 'asset'; final message = shouldArchive
final archiveOrLibrary = shouldArchive ? 'archive' : 'library'; ? t('moved_to_archive', {'count': selection.length})
: t('moved_to_library', {'count': selection.length});
if (context.mounted) { if (context.mounted) {
ImmichToast.show( ImmichToast.show(
context: context, context: context,
msg: 'Moved ${selection.length} $assetOrAssets to $archiveOrLibrary', msg: message,
gravity: toastGravity, gravity: toastGravity,
); );
} }
+14
View File
@@ -0,0 +1,14 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:intl/message_format.dart';
String t(String key, [Map<String, Object>? args]) {
try {
String message = key.tr();
if (args != null) {
return MessageFormat(message).format(args);
}
return message;
} catch (e) {
return key;
}
}
@@ -97,6 +97,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
); );
if (7 - scaleFactor.value.toInt() != perRow.value) { if (7 - scaleFactor.value.toInt() != perRow.value) {
perRow.value = 7 - scaleFactor.value.toInt(); perRow.value = 7 - scaleFactor.value.toInt();
settings.setSetting(AppSettingsEnum.tilesPerRow, perRow.value);
} }
}; };
}), }),
@@ -755,7 +755,7 @@ class _MonthTitle extends StatelessWidget {
key: Key("month-$title"), key: Key("month-$title"),
padding: const EdgeInsets.only(left: 12.0, top: 24.0), padding: const EdgeInsets.only(left: 12.0, top: 24.0),
child: Text( child: Text(
title, toBeginningOfSentenceCase(title, context.locale.languageCode),
style: const TextStyle( style: const TextStyle(
fontSize: 26, fontSize: 26,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -786,7 +786,7 @@ class _Title extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GroupDividerTitle( return GroupDividerTitle(
text: title, text: toBeginningOfSentenceCase(title, context.locale.languageCode),
multiselectEnabled: selectionActive, multiselectEnabled: selectionActive,
onSelect: () => selectAssets(assets), onSelect: () => selectAssets(assets),
onDeselect: () => deselectAssets(assets), onDeselect: () => deselectAssets(assets),
+22 -3
View File
@@ -207,9 +207,27 @@ class LoginForm extends HookConsumerWidget {
} }
String generateRandomString(int length) { String generateRandomString(int length) {
const chars =
'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
final random = Random.secure(); final random = Random.secure();
return base64Url return String.fromCharCodes(
.encode(List<int>.generate(32, (i) => random.nextInt(256))); Iterable.generate(
length,
(_) => chars.codeUnitAt(random.nextInt(chars.length)),
),
);
}
List<int> randomBytes(int length) {
final random = Random.secure();
return List<int>.generate(length, (i) => random.nextInt(256));
}
/// Per specification, the code verifier must be 43-128 characters long
/// and consist of characters [A-Z, a-z, 0-9, "-", ".", "_", "~"]
/// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
String randomCodeVerifier() {
return base64Url.encode(randomBytes(42));
} }
Future<String> generatePKCECodeChallenge(String codeVerifier) async { Future<String> generatePKCECodeChallenge(String codeVerifier) async {
@@ -223,7 +241,8 @@ class LoginForm extends HookConsumerWidget {
String? oAuthServerUrl; String? oAuthServerUrl;
final state = generateRandomString(32); final state = generateRandomString(32);
final codeVerifier = generateRandomString(64);
final codeVerifier = randomCodeVerifier();
final codeChallenge = await generatePKCECodeChallenge(codeVerifier); final codeChallenge = await generatePKCECodeChallenge(codeVerifier);
try { try {
@@ -49,7 +49,7 @@ class AdvancedSettings extends HookConsumerWidget {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
int sdkVersion = androidInfo.version.sdkInt; int sdkVersion = androidInfo.version.sdkInt;
return sdkVersion >= 30; return sdkVersion >= 31;
} }
return false; return false;
} }
@@ -74,7 +74,7 @@ class AdvancedSettings extends HookConsumerWidget {
if (value) { if (value) {
final result = await ref final result = await ref
.read(localFilesManagerRepositoryProvider) .read(localFilesManagerRepositoryProvider)
.requestManageStoragePermission(); .requestManageMediaPermission();
manageLocalMediaAndroid.value = result; manageLocalMediaAndroid.value = result;
} }
}, },
+18 -4
View File
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.132.0 - API version: 1.132.3
- Generator version: 7.8.0 - Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen - Build package: org.openapitools.codegen.languages.DartClientCodegen
@@ -145,8 +145,15 @@ Class | Method | HTTP request | Description
*MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets | *MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets |
*MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories | *MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories |
*MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} | *MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} |
*NotificationsAdminApi* | [**getNotificationTemplateAdmin**](doc//NotificationsAdminApi.md#getnotificationtemplateadmin) | **POST** /notifications/admin/templates/{name} | *NotificationsApi* | [**deleteNotification**](doc//NotificationsApi.md#deletenotification) | **DELETE** /notifications/{id} |
*NotificationsAdminApi* | [**sendTestEmailAdmin**](doc//NotificationsAdminApi.md#sendtestemailadmin) | **POST** /notifications/admin/test-email | *NotificationsApi* | [**deleteNotifications**](doc//NotificationsApi.md#deletenotifications) | **DELETE** /notifications |
*NotificationsApi* | [**getNotification**](doc//NotificationsApi.md#getnotification) | **GET** /notifications/{id} |
*NotificationsApi* | [**getNotifications**](doc//NotificationsApi.md#getnotifications) | **GET** /notifications |
*NotificationsApi* | [**updateNotification**](doc//NotificationsApi.md#updatenotification) | **PUT** /notifications/{id} |
*NotificationsApi* | [**updateNotifications**](doc//NotificationsApi.md#updatenotifications) | **PUT** /notifications |
*NotificationsAdminApi* | [**createNotification**](doc//NotificationsAdminApi.md#createnotification) | **POST** /admin/notifications |
*NotificationsAdminApi* | [**getNotificationTemplateAdmin**](doc//NotificationsAdminApi.md#getnotificationtemplateadmin) | **POST** /admin/notifications/templates/{name} |
*NotificationsAdminApi* | [**sendTestEmailAdmin**](doc//NotificationsAdminApi.md#sendtestemailadmin) | **POST** /admin/notifications/test-email |
*OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback | *OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback |
*OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link | *OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link |
*OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect | *OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect |
@@ -300,7 +307,6 @@ Class | Method | HTTP request | Description
- [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
- [AssetTypeEnum](doc//AssetTypeEnum.md) - [AssetTypeEnum](doc//AssetTypeEnum.md)
- [AudioCodec](doc//AudioCodec.md) - [AudioCodec](doc//AudioCodec.md)
- [AvatarResponse](doc//AvatarResponse.md)
- [AvatarUpdate](doc//AvatarUpdate.md) - [AvatarUpdate](doc//AvatarUpdate.md)
- [BulkIdResponseDto](doc//BulkIdResponseDto.md) - [BulkIdResponseDto](doc//BulkIdResponseDto.md)
- [BulkIdsDto](doc//BulkIdsDto.md) - [BulkIdsDto](doc//BulkIdsDto.md)
@@ -361,9 +367,17 @@ Class | Method | HTTP request | Description
- [MemoryUpdateDto](doc//MemoryUpdateDto.md) - [MemoryUpdateDto](doc//MemoryUpdateDto.md)
- [MergePersonDto](doc//MergePersonDto.md) - [MergePersonDto](doc//MergePersonDto.md)
- [MetadataSearchDto](doc//MetadataSearchDto.md) - [MetadataSearchDto](doc//MetadataSearchDto.md)
- [NotificationCreateDto](doc//NotificationCreateDto.md)
- [NotificationDeleteAllDto](doc//NotificationDeleteAllDto.md)
- [NotificationDto](doc//NotificationDto.md)
- [NotificationLevel](doc//NotificationLevel.md)
- [NotificationType](doc//NotificationType.md)
- [NotificationUpdateAllDto](doc//NotificationUpdateAllDto.md)
- [NotificationUpdateDto](doc//NotificationUpdateDto.md)
- [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md) - [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md)
- [OAuthCallbackDto](doc//OAuthCallbackDto.md) - [OAuthCallbackDto](doc//OAuthCallbackDto.md)
- [OAuthConfigDto](doc//OAuthConfigDto.md) - [OAuthConfigDto](doc//OAuthConfigDto.md)
- [OAuthTokenEndpointAuthMethod](doc//OAuthTokenEndpointAuthMethod.md)
- [OnThisDayDto](doc//OnThisDayDto.md) - [OnThisDayDto](doc//OnThisDayDto.md)
- [PartnerDirection](doc//PartnerDirection.md) - [PartnerDirection](doc//PartnerDirection.md)
- [PartnerResponseDto](doc//PartnerResponseDto.md) - [PartnerResponseDto](doc//PartnerResponseDto.md)
+9 -1
View File
@@ -44,6 +44,7 @@ part 'api/jobs_api.dart';
part 'api/libraries_api.dart'; part 'api/libraries_api.dart';
part 'api/map_api.dart'; part 'api/map_api.dart';
part 'api/memories_api.dart'; part 'api/memories_api.dart';
part 'api/notifications_api.dart';
part 'api/notifications_admin_api.dart'; part 'api/notifications_admin_api.dart';
part 'api/o_auth_api.dart'; part 'api/o_auth_api.dart';
part 'api/partners_api.dart'; part 'api/partners_api.dart';
@@ -107,7 +108,6 @@ part 'model/asset_stack_response_dto.dart';
part 'model/asset_stats_response_dto.dart'; part 'model/asset_stats_response_dto.dart';
part 'model/asset_type_enum.dart'; part 'model/asset_type_enum.dart';
part 'model/audio_codec.dart'; part 'model/audio_codec.dart';
part 'model/avatar_response.dart';
part 'model/avatar_update.dart'; part 'model/avatar_update.dart';
part 'model/bulk_id_response_dto.dart'; part 'model/bulk_id_response_dto.dart';
part 'model/bulk_ids_dto.dart'; part 'model/bulk_ids_dto.dart';
@@ -168,9 +168,17 @@ part 'model/memory_type.dart';
part 'model/memory_update_dto.dart'; part 'model/memory_update_dto.dart';
part 'model/merge_person_dto.dart'; part 'model/merge_person_dto.dart';
part 'model/metadata_search_dto.dart'; part 'model/metadata_search_dto.dart';
part 'model/notification_create_dto.dart';
part 'model/notification_delete_all_dto.dart';
part 'model/notification_dto.dart';
part 'model/notification_level.dart';
part 'model/notification_type.dart';
part 'model/notification_update_all_dto.dart';
part 'model/notification_update_dto.dart';
part 'model/o_auth_authorize_response_dto.dart'; part 'model/o_auth_authorize_response_dto.dart';
part 'model/o_auth_callback_dto.dart'; part 'model/o_auth_callback_dto.dart';
part 'model/o_auth_config_dto.dart'; part 'model/o_auth_config_dto.dart';
part 'model/o_auth_token_endpoint_auth_method.dart';
part 'model/on_this_day_dto.dart'; part 'model/on_this_day_dto.dart';
part 'model/partner_direction.dart'; part 'model/partner_direction.dart';
part 'model/partner_response_dto.dart'; part 'model/partner_response_dto.dart';
+51 -4
View File
@@ -16,7 +16,54 @@ class NotificationsAdminApi {
final ApiClient apiClient; final ApiClient apiClient;
/// Performs an HTTP 'POST /notifications/admin/templates/{name}' operation and returns the [Response]. /// Performs an HTTP 'POST /admin/notifications' operation and returns the [Response].
/// Parameters:
///
/// * [NotificationCreateDto] notificationCreateDto (required):
Future<Response> createNotificationWithHttpInfo(NotificationCreateDto notificationCreateDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/admin/notifications';
// ignore: prefer_final_locals
Object? postBody = notificationCreateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [NotificationCreateDto] notificationCreateDto (required):
Future<NotificationDto?> createNotification(NotificationCreateDto notificationCreateDto,) async {
final response = await createNotificationWithHttpInfo(notificationCreateDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'NotificationDto',) as NotificationDto;
}
return null;
}
/// Performs an HTTP 'POST /admin/notifications/templates/{name}' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
/// * [String] name (required): /// * [String] name (required):
@@ -24,7 +71,7 @@ class NotificationsAdminApi {
/// * [TemplateDto] templateDto (required): /// * [TemplateDto] templateDto (required):
Future<Response> getNotificationTemplateAdminWithHttpInfo(String name, TemplateDto templateDto,) async { Future<Response> getNotificationTemplateAdminWithHttpInfo(String name, TemplateDto templateDto,) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/notifications/admin/templates/{name}' final apiPath = r'/admin/notifications/templates/{name}'
.replaceAll('{name}', name); .replaceAll('{name}', name);
// ignore: prefer_final_locals // ignore: prefer_final_locals
@@ -68,13 +115,13 @@ class NotificationsAdminApi {
return null; return null;
} }
/// Performs an HTTP 'POST /notifications/admin/test-email' operation and returns the [Response]. /// Performs an HTTP 'POST /admin/notifications/test-email' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
/// * [SystemConfigSmtpDto] systemConfigSmtpDto (required): /// * [SystemConfigSmtpDto] systemConfigSmtpDto (required):
Future<Response> sendTestEmailAdminWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto,) async { Future<Response> sendTestEmailAdminWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto,) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/notifications/admin/test-email'; final apiPath = r'/admin/notifications/test-email';
// ignore: prefer_final_locals // ignore: prefer_final_locals
Object? postBody = systemConfigSmtpDto; Object? postBody = systemConfigSmtpDto;
+311
View File
@@ -0,0 +1,311 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class NotificationsApi {
NotificationsApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// Performs an HTTP 'DELETE /notifications/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> deleteNotificationWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/notifications/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<void> deleteNotification(String id,) async {
final response = await deleteNotificationWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'DELETE /notifications' operation and returns the [Response].
/// Parameters:
///
/// * [NotificationDeleteAllDto] notificationDeleteAllDto (required):
Future<Response> deleteNotificationsWithHttpInfo(NotificationDeleteAllDto notificationDeleteAllDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/notifications';
// ignore: prefer_final_locals
Object? postBody = notificationDeleteAllDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [NotificationDeleteAllDto] notificationDeleteAllDto (required):
Future<void> deleteNotifications(NotificationDeleteAllDto notificationDeleteAllDto,) async {
final response = await deleteNotificationsWithHttpInfo(notificationDeleteAllDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Performs an HTTP 'GET /notifications/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> getNotificationWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/notifications/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<NotificationDto?> getNotification(String id,) async {
final response = await getNotificationWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'NotificationDto',) as NotificationDto;
}
return null;
}
/// Performs an HTTP 'GET /notifications' operation and returns the [Response].
/// Parameters:
///
/// * [String] id:
///
/// * [NotificationLevel] level:
///
/// * [NotificationType] type:
///
/// * [bool] unread:
Future<Response> getNotificationsWithHttpInfo({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/notifications';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (id != null) {
queryParams.addAll(_queryParams('', 'id', id));
}
if (level != null) {
queryParams.addAll(_queryParams('', 'level', level));
}
if (type != null) {
queryParams.addAll(_queryParams('', 'type', type));
}
if (unread != null) {
queryParams.addAll(_queryParams('', 'unread', unread));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id:
///
/// * [NotificationLevel] level:
///
/// * [NotificationType] type:
///
/// * [bool] unread:
Future<List<NotificationDto>?> getNotifications({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async {
final response = await getNotificationsWithHttpInfo( id: id, level: level, type: type, unread: unread, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<NotificationDto>') as List)
.cast<NotificationDto>()
.toList(growable: false);
}
return null;
}
/// Performs an HTTP 'PUT /notifications/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [NotificationUpdateDto] notificationUpdateDto (required):
Future<Response> updateNotificationWithHttpInfo(String id, NotificationUpdateDto notificationUpdateDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/notifications/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = notificationUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [NotificationUpdateDto] notificationUpdateDto (required):
Future<NotificationDto?> updateNotification(String id, NotificationUpdateDto notificationUpdateDto,) async {
final response = await updateNotificationWithHttpInfo(id, notificationUpdateDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'NotificationDto',) as NotificationDto;
}
return null;
}
/// Performs an HTTP 'PUT /notifications' operation and returns the [Response].
/// Parameters:
///
/// * [NotificationUpdateAllDto] notificationUpdateAllDto (required):
Future<Response> updateNotificationsWithHttpInfo(NotificationUpdateAllDto notificationUpdateAllDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/notifications';
// ignore: prefer_final_locals
Object? postBody = notificationUpdateAllDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [NotificationUpdateAllDto] notificationUpdateAllDto (required):
Future<void> updateNotifications(NotificationUpdateAllDto notificationUpdateAllDto,) async {
final response = await updateNotificationsWithHttpInfo(notificationUpdateAllDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
}
+16 -2
View File
@@ -270,8 +270,6 @@ class ApiClient {
return AssetTypeEnumTypeTransformer().decode(value); return AssetTypeEnumTypeTransformer().decode(value);
case 'AudioCodec': case 'AudioCodec':
return AudioCodecTypeTransformer().decode(value); return AudioCodecTypeTransformer().decode(value);
case 'AvatarResponse':
return AvatarResponse.fromJson(value);
case 'AvatarUpdate': case 'AvatarUpdate':
return AvatarUpdate.fromJson(value); return AvatarUpdate.fromJson(value);
case 'BulkIdResponseDto': case 'BulkIdResponseDto':
@@ -392,12 +390,28 @@ class ApiClient {
return MergePersonDto.fromJson(value); return MergePersonDto.fromJson(value);
case 'MetadataSearchDto': case 'MetadataSearchDto':
return MetadataSearchDto.fromJson(value); return MetadataSearchDto.fromJson(value);
case 'NotificationCreateDto':
return NotificationCreateDto.fromJson(value);
case 'NotificationDeleteAllDto':
return NotificationDeleteAllDto.fromJson(value);
case 'NotificationDto':
return NotificationDto.fromJson(value);
case 'NotificationLevel':
return NotificationLevelTypeTransformer().decode(value);
case 'NotificationType':
return NotificationTypeTypeTransformer().decode(value);
case 'NotificationUpdateAllDto':
return NotificationUpdateAllDto.fromJson(value);
case 'NotificationUpdateDto':
return NotificationUpdateDto.fromJson(value);
case 'OAuthAuthorizeResponseDto': case 'OAuthAuthorizeResponseDto':
return OAuthAuthorizeResponseDto.fromJson(value); return OAuthAuthorizeResponseDto.fromJson(value);
case 'OAuthCallbackDto': case 'OAuthCallbackDto':
return OAuthCallbackDto.fromJson(value); return OAuthCallbackDto.fromJson(value);
case 'OAuthConfigDto': case 'OAuthConfigDto':
return OAuthConfigDto.fromJson(value); return OAuthConfigDto.fromJson(value);
case 'OAuthTokenEndpointAuthMethod':
return OAuthTokenEndpointAuthMethodTypeTransformer().decode(value);
case 'OnThisDayDto': case 'OnThisDayDto':
return OnThisDayDto.fromJson(value); return OnThisDayDto.fromJson(value);
case 'PartnerDirection': case 'PartnerDirection':
+9
View File
@@ -100,6 +100,15 @@ String parameterToString(dynamic value) {
if (value is MemoryType) { if (value is MemoryType) {
return MemoryTypeTypeTransformer().encode(value).toString(); return MemoryTypeTypeTransformer().encode(value).toString();
} }
if (value is NotificationLevel) {
return NotificationLevelTypeTransformer().encode(value).toString();
}
if (value is NotificationType) {
return NotificationTypeTypeTransformer().encode(value).toString();
}
if (value is OAuthTokenEndpointAuthMethod) {
return OAuthTokenEndpointAuthMethodTypeTransformer().encode(value).toString();
}
if (value is PartnerDirection) { if (value is PartnerDirection) {
return PartnerDirectionTypeTransformer().encode(value).toString(); return PartnerDirectionTypeTransformer().encode(value).toString();
} }
+180
View File
@@ -0,0 +1,180 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class NotificationCreateDto {
/// Returns a new [NotificationCreateDto] instance.
NotificationCreateDto({
this.data,
this.description,
this.level,
this.readAt,
required this.title,
this.type,
required this.userId,
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
Object? data;
String? description;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
NotificationLevel? level;
DateTime? readAt;
String title;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
NotificationType? type;
String userId;
@override
bool operator ==(Object other) => identical(this, other) || other is NotificationCreateDto &&
other.data == data &&
other.description == description &&
other.level == level &&
other.readAt == readAt &&
other.title == title &&
other.type == type &&
other.userId == userId;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(data == null ? 0 : data!.hashCode) +
(description == null ? 0 : description!.hashCode) +
(level == null ? 0 : level!.hashCode) +
(readAt == null ? 0 : readAt!.hashCode) +
(title.hashCode) +
(type == null ? 0 : type!.hashCode) +
(userId.hashCode);
@override
String toString() => 'NotificationCreateDto[data=$data, description=$description, level=$level, readAt=$readAt, title=$title, type=$type, userId=$userId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.data != null) {
json[r'data'] = this.data;
} else {
// json[r'data'] = null;
}
if (this.description != null) {
json[r'description'] = this.description;
} else {
// json[r'description'] = null;
}
if (this.level != null) {
json[r'level'] = this.level;
} else {
// json[r'level'] = null;
}
if (this.readAt != null) {
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
} else {
// json[r'readAt'] = null;
}
json[r'title'] = this.title;
if (this.type != null) {
json[r'type'] = this.type;
} else {
// json[r'type'] = null;
}
json[r'userId'] = this.userId;
return json;
}
/// Returns a new [NotificationCreateDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static NotificationCreateDto? fromJson(dynamic value) {
upgradeDto(value, "NotificationCreateDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return NotificationCreateDto(
data: mapValueOfType<Object>(json, r'data'),
description: mapValueOfType<String>(json, r'description'),
level: NotificationLevel.fromJson(json[r'level']),
readAt: mapDateTime(json, r'readAt', r''),
title: mapValueOfType<String>(json, r'title')!,
type: NotificationType.fromJson(json[r'type']),
userId: mapValueOfType<String>(json, r'userId')!,
);
}
return null;
}
static List<NotificationCreateDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <NotificationCreateDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = NotificationCreateDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, NotificationCreateDto> mapFromJson(dynamic json) {
final map = <String, NotificationCreateDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = NotificationCreateDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of NotificationCreateDto-objects as value to a dart map
static Map<String, List<NotificationCreateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<NotificationCreateDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = NotificationCreateDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'title',
'userId',
};
}
+101
View File
@@ -0,0 +1,101 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class NotificationDeleteAllDto {
/// Returns a new [NotificationDeleteAllDto] instance.
NotificationDeleteAllDto({
this.ids = const [],
});
List<String> ids;
@override
bool operator ==(Object other) => identical(this, other) || other is NotificationDeleteAllDto &&
_deepEquality.equals(other.ids, ids);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(ids.hashCode);
@override
String toString() => 'NotificationDeleteAllDto[ids=$ids]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'ids'] = this.ids;
return json;
}
/// Returns a new [NotificationDeleteAllDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static NotificationDeleteAllDto? fromJson(dynamic value) {
upgradeDto(value, "NotificationDeleteAllDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return NotificationDeleteAllDto(
ids: json[r'ids'] is Iterable
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
: const [],
);
}
return null;
}
static List<NotificationDeleteAllDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <NotificationDeleteAllDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = NotificationDeleteAllDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, NotificationDeleteAllDto> mapFromJson(dynamic json) {
final map = <String, NotificationDeleteAllDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = NotificationDeleteAllDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of NotificationDeleteAllDto-objects as value to a dart map
static Map<String, List<NotificationDeleteAllDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<NotificationDeleteAllDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = NotificationDeleteAllDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'ids',
};
}
+182
View File
@@ -0,0 +1,182 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class NotificationDto {
/// Returns a new [NotificationDto] instance.
NotificationDto({
required this.createdAt,
this.data,
this.description,
required this.id,
required this.level,
this.readAt,
required this.title,
required this.type,
});
DateTime createdAt;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
Object? data;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? description;
String id;
NotificationLevel level;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
DateTime? readAt;
String title;
NotificationType type;
@override
bool operator ==(Object other) => identical(this, other) || other is NotificationDto &&
other.createdAt == createdAt &&
other.data == data &&
other.description == description &&
other.id == id &&
other.level == level &&
other.readAt == readAt &&
other.title == title &&
other.type == type;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(createdAt.hashCode) +
(data == null ? 0 : data!.hashCode) +
(description == null ? 0 : description!.hashCode) +
(id.hashCode) +
(level.hashCode) +
(readAt == null ? 0 : readAt!.hashCode) +
(title.hashCode) +
(type.hashCode);
@override
String toString() => 'NotificationDto[createdAt=$createdAt, data=$data, description=$description, id=$id, level=$level, readAt=$readAt, title=$title, type=$type]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
if (this.data != null) {
json[r'data'] = this.data;
} else {
// json[r'data'] = null;
}
if (this.description != null) {
json[r'description'] = this.description;
} else {
// json[r'description'] = null;
}
json[r'id'] = this.id;
json[r'level'] = this.level;
if (this.readAt != null) {
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
} else {
// json[r'readAt'] = null;
}
json[r'title'] = this.title;
json[r'type'] = this.type;
return json;
}
/// Returns a new [NotificationDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static NotificationDto? fromJson(dynamic value) {
upgradeDto(value, "NotificationDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return NotificationDto(
createdAt: mapDateTime(json, r'createdAt', r'')!,
data: mapValueOfType<Object>(json, r'data'),
description: mapValueOfType<String>(json, r'description'),
id: mapValueOfType<String>(json, r'id')!,
level: NotificationLevel.fromJson(json[r'level'])!,
readAt: mapDateTime(json, r'readAt', r''),
title: mapValueOfType<String>(json, r'title')!,
type: NotificationType.fromJson(json[r'type'])!,
);
}
return null;
}
static List<NotificationDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <NotificationDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = NotificationDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, NotificationDto> mapFromJson(dynamic json) {
final map = <String, NotificationDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = NotificationDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of NotificationDto-objects as value to a dart map
static Map<String, List<NotificationDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<NotificationDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = NotificationDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'createdAt',
'id',
'level',
'title',
'type',
};
}
+91
View File
@@ -0,0 +1,91 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class NotificationLevel {
/// Instantiate a new enum with the provided [value].
const NotificationLevel._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const success = NotificationLevel._(r'success');
static const error = NotificationLevel._(r'error');
static const warning = NotificationLevel._(r'warning');
static const info = NotificationLevel._(r'info');
/// List of all possible values in this [enum][NotificationLevel].
static const values = <NotificationLevel>[
success,
error,
warning,
info,
];
static NotificationLevel? fromJson(dynamic value) => NotificationLevelTypeTransformer().decode(value);
static List<NotificationLevel> listFromJson(dynamic json, {bool growable = false,}) {
final result = <NotificationLevel>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = NotificationLevel.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [NotificationLevel] to String,
/// and [decode] dynamic data back to [NotificationLevel].
class NotificationLevelTypeTransformer {
factory NotificationLevelTypeTransformer() => _instance ??= const NotificationLevelTypeTransformer._();
const NotificationLevelTypeTransformer._();
String encode(NotificationLevel data) => data.value;
/// Decodes a [dynamic value][data] to a NotificationLevel.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
NotificationLevel? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'success': return NotificationLevel.success;
case r'error': return NotificationLevel.error;
case r'warning': return NotificationLevel.warning;
case r'info': return NotificationLevel.info;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [NotificationLevelTypeTransformer] instance.
static NotificationLevelTypeTransformer? _instance;
}
+91
View File
@@ -0,0 +1,91 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class NotificationType {
/// Instantiate a new enum with the provided [value].
const NotificationType._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const jobFailed = NotificationType._(r'JobFailed');
static const backupFailed = NotificationType._(r'BackupFailed');
static const systemMessage = NotificationType._(r'SystemMessage');
static const custom = NotificationType._(r'Custom');
/// List of all possible values in this [enum][NotificationType].
static const values = <NotificationType>[
jobFailed,
backupFailed,
systemMessage,
custom,
];
static NotificationType? fromJson(dynamic value) => NotificationTypeTypeTransformer().decode(value);
static List<NotificationType> listFromJson(dynamic json, {bool growable = false,}) {
final result = <NotificationType>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = NotificationType.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [NotificationType] to String,
/// and [decode] dynamic data back to [NotificationType].
class NotificationTypeTypeTransformer {
factory NotificationTypeTypeTransformer() => _instance ??= const NotificationTypeTypeTransformer._();
const NotificationTypeTypeTransformer._();
String encode(NotificationType data) => data.value;
/// Decodes a [dynamic value][data] to a NotificationType.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
NotificationType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'JobFailed': return NotificationType.jobFailed;
case r'BackupFailed': return NotificationType.backupFailed;
case r'SystemMessage': return NotificationType.systemMessage;
case r'Custom': return NotificationType.custom;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [NotificationTypeTypeTransformer] instance.
static NotificationTypeTypeTransformer? _instance;
}
+112
View File
@@ -0,0 +1,112 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class NotificationUpdateAllDto {
/// Returns a new [NotificationUpdateAllDto] instance.
NotificationUpdateAllDto({
this.ids = const [],
this.readAt,
});
List<String> ids;
DateTime? readAt;
@override
bool operator ==(Object other) => identical(this, other) || other is NotificationUpdateAllDto &&
_deepEquality.equals(other.ids, ids) &&
other.readAt == readAt;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(ids.hashCode) +
(readAt == null ? 0 : readAt!.hashCode);
@override
String toString() => 'NotificationUpdateAllDto[ids=$ids, readAt=$readAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'ids'] = this.ids;
if (this.readAt != null) {
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
} else {
// json[r'readAt'] = null;
}
return json;
}
/// Returns a new [NotificationUpdateAllDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static NotificationUpdateAllDto? fromJson(dynamic value) {
upgradeDto(value, "NotificationUpdateAllDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return NotificationUpdateAllDto(
ids: json[r'ids'] is Iterable
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
: const [],
readAt: mapDateTime(json, r'readAt', r''),
);
}
return null;
}
static List<NotificationUpdateAllDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <NotificationUpdateAllDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = NotificationUpdateAllDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, NotificationUpdateAllDto> mapFromJson(dynamic json) {
final map = <String, NotificationUpdateAllDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = NotificationUpdateAllDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of NotificationUpdateAllDto-objects as value to a dart map
static Map<String, List<NotificationUpdateAllDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<NotificationUpdateAllDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = NotificationUpdateAllDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'ids',
};
}
@@ -10,52 +10,56 @@
part of openapi.api; part of openapi.api;
class AvatarResponse { class NotificationUpdateDto {
/// Returns a new [AvatarResponse] instance. /// Returns a new [NotificationUpdateDto] instance.
AvatarResponse({ NotificationUpdateDto({
required this.color, this.readAt,
}); });
UserAvatarColor color; DateTime? readAt;
@override @override
bool operator ==(Object other) => identical(this, other) || other is AvatarResponse && bool operator ==(Object other) => identical(this, other) || other is NotificationUpdateDto &&
other.color == color; other.readAt == readAt;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(color.hashCode); (readAt == null ? 0 : readAt!.hashCode);
@override @override
String toString() => 'AvatarResponse[color=$color]'; String toString() => 'NotificationUpdateDto[readAt=$readAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'color'] = this.color; if (this.readAt != null) {
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
} else {
// json[r'readAt'] = null;
}
return json; return json;
} }
/// Returns a new [AvatarResponse] instance and imports its values from /// Returns a new [NotificationUpdateDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise. /// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods // ignore: prefer_constructors_over_static_methods
static AvatarResponse? fromJson(dynamic value) { static NotificationUpdateDto? fromJson(dynamic value) {
upgradeDto(value, "AvatarResponse"); upgradeDto(value, "NotificationUpdateDto");
if (value is Map) { if (value is Map) {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return AvatarResponse( return NotificationUpdateDto(
color: UserAvatarColor.fromJson(json[r'color'])!, readAt: mapDateTime(json, r'readAt', r''),
); );
} }
return null; return null;
} }
static List<AvatarResponse> listFromJson(dynamic json, {bool growable = false,}) { static List<NotificationUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AvatarResponse>[]; final result = <NotificationUpdateDto>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
final value = AvatarResponse.fromJson(row); final value = NotificationUpdateDto.fromJson(row);
if (value != null) { if (value != null) {
result.add(value); result.add(value);
} }
@@ -64,12 +68,12 @@ class AvatarResponse {
return result.toList(growable: growable); return result.toList(growable: growable);
} }
static Map<String, AvatarResponse> mapFromJson(dynamic json) { static Map<String, NotificationUpdateDto> mapFromJson(dynamic json) {
final map = <String, AvatarResponse>{}; final map = <String, NotificationUpdateDto>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) { for (final entry in json.entries) {
final value = AvatarResponse.fromJson(entry.value); final value = NotificationUpdateDto.fromJson(entry.value);
if (value != null) { if (value != null) {
map[entry.key] = value; map[entry.key] = value;
} }
@@ -78,14 +82,14 @@ class AvatarResponse {
return map; return map;
} }
// maps a json object with a list of AvatarResponse-objects as value to a dart map // maps a json object with a list of NotificationUpdateDto-objects as value to a dart map
static Map<String, List<AvatarResponse>> mapListFromJson(dynamic json, {bool growable = false,}) { static Map<String, List<NotificationUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AvatarResponse>>{}; final map = <String, List<NotificationUpdateDto>>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments // ignore: parameter_assignments
json = json.cast<String, dynamic>(); json = json.cast<String, dynamic>();
for (final entry in json.entries) { for (final entry in json.entries) {
map[entry.key] = AvatarResponse.listFromJson(entry.value, growable: growable,); map[entry.key] = NotificationUpdateDto.listFromJson(entry.value, growable: growable,);
} }
} }
return map; return map;
@@ -93,7 +97,6 @@ class AvatarResponse {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'color',
}; };
} }
@@ -0,0 +1,85 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class OAuthTokenEndpointAuthMethod {
/// Instantiate a new enum with the provided [value].
const OAuthTokenEndpointAuthMethod._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const post = OAuthTokenEndpointAuthMethod._(r'client_secret_post');
static const basic = OAuthTokenEndpointAuthMethod._(r'client_secret_basic');
/// List of all possible values in this [enum][OAuthTokenEndpointAuthMethod].
static const values = <OAuthTokenEndpointAuthMethod>[
post,
basic,
];
static OAuthTokenEndpointAuthMethod? fromJson(dynamic value) => OAuthTokenEndpointAuthMethodTypeTransformer().decode(value);
static List<OAuthTokenEndpointAuthMethod> listFromJson(dynamic json, {bool growable = false,}) {
final result = <OAuthTokenEndpointAuthMethod>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = OAuthTokenEndpointAuthMethod.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [OAuthTokenEndpointAuthMethod] to String,
/// and [decode] dynamic data back to [OAuthTokenEndpointAuthMethod].
class OAuthTokenEndpointAuthMethodTypeTransformer {
factory OAuthTokenEndpointAuthMethodTypeTransformer() => _instance ??= const OAuthTokenEndpointAuthMethodTypeTransformer._();
const OAuthTokenEndpointAuthMethodTypeTransformer._();
String encode(OAuthTokenEndpointAuthMethod data) => data.value;
/// Decodes a [dynamic value][data] to a OAuthTokenEndpointAuthMethod.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
OAuthTokenEndpointAuthMethod? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'client_secret_post': return OAuthTokenEndpointAuthMethod.post;
case r'client_secret_basic': return OAuthTokenEndpointAuthMethod.basic;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [OAuthTokenEndpointAuthMethodTypeTransformer] instance.
static OAuthTokenEndpointAuthMethodTypeTransformer? _instance;
}
+12
View File
@@ -66,6 +66,10 @@ class Permission {
static const memoryPeriodRead = Permission._(r'memory.read'); static const memoryPeriodRead = Permission._(r'memory.read');
static const memoryPeriodUpdate = Permission._(r'memory.update'); static const memoryPeriodUpdate = Permission._(r'memory.update');
static const memoryPeriodDelete = Permission._(r'memory.delete'); static const memoryPeriodDelete = Permission._(r'memory.delete');
static const notificationPeriodCreate = Permission._(r'notification.create');
static const notificationPeriodRead = Permission._(r'notification.read');
static const notificationPeriodUpdate = Permission._(r'notification.update');
static const notificationPeriodDelete = Permission._(r'notification.delete');
static const partnerPeriodCreate = Permission._(r'partner.create'); static const partnerPeriodCreate = Permission._(r'partner.create');
static const partnerPeriodRead = Permission._(r'partner.read'); static const partnerPeriodRead = Permission._(r'partner.read');
static const partnerPeriodUpdate = Permission._(r'partner.update'); static const partnerPeriodUpdate = Permission._(r'partner.update');
@@ -147,6 +151,10 @@ class Permission {
memoryPeriodRead, memoryPeriodRead,
memoryPeriodUpdate, memoryPeriodUpdate,
memoryPeriodDelete, memoryPeriodDelete,
notificationPeriodCreate,
notificationPeriodRead,
notificationPeriodUpdate,
notificationPeriodDelete,
partnerPeriodCreate, partnerPeriodCreate,
partnerPeriodRead, partnerPeriodRead,
partnerPeriodUpdate, partnerPeriodUpdate,
@@ -263,6 +271,10 @@ class PermissionTypeTransformer {
case r'memory.read': return Permission.memoryPeriodRead; case r'memory.read': return Permission.memoryPeriodRead;
case r'memory.update': return Permission.memoryPeriodUpdate; case r'memory.update': return Permission.memoryPeriodUpdate;
case r'memory.delete': return Permission.memoryPeriodDelete; case r'memory.delete': return Permission.memoryPeriodDelete;
case r'notification.create': return Permission.notificationPeriodCreate;
case r'notification.read': return Permission.notificationPeriodRead;
case r'notification.update': return Permission.notificationPeriodUpdate;
case r'notification.delete': return Permission.notificationPeriodDelete;
case r'partner.create': return Permission.partnerPeriodCreate; case r'partner.create': return Permission.partnerPeriodCreate;
case r'partner.read': return Permission.partnerPeriodRead; case r'partner.read': return Permission.partnerPeriodRead;
case r'partner.update': return Permission.partnerPeriodUpdate; case r'partner.update': return Permission.partnerPeriodUpdate;
+20 -3
View File
@@ -28,6 +28,8 @@ class SystemConfigOAuthDto {
required this.signingAlgorithm, required this.signingAlgorithm,
required this.storageLabelClaim, required this.storageLabelClaim,
required this.storageQuotaClaim, required this.storageQuotaClaim,
required this.timeout,
required this.tokenEndpointAuthMethod,
}); });
bool autoLaunch; bool autoLaunch;
@@ -61,6 +63,11 @@ class SystemConfigOAuthDto {
String storageQuotaClaim; String storageQuotaClaim;
/// Minimum value: 1
int timeout;
OAuthTokenEndpointAuthMethod tokenEndpointAuthMethod;
@override @override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigOAuthDto && bool operator ==(Object other) => identical(this, other) || other is SystemConfigOAuthDto &&
other.autoLaunch == autoLaunch && other.autoLaunch == autoLaunch &&
@@ -77,7 +84,9 @@ class SystemConfigOAuthDto {
other.scope == scope && other.scope == scope &&
other.signingAlgorithm == signingAlgorithm && other.signingAlgorithm == signingAlgorithm &&
other.storageLabelClaim == storageLabelClaim && other.storageLabelClaim == storageLabelClaim &&
other.storageQuotaClaim == storageQuotaClaim; other.storageQuotaClaim == storageQuotaClaim &&
other.timeout == timeout &&
other.tokenEndpointAuthMethod == tokenEndpointAuthMethod;
@override @override
int get hashCode => int get hashCode =>
@@ -96,10 +105,12 @@ class SystemConfigOAuthDto {
(scope.hashCode) + (scope.hashCode) +
(signingAlgorithm.hashCode) + (signingAlgorithm.hashCode) +
(storageLabelClaim.hashCode) + (storageLabelClaim.hashCode) +
(storageQuotaClaim.hashCode); (storageQuotaClaim.hashCode) +
(timeout.hashCode) +
(tokenEndpointAuthMethod.hashCode);
@override @override
String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim]'; String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -118,6 +129,8 @@ class SystemConfigOAuthDto {
json[r'signingAlgorithm'] = this.signingAlgorithm; json[r'signingAlgorithm'] = this.signingAlgorithm;
json[r'storageLabelClaim'] = this.storageLabelClaim; json[r'storageLabelClaim'] = this.storageLabelClaim;
json[r'storageQuotaClaim'] = this.storageQuotaClaim; json[r'storageQuotaClaim'] = this.storageQuotaClaim;
json[r'timeout'] = this.timeout;
json[r'tokenEndpointAuthMethod'] = this.tokenEndpointAuthMethod;
return json; return json;
} }
@@ -145,6 +158,8 @@ class SystemConfigOAuthDto {
signingAlgorithm: mapValueOfType<String>(json, r'signingAlgorithm')!, signingAlgorithm: mapValueOfType<String>(json, r'signingAlgorithm')!,
storageLabelClaim: mapValueOfType<String>(json, r'storageLabelClaim')!, storageLabelClaim: mapValueOfType<String>(json, r'storageLabelClaim')!,
storageQuotaClaim: mapValueOfType<String>(json, r'storageQuotaClaim')!, storageQuotaClaim: mapValueOfType<String>(json, r'storageQuotaClaim')!,
timeout: mapValueOfType<int>(json, r'timeout')!,
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.fromJson(json[r'tokenEndpointAuthMethod'])!,
); );
} }
return null; return null;
@@ -207,6 +222,8 @@ class SystemConfigOAuthDto {
'signingAlgorithm', 'signingAlgorithm',
'storageLabelClaim', 'storageLabelClaim',
'storageQuotaClaim', 'storageQuotaClaim',
'timeout',
'tokenEndpointAuthMethod',
}; };
} }
+12 -1
View File
@@ -13,6 +13,7 @@ part of openapi.api;
class UserAdminCreateDto { class UserAdminCreateDto {
/// Returns a new [UserAdminCreateDto] instance. /// Returns a new [UserAdminCreateDto] instance.
UserAdminCreateDto({ UserAdminCreateDto({
this.avatarColor,
required this.email, required this.email,
required this.name, required this.name,
this.notify, this.notify,
@@ -22,6 +23,8 @@ class UserAdminCreateDto {
this.storageLabel, this.storageLabel,
}); });
UserAvatarColor? avatarColor;
String email; String email;
String name; String name;
@@ -51,6 +54,7 @@ class UserAdminCreateDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserAdminCreateDto && bool operator ==(Object other) => identical(this, other) || other is UserAdminCreateDto &&
other.avatarColor == avatarColor &&
other.email == email && other.email == email &&
other.name == name && other.name == name &&
other.notify == notify && other.notify == notify &&
@@ -62,6 +66,7 @@ class UserAdminCreateDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatarColor == null ? 0 : avatarColor!.hashCode) +
(email.hashCode) + (email.hashCode) +
(name.hashCode) + (name.hashCode) +
(notify == null ? 0 : notify!.hashCode) + (notify == null ? 0 : notify!.hashCode) +
@@ -71,10 +76,15 @@ class UserAdminCreateDto {
(storageLabel == null ? 0 : storageLabel!.hashCode); (storageLabel == null ? 0 : storageLabel!.hashCode);
@override @override
String toString() => 'UserAdminCreateDto[email=$email, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; String toString() => 'UserAdminCreateDto[avatarColor=$avatarColor, email=$email, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.avatarColor != null) {
json[r'avatarColor'] = this.avatarColor;
} else {
// json[r'avatarColor'] = null;
}
json[r'email'] = this.email; json[r'email'] = this.email;
json[r'name'] = this.name; json[r'name'] = this.name;
if (this.notify != null) { if (this.notify != null) {
@@ -110,6 +120,7 @@ class UserAdminCreateDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UserAdminCreateDto( return UserAdminCreateDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
email: mapValueOfType<String>(json, r'email')!, email: mapValueOfType<String>(json, r'email')!,
name: mapValueOfType<String>(json, r'name')!, name: mapValueOfType<String>(json, r'name')!,
notify: mapValueOfType<bool>(json, r'notify'), notify: mapValueOfType<bool>(json, r'notify'),
+12 -1
View File
@@ -13,6 +13,7 @@ part of openapi.api;
class UserAdminUpdateDto { class UserAdminUpdateDto {
/// Returns a new [UserAdminUpdateDto] instance. /// Returns a new [UserAdminUpdateDto] instance.
UserAdminUpdateDto({ UserAdminUpdateDto({
this.avatarColor,
this.email, this.email,
this.name, this.name,
this.password, this.password,
@@ -21,6 +22,8 @@ class UserAdminUpdateDto {
this.storageLabel, this.storageLabel,
}); });
UserAvatarColor? avatarColor;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated /// does not include a default value (using the "default:" property), however, the generated
@@ -60,6 +63,7 @@ class UserAdminUpdateDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserAdminUpdateDto && bool operator ==(Object other) => identical(this, other) || other is UserAdminUpdateDto &&
other.avatarColor == avatarColor &&
other.email == email && other.email == email &&
other.name == name && other.name == name &&
other.password == password && other.password == password &&
@@ -70,6 +74,7 @@ class UserAdminUpdateDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatarColor == null ? 0 : avatarColor!.hashCode) +
(email == null ? 0 : email!.hashCode) + (email == null ? 0 : email!.hashCode) +
(name == null ? 0 : name!.hashCode) + (name == null ? 0 : name!.hashCode) +
(password == null ? 0 : password!.hashCode) + (password == null ? 0 : password!.hashCode) +
@@ -78,10 +83,15 @@ class UserAdminUpdateDto {
(storageLabel == null ? 0 : storageLabel!.hashCode); (storageLabel == null ? 0 : storageLabel!.hashCode);
@override @override
String toString() => 'UserAdminUpdateDto[email=$email, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; String toString() => 'UserAdminUpdateDto[avatarColor=$avatarColor, email=$email, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.avatarColor != null) {
json[r'avatarColor'] = this.avatarColor;
} else {
// json[r'avatarColor'] = null;
}
if (this.email != null) { if (this.email != null) {
json[r'email'] = this.email; json[r'email'] = this.email;
} else { } else {
@@ -124,6 +134,7 @@ class UserAdminUpdateDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UserAdminUpdateDto( return UserAdminUpdateDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
email: mapValueOfType<String>(json, r'email'), email: mapValueOfType<String>(json, r'email'),
name: mapValueOfType<String>(json, r'name'), name: mapValueOfType<String>(json, r'name'),
password: mapValueOfType<String>(json, r'password'), password: mapValueOfType<String>(json, r'password'),
+1 -9
View File
@@ -13,7 +13,6 @@ part of openapi.api;
class UserPreferencesResponseDto { class UserPreferencesResponseDto {
/// Returns a new [UserPreferencesResponseDto] instance. /// Returns a new [UserPreferencesResponseDto] instance.
UserPreferencesResponseDto({ UserPreferencesResponseDto({
required this.avatar,
required this.download, required this.download,
required this.emailNotifications, required this.emailNotifications,
required this.folders, required this.folders,
@@ -25,8 +24,6 @@ class UserPreferencesResponseDto {
required this.tags, required this.tags,
}); });
AvatarResponse avatar;
DownloadResponse download; DownloadResponse download;
EmailNotificationsResponse emailNotifications; EmailNotificationsResponse emailNotifications;
@@ -47,7 +44,6 @@ class UserPreferencesResponseDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto && bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto &&
other.avatar == avatar &&
other.download == download && other.download == download &&
other.emailNotifications == emailNotifications && other.emailNotifications == emailNotifications &&
other.folders == folders && other.folders == folders &&
@@ -61,7 +57,6 @@ class UserPreferencesResponseDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatar.hashCode) +
(download.hashCode) + (download.hashCode) +
(emailNotifications.hashCode) + (emailNotifications.hashCode) +
(folders.hashCode) + (folders.hashCode) +
@@ -73,11 +68,10 @@ class UserPreferencesResponseDto {
(tags.hashCode); (tags.hashCode);
@override @override
String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; String toString() => 'UserPreferencesResponseDto[download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'avatar'] = this.avatar;
json[r'download'] = this.download; json[r'download'] = this.download;
json[r'emailNotifications'] = this.emailNotifications; json[r'emailNotifications'] = this.emailNotifications;
json[r'folders'] = this.folders; json[r'folders'] = this.folders;
@@ -99,7 +93,6 @@ class UserPreferencesResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UserPreferencesResponseDto( return UserPreferencesResponseDto(
avatar: AvatarResponse.fromJson(json[r'avatar'])!,
download: DownloadResponse.fromJson(json[r'download'])!, download: DownloadResponse.fromJson(json[r'download'])!,
emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!, emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!,
folders: FoldersResponse.fromJson(json[r'folders'])!, folders: FoldersResponse.fromJson(json[r'folders'])!,
@@ -156,7 +149,6 @@ class UserPreferencesResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'avatar',
'download', 'download',
'emailNotifications', 'emailNotifications',
'folders', 'folders',
+12 -1
View File
@@ -13,11 +13,14 @@ part of openapi.api;
class UserUpdateMeDto { class UserUpdateMeDto {
/// Returns a new [UserUpdateMeDto] instance. /// Returns a new [UserUpdateMeDto] instance.
UserUpdateMeDto({ UserUpdateMeDto({
this.avatarColor,
this.email, this.email,
this.name, this.name,
this.password, this.password,
}); });
UserAvatarColor? avatarColor;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated /// does not include a default value (using the "default:" property), however, the generated
@@ -44,6 +47,7 @@ class UserUpdateMeDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is UserUpdateMeDto && bool operator ==(Object other) => identical(this, other) || other is UserUpdateMeDto &&
other.avatarColor == avatarColor &&
other.email == email && other.email == email &&
other.name == name && other.name == name &&
other.password == password; other.password == password;
@@ -51,15 +55,21 @@ class UserUpdateMeDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(avatarColor == null ? 0 : avatarColor!.hashCode) +
(email == null ? 0 : email!.hashCode) + (email == null ? 0 : email!.hashCode) +
(name == null ? 0 : name!.hashCode) + (name == null ? 0 : name!.hashCode) +
(password == null ? 0 : password!.hashCode); (password == null ? 0 : password!.hashCode);
@override @override
String toString() => 'UserUpdateMeDto[email=$email, name=$name, password=$password]'; String toString() => 'UserUpdateMeDto[avatarColor=$avatarColor, email=$email, name=$name, password=$password]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.avatarColor != null) {
json[r'avatarColor'] = this.avatarColor;
} else {
// json[r'avatarColor'] = null;
}
if (this.email != null) { if (this.email != null) {
json[r'email'] = this.email; json[r'email'] = this.email;
} else { } else {
@@ -87,6 +97,7 @@ class UserUpdateMeDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UserUpdateMeDto( return UserUpdateMeDto(
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
email: mapValueOfType<String>(json, r'email'), email: mapValueOfType<String>(json, r'email'),
name: mapValueOfType<String>(json, r'name'), name: mapValueOfType<String>(json, r'name'),
password: mapValueOfType<String>(json, r'password'), password: mapValueOfType<String>(json, r'password'),
+1 -1
View File
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none' publish_to: 'none'
version: 1.132.0+194 version: 1.132.3+197
environment: environment:
sdk: '>=3.3.0 <4.0.0' sdk: '>=3.3.0 <4.0.0'
+552 -70
View File
@@ -206,6 +206,141 @@
] ]
} }
}, },
"/admin/notifications": {
"post": {
"operationId": "createNotification",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotificationCreateDto"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotificationDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Notifications (Admin)"
]
}
},
"/admin/notifications/templates/{name}": {
"post": {
"operationId": "getNotificationTemplateAdmin",
"parameters": [
{
"name": "name",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TemplateDto"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TemplateResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Notifications (Admin)"
]
}
},
"/admin/notifications/test-email": {
"post": {
"operationId": "sendTestEmailAdmin",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SystemConfigSmtpDto"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestEmailResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Notifications (Admin)"
]
}
},
"/admin/users": { "/admin/users": {
"get": { "get": {
"operationId": "searchUsersAdmin", "operationId": "searchUsersAdmin",
@@ -3485,15 +3620,224 @@
] ]
} }
}, },
"/notifications/admin/templates/{name}": { "/notifications": {
"post": { "delete": {
"operationId": "getNotificationTemplateAdmin", "operationId": "deleteNotifications",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotificationDeleteAllDto"
}
}
},
"required": true
},
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Notifications"
]
},
"get": {
"operationId": "getNotifications",
"parameters": [ "parameters": [
{ {
"name": "name", "name": "id",
"required": false,
"in": "query",
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"name": "level",
"required": false,
"in": "query",
"schema": {
"$ref": "#/components/schemas/NotificationLevel"
}
},
{
"name": "type",
"required": false,
"in": "query",
"schema": {
"$ref": "#/components/schemas/NotificationType"
}
},
{
"name": "unread",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/NotificationDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Notifications"
]
},
"put": {
"operationId": "updateNotifications",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotificationUpdateAllDto"
}
}
},
"required": true
},
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Notifications"
]
}
},
"/notifications/{id}": {
"delete": {
"operationId": "deleteNotification",
"parameters": [
{
"name": "id",
"required": true, "required": true,
"in": "path", "in": "path",
"schema": { "schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Notifications"
]
},
"get": {
"operationId": "getNotification",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NotificationDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Notifications"
]
},
"put": {
"operationId": "updateNotification",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string" "type": "string"
} }
} }
@@ -3502,7 +3846,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/TemplateDto" "$ref": "#/components/schemas/NotificationUpdateDto"
} }
} }
}, },
@@ -3513,7 +3857,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/TemplateResponseDto" "$ref": "#/components/schemas/NotificationDto"
} }
} }
}, },
@@ -3532,49 +3876,7 @@
} }
], ],
"tags": [ "tags": [
"Notifications (Admin)" "Notifications"
]
}
},
"/notifications/admin/test-email": {
"post": {
"operationId": "sendTestEmailAdmin",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SystemConfigSmtpDto"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestEmailResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Notifications (Admin)"
] ]
} }
}, },
@@ -7656,7 +7958,7 @@
"info": { "info": {
"title": "Immich", "title": "Immich",
"description": "Immich API", "description": "Immich API",
"version": "1.132.0", "version": "1.132.3",
"contact": {} "contact": {}
}, },
"tags": [], "tags": [],
@@ -8884,21 +9186,6 @@
], ],
"type": "string" "type": "string"
}, },
"AvatarResponse": {
"properties": {
"color": {
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
]
}
},
"required": [
"color"
],
"type": "object"
},
"AvatarUpdate": { "AvatarUpdate": {
"properties": { "properties": {
"color": { "color": {
@@ -10341,6 +10628,157 @@
}, },
"type": "object" "type": "object"
}, },
"NotificationCreateDto": {
"properties": {
"data": {
"type": "object"
},
"description": {
"nullable": true,
"type": "string"
},
"level": {
"allOf": [
{
"$ref": "#/components/schemas/NotificationLevel"
}
]
},
"readAt": {
"format": "date-time",
"nullable": true,
"type": "string"
},
"title": {
"type": "string"
},
"type": {
"allOf": [
{
"$ref": "#/components/schemas/NotificationType"
}
]
},
"userId": {
"format": "uuid",
"type": "string"
}
},
"required": [
"title",
"userId"
],
"type": "object"
},
"NotificationDeleteAllDto": {
"properties": {
"ids": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
}
},
"required": [
"ids"
],
"type": "object"
},
"NotificationDto": {
"properties": {
"createdAt": {
"format": "date-time",
"type": "string"
},
"data": {
"type": "object"
},
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"level": {
"allOf": [
{
"$ref": "#/components/schemas/NotificationLevel"
}
]
},
"readAt": {
"format": "date-time",
"type": "string"
},
"title": {
"type": "string"
},
"type": {
"allOf": [
{
"$ref": "#/components/schemas/NotificationType"
}
]
}
},
"required": [
"createdAt",
"id",
"level",
"title",
"type"
],
"type": "object"
},
"NotificationLevel": {
"enum": [
"success",
"error",
"warning",
"info"
],
"type": "string"
},
"NotificationType": {
"enum": [
"JobFailed",
"BackupFailed",
"SystemMessage",
"Custom"
],
"type": "string"
},
"NotificationUpdateAllDto": {
"properties": {
"ids": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
},
"readAt": {
"format": "date-time",
"nullable": true,
"type": "string"
}
},
"required": [
"ids"
],
"type": "object"
},
"NotificationUpdateDto": {
"properties": {
"readAt": {
"format": "date-time",
"nullable": true,
"type": "string"
}
},
"type": "object"
},
"OAuthAuthorizeResponseDto": { "OAuthAuthorizeResponseDto": {
"properties": { "properties": {
"url": { "url": {
@@ -10386,6 +10824,13 @@
], ],
"type": "object" "type": "object"
}, },
"OAuthTokenEndpointAuthMethod": {
"enum": [
"client_secret_post",
"client_secret_basic"
],
"type": "string"
},
"OnThisDayDto": { "OnThisDayDto": {
"properties": { "properties": {
"year": { "year": {
@@ -10615,6 +11060,10 @@
"memory.read", "memory.read",
"memory.update", "memory.update",
"memory.delete", "memory.delete",
"notification.create",
"notification.read",
"notification.update",
"notification.delete",
"partner.create", "partner.create",
"partner.read", "partner.read",
"partner.update", "partner.update",
@@ -12962,6 +13411,17 @@
}, },
"storageQuotaClaim": { "storageQuotaClaim": {
"type": "string" "type": "string"
},
"timeout": {
"minimum": 1,
"type": "integer"
},
"tokenEndpointAuthMethod": {
"allOf": [
{
"$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod"
}
]
} }
}, },
"required": [ "required": [
@@ -12979,7 +13439,9 @@
"scope", "scope",
"signingAlgorithm", "signingAlgorithm",
"storageLabelClaim", "storageLabelClaim",
"storageQuotaClaim" "storageQuotaClaim",
"timeout",
"tokenEndpointAuthMethod"
], ],
"type": "object" "type": "object"
}, },
@@ -13621,6 +14083,14 @@
}, },
"UserAdminCreateDto": { "UserAdminCreateDto": {
"properties": { "properties": {
"avatarColor": {
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
],
"nullable": true
},
"email": { "email": {
"format": "email", "format": "email",
"type": "string" "type": "string"
@@ -13763,6 +14233,14 @@
}, },
"UserAdminUpdateDto": { "UserAdminUpdateDto": {
"properties": { "properties": {
"avatarColor": {
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
],
"nullable": true
},
"email": { "email": {
"format": "email", "format": "email",
"type": "string" "type": "string"
@@ -13826,9 +14304,6 @@
}, },
"UserPreferencesResponseDto": { "UserPreferencesResponseDto": {
"properties": { "properties": {
"avatar": {
"$ref": "#/components/schemas/AvatarResponse"
},
"download": { "download": {
"$ref": "#/components/schemas/DownloadResponse" "$ref": "#/components/schemas/DownloadResponse"
}, },
@@ -13858,7 +14333,6 @@
} }
}, },
"required": [ "required": [
"avatar",
"download", "download",
"emailNotifications", "emailNotifications",
"folders", "folders",
@@ -13952,6 +14426,14 @@
}, },
"UserUpdateMeDto": { "UserUpdateMeDto": {
"properties": { "properties": {
"avatarColor": {
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
],
"nullable": true
},
"email": { "email": {
"format": "email", "format": "email",
"type": "string" "type": "string"
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.132.0", "version": "1.132.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.132.0", "version": "1.132.3",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.132.0", "version": "1.132.3",
"description": "Auto-generated TypeScript SDK for the Immich API", "description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module", "type": "module",
"main": "./build/index.js", "main": "./build/index.js",
+169 -40
View File
@@ -1,6 +1,6 @@
/** /**
* Immich * Immich
* 1.132.0 * 1.132.3
* DO NOT MODIFY - This file has been generated using oazapfts. * DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts * See https://www.npmjs.com/package/oazapfts
*/ */
@@ -39,6 +39,48 @@ export type ActivityCreateDto = {
export type ActivityStatisticsResponseDto = { export type ActivityStatisticsResponseDto = {
comments: number; comments: number;
}; };
export type NotificationCreateDto = {
data?: object;
description?: string | null;
level?: NotificationLevel;
readAt?: string | null;
title: string;
"type"?: NotificationType;
userId: string;
};
export type NotificationDto = {
createdAt: string;
data?: object;
description?: string;
id: string;
level: NotificationLevel;
readAt?: string;
title: string;
"type": NotificationType;
};
export type TemplateDto = {
template: string;
};
export type TemplateResponseDto = {
html: string;
name: string;
};
export type SystemConfigSmtpTransportDto = {
host: string;
ignoreCert: boolean;
password: string;
port: number;
username: string;
};
export type SystemConfigSmtpDto = {
enabled: boolean;
"from": string;
replyTo: string;
transport: SystemConfigSmtpTransportDto;
};
export type TestEmailResponseDto = {
messageId: string;
};
export type UserLicense = { export type UserLicense = {
activatedAt: string; activatedAt: string;
activationKey: string; activationKey: string;
@@ -64,6 +106,7 @@ export type UserAdminResponseDto = {
updatedAt: string; updatedAt: string;
}; };
export type UserAdminCreateDto = { export type UserAdminCreateDto = {
avatarColor?: (UserAvatarColor) | null;
email: string; email: string;
name: string; name: string;
notify?: boolean; notify?: boolean;
@@ -76,6 +119,7 @@ export type UserAdminDeleteDto = {
force?: boolean; force?: boolean;
}; };
export type UserAdminUpdateDto = { export type UserAdminUpdateDto = {
avatarColor?: (UserAvatarColor) | null;
email?: string; email?: string;
name?: string; name?: string;
password?: string; password?: string;
@@ -83,9 +127,6 @@ export type UserAdminUpdateDto = {
shouldChangePassword?: boolean; shouldChangePassword?: boolean;
storageLabel?: string | null; storageLabel?: string | null;
}; };
export type AvatarResponse = {
color: UserAvatarColor;
};
export type DownloadResponse = { export type DownloadResponse = {
archiveSize: number; archiveSize: number;
includeEmbeddedVideos: boolean; includeEmbeddedVideos: boolean;
@@ -122,7 +163,6 @@ export type TagsResponse = {
sidebarWeb: boolean; sidebarWeb: boolean;
}; };
export type UserPreferencesResponseDto = { export type UserPreferencesResponseDto = {
avatar: AvatarResponse;
download: DownloadResponse; download: DownloadResponse;
emailNotifications: EmailNotificationsResponse; emailNotifications: EmailNotificationsResponse;
folders: FoldersResponse; folders: FoldersResponse;
@@ -663,28 +703,15 @@ export type MemoryUpdateDto = {
memoryAt?: string; memoryAt?: string;
seenAt?: string; seenAt?: string;
}; };
export type TemplateDto = { export type NotificationDeleteAllDto = {
template: string; ids: string[];
}; };
export type TemplateResponseDto = { export type NotificationUpdateAllDto = {
html: string; ids: string[];
name: string; readAt?: string | null;
}; };
export type SystemConfigSmtpTransportDto = { export type NotificationUpdateDto = {
host: string; readAt?: string | null;
ignoreCert: boolean;
password: string;
port: number;
username: string;
};
export type SystemConfigSmtpDto = {
enabled: boolean;
"from": string;
replyTo: string;
transport: SystemConfigSmtpTransportDto;
};
export type TestEmailResponseDto = {
messageId: string;
}; };
export type OAuthConfigDto = { export type OAuthConfigDto = {
codeChallenge?: string; codeChallenge?: string;
@@ -1288,6 +1315,8 @@ export type SystemConfigOAuthDto = {
signingAlgorithm: string; signingAlgorithm: string;
storageLabelClaim: string; storageLabelClaim: string;
storageQuotaClaim: string; storageQuotaClaim: string;
timeout: number;
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod;
}; };
export type SystemConfigPasswordLoginDto = { export type SystemConfigPasswordLoginDto = {
enabled: boolean; enabled: boolean;
@@ -1388,6 +1417,7 @@ export type TrashResponseDto = {
count: number; count: number;
}; };
export type UserUpdateMeDto = { export type UserUpdateMeDto = {
avatarColor?: (UserAvatarColor) | null;
email?: string; email?: string;
name?: string; name?: string;
password?: string; password?: string;
@@ -1454,6 +1484,43 @@ export function deleteActivity({ id }: {
method: "DELETE" method: "DELETE"
})); }));
} }
export function createNotification({ notificationCreateDto }: {
notificationCreateDto: NotificationCreateDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 201;
data: NotificationDto;
}>("/admin/notifications", oazapfts.json({
...opts,
method: "POST",
body: notificationCreateDto
})));
}
export function getNotificationTemplateAdmin({ name, templateDto }: {
name: string;
templateDto: TemplateDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: TemplateResponseDto;
}>(`/admin/notifications/templates/${encodeURIComponent(name)}`, oazapfts.json({
...opts,
method: "POST",
body: templateDto
})));
}
export function sendTestEmailAdmin({ systemConfigSmtpDto }: {
systemConfigSmtpDto: SystemConfigSmtpDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: TestEmailResponseDto;
}>("/admin/notifications/test-email", oazapfts.json({
...opts,
method: "POST",
body: systemConfigSmtpDto
})));
}
export function searchUsersAdmin({ withDeleted }: { export function searchUsersAdmin({ withDeleted }: {
withDeleted?: boolean; withDeleted?: boolean;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
@@ -2322,29 +2389,71 @@ export function addMemoryAssets({ id, bulkIdsDto }: {
body: bulkIdsDto body: bulkIdsDto
}))); })));
} }
export function getNotificationTemplateAdmin({ name, templateDto }: { export function deleteNotifications({ notificationDeleteAllDto }: {
name: string; notificationDeleteAllDto: NotificationDeleteAllDto;
templateDto: TemplateDto;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchText("/notifications", oazapfts.json({
status: 200;
data: TemplateResponseDto;
}>(`/notifications/admin/templates/${encodeURIComponent(name)}`, oazapfts.json({
...opts, ...opts,
method: "POST", method: "DELETE",
body: templateDto body: notificationDeleteAllDto
}))); })));
} }
export function sendTestEmailAdmin({ systemConfigSmtpDto }: { export function getNotifications({ id, level, $type, unread }: {
systemConfigSmtpDto: SystemConfigSmtpDto; id?: string;
level?: NotificationLevel;
$type?: NotificationType;
unread?: boolean;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
data: TestEmailResponseDto; data: NotificationDto[];
}>("/notifications/admin/test-email", oazapfts.json({ }>(`/notifications${QS.query(QS.explode({
id,
level,
"type": $type,
unread
}))}`, {
...opts
}));
}
export function updateNotifications({ notificationUpdateAllDto }: {
notificationUpdateAllDto: NotificationUpdateAllDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText("/notifications", oazapfts.json({
...opts, ...opts,
method: "POST", method: "PUT",
body: systemConfigSmtpDto body: notificationUpdateAllDto
})));
}
export function deleteNotification({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/notifications/${encodeURIComponent(id)}`, {
...opts,
method: "DELETE"
}));
}
export function getNotification({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: NotificationDto;
}>(`/notifications/${encodeURIComponent(id)}`, {
...opts
}));
}
export function updateNotification({ id, notificationUpdateDto }: {
id: string;
notificationUpdateDto: NotificationUpdateDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: NotificationDto;
}>(`/notifications/${encodeURIComponent(id)}`, oazapfts.json({
...opts,
method: "PUT",
body: notificationUpdateDto
}))); })));
} }
export function startOAuth({ oAuthConfigDto }: { export function startOAuth({ oAuthConfigDto }: {
@@ -3453,6 +3562,18 @@ export enum UserAvatarColor {
Gray = "gray", Gray = "gray",
Amber = "amber" Amber = "amber"
} }
export enum NotificationLevel {
Success = "success",
Error = "error",
Warning = "warning",
Info = "info"
}
export enum NotificationType {
JobFailed = "JobFailed",
BackupFailed = "BackupFailed",
SystemMessage = "SystemMessage",
Custom = "Custom"
}
export enum UserStatus { export enum UserStatus {
Active = "active", Active = "active",
Removing = "removing", Removing = "removing",
@@ -3527,6 +3648,10 @@ export enum Permission {
MemoryRead = "memory.read", MemoryRead = "memory.read",
MemoryUpdate = "memory.update", MemoryUpdate = "memory.update",
MemoryDelete = "memory.delete", MemoryDelete = "memory.delete",
NotificationCreate = "notification.create",
NotificationRead = "notification.read",
NotificationUpdate = "notification.update",
NotificationDelete = "notification.delete",
PartnerCreate = "partner.create", PartnerCreate = "partner.create",
PartnerRead = "partner.read", PartnerRead = "partner.read",
PartnerUpdate = "partner.update", PartnerUpdate = "partner.update",
@@ -3736,6 +3861,10 @@ export enum LogLevel {
Error = "error", Error = "error",
Fatal = "fatal" Fatal = "fatal"
} }
export enum OAuthTokenEndpointAuthMethod {
ClientSecretPost = "client_secret_post",
ClientSecretBasic = "client_secret_basic"
}
export enum TimeBucketSize { export enum TimeBucketSize {
Day = "DAY", Day = "DAY",
Month = "MONTH" Month = "MONTH"
+10 -10
View File
@@ -6,14 +6,14 @@ WORKDIR /usr/src/app
COPY server/package.json server/package-lock.json ./ COPY server/package.json server/package-lock.json ./
COPY server/patches ./patches COPY server/patches ./patches
RUN npm ci && \ RUN npm ci && \
# exiftool-vendored.pl, sharp-linux-x64 and sharp-linux-arm64 are the only ones we need # exiftool-vendored.pl, sharp-linux-x64 and sharp-linux-arm64 are the only ones we need
# they're marked as optional dependencies, so we need to copy them manually after pruning # they're marked as optional dependencies, so we need to copy them manually after pruning
rm -rf node_modules/@img/sharp-libvips* && \ rm -rf node_modules/@img/sharp-libvips* && \
rm -rf node_modules/@img/sharp-linuxmusl-x64 rm -rf node_modules/@img/sharp-linuxmusl-x64
ENV PATH="${PATH}:/usr/src/app/bin" \ ENV PATH="${PATH}:/usr/src/app/bin" \
IMMICH_ENV=development \ IMMICH_ENV=development \
NVIDIA_DRIVER_CAPABILITIES=all \ NVIDIA_DRIVER_CAPABILITIES=all \
NVIDIA_VISIBLE_DEVICES=all NVIDIA_VISIBLE_DEVICES=all
ENTRYPOINT ["tini", "--", "/bin/sh"] ENTRYPOINT ["tini", "--", "/bin/sh"]
@@ -26,7 +26,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
# web build # web build
FROM node:22.14.0-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS web FROM node:22.15.0-alpine3.20@sha256:686b8892b69879ef5bfd6047589666933508f9a5451c67320df3070ba0e9807b AS web
WORKDIR /usr/src/open-api/typescript-sdk WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
@@ -47,8 +47,8 @@ FROM ghcr.io/immich-app/base-server-prod:202504081114@sha256:8353bcbdb4e6579300a
WORKDIR /usr/src/app WORKDIR /usr/src/app
ENV NODE_ENV=production \ ENV NODE_ENV=production \
NVIDIA_DRIVER_CAPABILITIES=all \ NVIDIA_DRIVER_CAPABILITIES=all \
NVIDIA_VISIBLE_DEVICES=all NVIDIA_VISIBLE_DEVICES=all
COPY --from=prod /usr/src/app/node_modules ./node_modules COPY --from=prod /usr/src/app/node_modules ./node_modules
COPY --from=prod /usr/src/app/dist ./dist COPY --from=prod /usr/src/app/dist ./dist
COPY --from=prod /usr/src/app/bin ./bin COPY --from=prod /usr/src/app/bin ./bin
+1642 -263
View File
File diff suppressed because it is too large Load Diff
+9 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich", "name": "immich",
"version": "1.132.0", "version": "1.132.3",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
@@ -57,6 +57,7 @@
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"compression": "^1.8.0",
"cookie": "^1.0.2", "cookie": "^1.0.2",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"exiftool-vendored": "^28.3.1", "exiftool-vendored": "^28.3.1",
@@ -88,7 +89,7 @@
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"sanitize-html": "^2.14.0", "sanitize-html": "^2.14.0",
"semver": "^7.6.2", "semver": "^7.6.2",
"sharp": "^0.33.5", "sharp": "^0.34.0",
"sirv": "^3.0.0", "sirv": "^3.0.0",
"tailwindcss-preset-email": "^1.3.2", "tailwindcss-preset-email": "^1.3.2",
"thumbhash": "^0.1.1", "thumbhash": "^0.1.1",
@@ -108,6 +109,7 @@
"@types/archiver": "^6.0.0", "@types/archiver": "^6.0.0",
"@types/async-lock": "^1.4.2", "@types/async-lock": "^1.4.2",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/compression": "^1.7.5",
"@types/cookie-parser": "^1.4.8", "@types/cookie-parser": "^1.4.8",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/fluent-ffmpeg": "^2.1.21", "@types/fluent-ffmpeg": "^2.1.21",
@@ -132,7 +134,8 @@
"globals": "^16.0.0", "globals": "^16.0.0",
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"node-addon-api": "^8.3.0", "node-addon-api": "^8.3.1",
"node-gyp": "^11.2.0",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"prettier": "^3.0.2", "prettier": "^3.0.2",
@@ -152,5 +155,8 @@
}, },
"volta": { "volta": {
"node": "22.14.0" "node": "22.14.0"
},
"overrides": {
"sharp": "^0.34.0"
} }
} }
+1 -1
View File
@@ -44,7 +44,7 @@ const imports = [
BullModule.registerQueue(...bull.queues), BullModule.registerQueue(...bull.queues),
ClsModule.forRoot(cls.config), ClsModule.forRoot(cls.config),
OpenTelemetryModule.forRoot(otel), OpenTelemetryModule.forRoot(otel),
KyselyModule.forRoot(getKyselyConfig(database.config.kysely)), KyselyModule.forRoot(getKyselyConfig(database.config)),
]; ];
class BaseModule implements OnModuleInit, OnModuleDestroy { class BaseModule implements OnModuleInit, OnModuleDestroy {
+3 -3
View File
@@ -10,7 +10,7 @@ import { DatabaseRepository } from 'src/repositories/database.repository';
import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoggingRepository } from 'src/repositories/logging.repository';
import 'src/schema'; import 'src/schema';
import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools'; import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools';
import { getKyselyConfig } from 'src/utils/database'; import { asPostgresConnectionConfig, getKyselyConfig } from 'src/utils/database';
const main = async () => { const main = async () => {
const command = process.argv[2]; const command = process.argv[2];
@@ -56,7 +56,7 @@ const main = async () => {
const getDatabaseClient = () => { const getDatabaseClient = () => {
const configRepository = new ConfigRepository(); const configRepository = new ConfigRepository();
const { database } = configRepository.getEnv(); const { database } = configRepository.getEnv();
return new Kysely<any>(getKyselyConfig(database.config.kysely)); return new Kysely<any>(getKyselyConfig(database.config));
}; };
const runQuery = async (query: string) => { const runQuery = async (query: string) => {
@@ -105,7 +105,7 @@ const create = (path: string, up: string[], down: string[]) => {
const compare = async () => { const compare = async () => {
const configRepository = new ConfigRepository(); const configRepository = new ConfigRepository();
const { database } = configRepository.getEnv(); const { database } = configRepository.getEnv();
const db = postgres(database.config.kysely); const db = postgres(asPostgresConnectionConfig(database.config));
const source = schemaFromCode(); const source = schemaFromCode();
const target = await schemaFromDatabase(db, {}); const target = await schemaFromDatabase(db, {});
+1 -1
View File
@@ -78,7 +78,7 @@ class SqlGenerator {
const moduleFixture = await Test.createTestingModule({ const moduleFixture = await Test.createTestingModule({
imports: [ imports: [
KyselyModule.forRoot({ KyselyModule.forRoot({
...getKyselyConfig(database.config.kysely), ...getKyselyConfig(database.config),
log: (event) => { log: (event) => {
if (event.level === 'query') { if (event.level === 'query') {
this.sqlLogger.logQuery(event.query.sql); this.sqlLogger.logQuery(event.query.sql);
+5
View File
@@ -5,6 +5,7 @@ import {
CQMode, CQMode,
ImageFormat, ImageFormat,
LogLevel, LogLevel,
OAuthTokenEndpointAuthMethod,
QueueName, QueueName,
ToneMapping, ToneMapping,
TranscodeHWAccel, TranscodeHWAccel,
@@ -96,6 +97,8 @@ export interface SystemConfig {
scope: string; scope: string;
signingAlgorithm: string; signingAlgorithm: string;
profileSigningAlgorithm: string; profileSigningAlgorithm: string;
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod;
timeout: number;
storageLabelClaim: string; storageLabelClaim: string;
storageQuotaClaim: string; storageQuotaClaim: string;
}; };
@@ -260,6 +263,8 @@ export const defaults = Object.freeze<SystemConfig>({
profileSigningAlgorithm: 'none', profileSigningAlgorithm: 'none',
storageLabelClaim: 'preferred_username', storageLabelClaim: 'preferred_username',
storageQuotaClaim: 'immich_quota', storageQuotaClaim: 'immich_quota',
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST,
timeout: 30_000,
}, },
passwordLogin: { passwordLogin: {
enabled: true, enabled: true,
+2
View File
@@ -14,6 +14,7 @@ import { LibraryController } from 'src/controllers/library.controller';
import { MapController } from 'src/controllers/map.controller'; import { MapController } from 'src/controllers/map.controller';
import { MemoryController } from 'src/controllers/memory.controller'; import { MemoryController } from 'src/controllers/memory.controller';
import { NotificationAdminController } from 'src/controllers/notification-admin.controller'; import { NotificationAdminController } from 'src/controllers/notification-admin.controller';
import { NotificationController } from 'src/controllers/notification.controller';
import { OAuthController } from 'src/controllers/oauth.controller'; import { OAuthController } from 'src/controllers/oauth.controller';
import { PartnerController } from 'src/controllers/partner.controller'; import { PartnerController } from 'src/controllers/partner.controller';
import { PersonController } from 'src/controllers/person.controller'; import { PersonController } from 'src/controllers/person.controller';
@@ -47,6 +48,7 @@ export const controllers = [
LibraryController, LibraryController,
MapController, MapController,
MemoryController, MemoryController,
NotificationController,
NotificationAdminController, NotificationAdminController,
OAuthController, OAuthController,
PartnerController, PartnerController,
@@ -1,16 +1,28 @@
import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/common'; import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { TemplateDto, TemplateResponseDto, TestEmailResponseDto } from 'src/dtos/notification.dto'; import {
NotificationCreateDto,
NotificationDto,
TemplateDto,
TemplateResponseDto,
TestEmailResponseDto,
} from 'src/dtos/notification.dto';
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { EmailTemplate } from 'src/repositories/email.repository'; import { EmailTemplate } from 'src/repositories/email.repository';
import { NotificationService } from 'src/services/notification.service'; import { NotificationAdminService } from 'src/services/notification-admin.service';
@ApiTags('Notifications (Admin)') @ApiTags('Notifications (Admin)')
@Controller('notifications/admin') @Controller('admin/notifications')
export class NotificationAdminController { export class NotificationAdminController {
constructor(private service: NotificationService) {} constructor(private service: NotificationAdminService) {}
@Post()
@Authenticated({ admin: true })
createNotification(@Auth() auth: AuthDto, @Body() dto: NotificationCreateDto): Promise<NotificationDto> {
return this.service.create(auth, dto);
}
@Post('test-email') @Post('test-email')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@@ -0,0 +1,60 @@
import { Body, Controller, Delete, Get, Param, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthDto } from 'src/dtos/auth.dto';
import {
NotificationDeleteAllDto,
NotificationDto,
NotificationSearchDto,
NotificationUpdateAllDto,
NotificationUpdateDto,
} from 'src/dtos/notification.dto';
import { Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { NotificationService } from 'src/services/notification.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Notifications')
@Controller('notifications')
export class NotificationController {
constructor(private service: NotificationService) {}
@Get()
@Authenticated({ permission: Permission.NOTIFICATION_READ })
getNotifications(@Auth() auth: AuthDto, @Query() dto: NotificationSearchDto): Promise<NotificationDto[]> {
return this.service.search(auth, dto);
}
@Put()
@Authenticated({ permission: Permission.NOTIFICATION_UPDATE })
updateNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationUpdateAllDto): Promise<void> {
return this.service.updateAll(auth, dto);
}
@Delete()
@Authenticated({ permission: Permission.NOTIFICATION_DELETE })
deleteNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationDeleteAllDto): Promise<void> {
return this.service.deleteAll(auth, dto);
}
@Get(':id')
@Authenticated({ permission: Permission.NOTIFICATION_READ })
getNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<NotificationDto> {
return this.service.get(auth, id);
}
@Put(':id')
@Authenticated({ permission: Permission.NOTIFICATION_UPDATE })
updateNotification(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: NotificationUpdateDto,
): Promise<NotificationDto> {
return this.service.update(auth, id, dto);
}
@Delete(':id')
@Authenticated({ permission: Permission.NOTIFICATION_DELETE })
deleteNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id);
}
}
+1 -1
View File
@@ -90,7 +90,7 @@ export class StorageCore {
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`); return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`);
} }
static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: ImageFormat) { static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: 'jpeg' | 'webp') {
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}-${type}.${format}`); return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}-${type}.${format}`);
} }
+13 -2
View File
@@ -9,6 +9,7 @@ import {
Permission, Permission,
SharedLinkType, SharedLinkType,
SourceType, SourceType,
UserAvatarColor,
UserStatus, UserStatus,
} from 'src/enum'; } from 'src/enum';
import { OnThisDayData, UserMetadataItem } from 'src/types'; import { OnThisDayData, UserMetadataItem } from 'src/types';
@@ -122,6 +123,7 @@ export type User = {
id: string; id: string;
name: string; name: string;
email: string; email: string;
avatarColor: UserAvatarColor | null;
profileImagePath: string; profileImagePath: string;
profileChangedAt: Date; profileChangedAt: Date;
}; };
@@ -264,7 +266,15 @@ export type AssetFace = {
person?: Person | null; person?: Person | null;
}; };
const userColumns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const; const userColumns = ['id', 'name', 'email', 'avatarColor', 'profileImagePath', 'profileChangedAt'] as const;
const userWithPrefixColumns = [
'users.id',
'users.name',
'users.email',
'users.avatarColor',
'users.profileImagePath',
'users.profileChangedAt',
] as const;
export const columns = { export const columns = {
asset: [ asset: [
@@ -306,7 +316,7 @@ export const columns = {
'shared_links.password', 'shared_links.password',
], ],
user: userColumns, user: userColumns,
userWithPrefix: ['users.id', 'users.name', 'users.email', 'users.profileImagePath', 'users.profileChangedAt'], userWithPrefix: userWithPrefixColumns,
userAdmin: [ userAdmin: [
...userColumns, ...userColumns,
'createdAt', 'createdAt',
@@ -323,6 +333,7 @@ export const columns = {
], ],
tag: ['tags.id', 'tags.value', 'tags.createdAt', 'tags.updatedAt', 'tags.color', 'tags.parentId'], tag: ['tags.id', 'tags.value', 'tags.createdAt', 'tags.updatedAt', 'tags.color', 'tags.parentId'],
apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'], apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'],
notification: ['id', 'createdAt', 'level', 'type', 'title', 'description', 'data', 'readAt'],
syncAsset: [ syncAsset: [
'id', 'id',
'ownerId', 'ownerId',
+18
View File
@@ -11,6 +11,8 @@ import {
AssetStatus, AssetStatus,
AssetType, AssetType,
MemoryType, MemoryType,
NotificationLevel,
NotificationType,
Permission, Permission,
SharedLinkType, SharedLinkType,
SourceType, SourceType,
@@ -263,6 +265,21 @@ export interface Memories {
updateId: Generated<string>; updateId: Generated<string>;
} }
export interface Notifications {
id: Generated<string>;
createdAt: Generated<Timestamp>;
updatedAt: Generated<Timestamp>;
deletedAt: Timestamp | null;
updateId: Generated<string>;
userId: string;
level: Generated<NotificationLevel>;
type: NotificationType;
title: string;
description: string | null;
data: any | null;
readAt: Timestamp | null;
}
export interface MemoriesAssetsAssets { export interface MemoriesAssetsAssets {
assetsId: string; assetsId: string;
memoriesId: string; memoriesId: string;
@@ -463,6 +480,7 @@ export interface DB {
memories: Memories; memories: Memories;
memories_assets_assets: MemoriesAssetsAssets; memories_assets_assets: MemoriesAssetsAssets;
migrations: Migrations; migrations: Migrations;
notifications: Notifications;
move_history: MoveHistory; move_history: MoveHistory;
naturalearth_countries: NaturalearthCountries; naturalearth_countries: NaturalearthCountries;
partners_audit: PartnersAudit; partners_audit: PartnersAudit;
+107 -1
View File
@@ -1,4 +1,7 @@
import { IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsString } from 'class-validator';
import { NotificationLevel, NotificationType } from 'src/enum';
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
export class TestEmailResponseDto { export class TestEmailResponseDto {
messageId!: string; messageId!: string;
@@ -11,3 +14,106 @@ export class TemplateDto {
@IsString() @IsString()
template!: string; template!: string;
} }
export class NotificationDto {
id!: string;
@ValidateDate()
createdAt!: Date;
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
level!: NotificationLevel;
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
type!: NotificationType;
title!: string;
description?: string;
data?: any;
readAt?: Date;
}
export class NotificationSearchDto {
@Optional()
@ValidateUUID({ optional: true })
id?: string;
@IsEnum(NotificationLevel)
@Optional()
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
level?: NotificationLevel;
@IsEnum(NotificationType)
@Optional()
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
type?: NotificationType;
@ValidateBoolean({ optional: true })
unread?: boolean;
}
export class NotificationCreateDto {
@Optional()
@IsEnum(NotificationLevel)
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
level?: NotificationLevel;
@IsEnum(NotificationType)
@Optional()
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
type?: NotificationType;
@IsString()
title!: string;
@IsString()
@Optional({ nullable: true })
description?: string | null;
@Optional({ nullable: true })
data?: any;
@ValidateDate({ optional: true, nullable: true })
readAt?: Date | null;
@ValidateUUID()
userId!: string;
}
export class NotificationUpdateDto {
@ValidateDate({ optional: true, nullable: true })
readAt?: Date | null;
}
export class NotificationUpdateAllDto {
@ValidateUUID({ each: true, optional: true })
ids!: string[];
@ValidateDate({ optional: true, nullable: true })
readAt?: Date | null;
}
export class NotificationDeleteAllDto {
@ValidateUUID({ each: true })
ids!: string[];
}
export type MapNotification = {
id: string;
createdAt: Date;
updateId?: string;
level: NotificationLevel;
type: NotificationType;
data: any | null;
title: string;
description: string | null;
readAt: Date | null;
};
export const mapNotification = (notification: MapNotification): NotificationDto => {
return {
id: notification.id,
createdAt: notification.createdAt,
level: notification.level,
type: notification.type,
title: notification.title,
description: notification.description ?? undefined,
data: notification.data ?? undefined,
readAt: notification.readAt ?? undefined,
};
};
+12 -2
View File
@@ -25,6 +25,7 @@ import {
Colorspace, Colorspace,
ImageFormat, ImageFormat,
LogLevel, LogLevel,
OAuthTokenEndpointAuthMethod,
QueueName, QueueName,
ToneMapping, ToneMapping,
TranscodeHWAccel, TranscodeHWAccel,
@@ -33,7 +34,7 @@ import {
VideoContainer, VideoContainer,
} from 'src/enum'; } from 'src/enum';
import { ConcurrentQueueName } from 'src/types'; import { ConcurrentQueueName } from 'src/types';
import { IsCronExpression, ValidateBoolean } from 'src/validation'; import { IsCronExpression, Optional, ValidateBoolean } from 'src/validation';
const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled;
const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled; const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled;
@@ -344,10 +345,19 @@ class SystemConfigOAuthDto {
clientId!: string; clientId!: string;
@ValidateIf(isOAuthEnabled) @ValidateIf(isOAuthEnabled)
@IsNotEmpty()
@IsString() @IsString()
clientSecret!: string; clientSecret!: string;
@IsEnum(OAuthTokenEndpointAuthMethod)
@ApiProperty({ enum: OAuthTokenEndpointAuthMethod, enumName: 'OAuthTokenEndpointAuthMethod' })
tokenEndpointAuthMethod!: OAuthTokenEndpointAuthMethod;
@IsInt()
@IsPositive()
@Optional()
@ApiProperty({ type: 'integer' })
timeout!: number;
@IsNumber() @IsNumber()
@Min(0) @Min(0)
defaultStorageQuota!: number; defaultStorageQuota!: number;
-6
View File
@@ -137,11 +137,6 @@ export class UserPreferencesUpdateDto {
purchase?: PurchaseUpdate; purchase?: PurchaseUpdate;
} }
class AvatarResponse {
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
color!: UserAvatarColor;
}
class RatingsResponse { class RatingsResponse {
enabled: boolean = false; enabled: boolean = false;
} }
@@ -195,7 +190,6 @@ export class UserPreferencesResponseDto implements UserPreferences {
ratings!: RatingsResponse; ratings!: RatingsResponse;
sharedLinks!: SharedLinksResponse; sharedLinks!: SharedLinksResponse;
tags!: TagsResponse; tags!: TagsResponse;
avatar!: AvatarResponse;
emailNotifications!: EmailNotificationsResponse; emailNotifications!: EmailNotificationsResponse;
download!: DownloadResponse; download!: DownloadResponse;
purchase!: PurchaseResponse; purchase!: PurchaseResponse;
+25 -3
View File
@@ -1,10 +1,9 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator'; import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
import { User, UserAdmin } from 'src/database'; import { User, UserAdmin } from 'src/database';
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
import { UserMetadataItem } from 'src/types'; import { UserMetadataItem } from 'src/types';
import { getPreferences } from 'src/utils/preferences';
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation'; import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
export class UserUpdateMeDto { export class UserUpdateMeDto {
@@ -23,6 +22,11 @@ export class UserUpdateMeDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
name?: string; name?: string;
@Optional({ nullable: true })
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
avatarColor?: UserAvatarColor | null;
} }
export class UserResponseDto { export class UserResponseDto {
@@ -41,13 +45,21 @@ export class UserLicense {
activatedAt!: Date; activatedAt!: Date;
} }
const emailToAvatarColor = (email: string): UserAvatarColor => {
const values = Object.values(UserAvatarColor);
const randomIndex = Math.floor(
[...email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
);
return values[randomIndex];
};
export const mapUser = (entity: User | UserAdmin): UserResponseDto => { export const mapUser = (entity: User | UserAdmin): UserResponseDto => {
return { return {
id: entity.id, id: entity.id,
email: entity.email, email: entity.email,
name: entity.name, name: entity.name,
profileImagePath: entity.profileImagePath, profileImagePath: entity.profileImagePath,
avatarColor: getPreferences(entity.email, (entity as UserAdmin).metadata || []).avatar.color, avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email),
profileChangedAt: entity.profileChangedAt, profileChangedAt: entity.profileChangedAt,
}; };
}; };
@@ -69,6 +81,11 @@ export class UserAdminCreateDto {
@IsString() @IsString()
name!: string; name!: string;
@Optional({ nullable: true })
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
avatarColor?: UserAvatarColor | null;
@Optional({ nullable: true }) @Optional({ nullable: true })
@IsString() @IsString()
@Transform(toSanitized) @Transform(toSanitized)
@@ -104,6 +121,11 @@ export class UserAdminUpdateDto {
@IsNotEmpty() @IsNotEmpty()
name?: string; name?: string;
@Optional({ nullable: true })
@IsEnum(UserAvatarColor)
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
avatarColor?: UserAvatarColor | null;
@Optional({ nullable: true }) @Optional({ nullable: true })
@IsString() @IsString()
@Transform(toSanitized) @Transform(toSanitized)
+30
View File
@@ -126,6 +126,11 @@ export enum Permission {
MEMORY_UPDATE = 'memory.update', MEMORY_UPDATE = 'memory.update',
MEMORY_DELETE = 'memory.delete', MEMORY_DELETE = 'memory.delete',
NOTIFICATION_CREATE = 'notification.create',
NOTIFICATION_READ = 'notification.read',
NOTIFICATION_UPDATE = 'notification.update',
NOTIFICATION_DELETE = 'notification.delete',
PARTNER_CREATE = 'partner.create', PARTNER_CREATE = 'partner.create',
PARTNER_READ = 'partner.read', PARTNER_READ = 'partner.read',
PARTNER_UPDATE = 'partner.update', PARTNER_UPDATE = 'partner.update',
@@ -332,6 +337,11 @@ export enum ImageFormat {
WEBP = 'webp', WEBP = 'webp',
} }
export enum RawExtractedFormat {
JPEG = 'jpeg',
JXL = 'jxl',
}
export enum LogLevel { export enum LogLevel {
VERBOSE = 'verbose', VERBOSE = 'verbose',
DEBUG = 'debug', DEBUG = 'debug',
@@ -515,6 +525,7 @@ export enum JobName {
NOTIFY_SIGNUP = 'notify-signup', NOTIFY_SIGNUP = 'notify-signup',
NOTIFY_ALBUM_INVITE = 'notify-album-invite', NOTIFY_ALBUM_INVITE = 'notify-album-invite',
NOTIFY_ALBUM_UPDATE = 'notify-album-update', NOTIFY_ALBUM_UPDATE = 'notify-album-update',
NOTIFICATIONS_CLEANUP = 'notifications-cleanup',
SEND_EMAIL = 'notification-send-email', SEND_EMAIL = 'notification-send-email',
// Version check // Version check
@@ -580,3 +591,22 @@ export enum SyncEntityType {
PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1', PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1',
PartnerAssetExifV1 = 'PartnerAssetExifV1', PartnerAssetExifV1 = 'PartnerAssetExifV1',
} }
export enum NotificationLevel {
Success = 'success',
Error = 'error',
Warning = 'warning',
Info = 'info',
}
export enum NotificationType {
JobFailed = 'JobFailed',
BackupFailed = 'BackupFailed',
SystemMessage = 'SystemMessage',
Custom = 'Custom',
}
export enum OAuthTokenEndpointAuthMethod {
CLIENT_SECRET_POST = 'client_secret_post',
CLIENT_SECRET_BASIC = 'client_secret_basic',
}
@@ -1,5 +1,5 @@
import { DatabaseExtension } from 'src/enum';
import { ConfigRepository } from 'src/repositories/config.repository'; import { ConfigRepository } from 'src/repositories/config.repository';
import { vectorIndexQuery } from 'src/utils/database';
import { MigrationInterface, QueryRunner } from 'typeorm'; import { MigrationInterface, QueryRunner } from 'typeorm';
const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension; const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension;
@@ -8,15 +8,9 @@ export class AddCLIPEmbeddingIndex1700713994428 implements MigrationInterface {
name = 'AddCLIPEmbeddingIndex1700713994428'; name = 'AddCLIPEmbeddingIndex1700713994428';
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
if (vectorExtension === DatabaseExtension.VECTORS) {
await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
}
await queryRunner.query(`SET search_path TO "$user", public, vectors`); await queryRunner.query(`SET search_path TO "$user", public, vectors`);
await queryRunner.query(` await queryRunner.query(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: 'clip_index' }));
CREATE INDEX IF NOT EXISTS clip_index ON smart_search
USING hnsw (embedding vector_cosine_ops)
WITH (ef_construction = 300, m = 16)`);
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
@@ -1,5 +1,5 @@
import { DatabaseExtension } from 'src/enum';
import { ConfigRepository } from 'src/repositories/config.repository'; import { ConfigRepository } from 'src/repositories/config.repository';
import { vectorIndexQuery } from 'src/utils/database';
import { MigrationInterface, QueryRunner } from 'typeorm'; import { MigrationInterface, QueryRunner } from 'typeorm';
const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension; const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension;
@@ -8,15 +8,9 @@ export class AddFaceEmbeddingIndex1700714033632 implements MigrationInterface {
name = 'AddFaceEmbeddingIndex1700714033632'; name = 'AddFaceEmbeddingIndex1700714033632';
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
if (vectorExtension === DatabaseExtension.VECTORS) {
await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
}
await queryRunner.query(`SET search_path TO "$user", public, vectors`); await queryRunner.query(`SET search_path TO "$user", public, vectors`);
await queryRunner.query(` await queryRunner.query(vectorIndexQuery({ vectorExtension, table: 'asset_faces', indexName: 'face_index' }));
CREATE INDEX IF NOT EXISTS face_index ON asset_faces
USING hnsw (embedding vector_cosine_ops)
WITH (ef_construction = 300, m = 16)`);
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
@@ -1,5 +1,6 @@
import { DatabaseExtension } from 'src/enum'; import { DatabaseExtension } from 'src/enum';
import { ConfigRepository } from 'src/repositories/config.repository'; import { ConfigRepository } from 'src/repositories/config.repository';
import { vectorIndexQuery } from 'src/utils/database';
import { MigrationInterface, QueryRunner } from 'typeorm'; import { MigrationInterface, QueryRunner } from 'typeorm';
const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension; const vectorExtension = new ConfigRepository().getEnv().database.vectorExtension;
@@ -8,7 +9,6 @@ export class AddFaceSearchRelation1718486162779 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
if (vectorExtension === DatabaseExtension.VECTORS) { if (vectorExtension === DatabaseExtension.VECTORS) {
await queryRunner.query(`SET search_path TO "$user", public, vectors`); await queryRunner.query(`SET search_path TO "$user", public, vectors`);
await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
} }
const hasEmbeddings = async (tableName: string): Promise<boolean> => { const hasEmbeddings = async (tableName: string): Promise<boolean> => {
@@ -47,21 +47,14 @@ export class AddFaceSearchRelation1718486162779 implements MigrationInterface {
await queryRunner.query(`ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE real[]`); await queryRunner.query(`ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE real[]`);
await queryRunner.query(`ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE vector(512)`); await queryRunner.query(`ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE vector(512)`);
await queryRunner.query(` await queryRunner.query(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: 'clip_index' }));
CREATE INDEX IF NOT EXISTS clip_index ON smart_search
USING hnsw (embedding vector_cosine_ops)
WITH (ef_construction = 300, m = 16)`);
await queryRunner.query(` await queryRunner.query(vectorIndexQuery({ vectorExtension, table: 'face_search', indexName: 'face_index' }));
CREATE INDEX face_index ON face_search
USING hnsw (embedding vector_cosine_ops)
WITH (ef_construction = 300, m = 16)`);
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
if (vectorExtension === DatabaseExtension.VECTORS) { if (vectorExtension === DatabaseExtension.VECTORS) {
await queryRunner.query(`SET search_path TO "$user", public, vectors`); await queryRunner.query(`SET search_path TO "$user", public, vectors`);
await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
} }
await queryRunner.query(`ALTER TABLE asset_faces ADD COLUMN "embedding" vector(512)`); await queryRunner.query(`ALTER TABLE asset_faces ADD COLUMN "embedding" vector(512)`);
@@ -74,9 +67,6 @@ export class AddFaceSearchRelation1718486162779 implements MigrationInterface {
WHERE id = fs."faceId"`); WHERE id = fs."faceId"`);
await queryRunner.query(`DROP TABLE face_search`); await queryRunner.query(`DROP TABLE face_search`);
await queryRunner.query(` await queryRunner.query(vectorIndexQuery({ vectorExtension, table: 'asset_faces', indexName: 'face_index' }));
CREATE INDEX face_index ON asset_faces
USING hnsw (embedding vector_cosine_ops)
WITH (ef_construction = 300, m = 16)`);
} }
} }
+9
View File
@@ -157,6 +157,15 @@ where
and "memories"."ownerId" = $2 and "memories"."ownerId" = $2
and "memories"."deletedAt" is null and "memories"."deletedAt" is null
-- AccessRepository.notification.checkOwnerAccess
select
"notifications"."id"
from
"notifications"
where
"notifications"."id" in ($1)
and "notifications"."userId" = $2
-- AccessRepository.person.checkOwnerAccess -- AccessRepository.person.checkOwnerAccess
select select
"person"."id" "person"."id"
@@ -13,6 +13,7 @@ from
"users"."id", "users"."id",
"users"."name", "users"."name",
"users"."email", "users"."email",
"users"."avatarColor",
"users"."profileImagePath", "users"."profileImagePath",
"users"."profileChangedAt" "users"."profileChangedAt"
from from
@@ -44,6 +45,7 @@ returning
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
+9
View File
@@ -12,6 +12,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@@ -36,6 +37,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@@ -100,6 +102,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@@ -124,6 +127,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@@ -191,6 +195,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@@ -215,6 +220,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@@ -269,6 +275,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@@ -292,6 +299,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from
@@ -353,6 +361,7 @@ select
"id", "id",
"name", "name",
"email", "email",
"avatarColor",
"profileImagePath", "profileImagePath",
"profileChangedAt" "profileChangedAt"
from from

Some files were not shown because too many files have changed in this diff Show More