Compare commits

...

48 Commits

Author SHA1 Message Date
bo0tzz
c24830acbf fix: do not cancel running docs builds 2024-10-02 10:24:20 +02:00
renovate[bot]
e5457ac8ee chore(deps): update dependency flutter_launcher_icons to ^0.14.0 (#13072)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-02 15:04:47 +07:00
renovate[bot]
b0bcc6c03e chore(deps): update typescript-projects (#13099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 19:29:48 -04:00
Jason Rasmussen
63437529e1 refactor(server): config file env (#13100) 2024-10-01 16:03:55 -04:00
Jason Rasmussen
4d20b11f25 feat: track upgrade history (#13097) 2024-10-01 13:33:58 -04:00
renovate[bot]
1c3603e23b chore(deps): update grafana/grafana docker tag to v11.2.1 (#13094)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 13:06:45 -04:00
renovate[bot]
eb3ac09e0d chore(deps): update dependency svelte-check to v4.0.3 (#13090)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 13:05:33 -04:00
Jason Rasmussen
305fc77ebe feat(server): better mount checks (#13092) 2024-10-01 13:04:37 -04:00
Zack Pollard
d46e50213a fix(server): offline assets don't restore when coming back online (#13087) 2024-10-01 14:03:19 +01:00
renovate[bot]
49486f2d26 chore(deps): update base-image to v20241001 (major) (#13089)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 12:26:00 +00:00
renovate[bot]
eac189a9e5 chore(deps): update dependency prettier-plugin-svelte to v3.2.7 (#13088)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 12:25:08 +00:00
Zack Pollard
3b968707a7 fix: deletedAt not set for offline assets during 1.116.0 migration (#13086) 2024-10-01 13:09:08 +01:00
Carsten Otto
67aa124de9 feat(server): parse offset from "Image_UTC_Data" (Samsung) (#13080)
* fix(deps): update dependency exiftool-vendored to v28.3.0

* feat(server): parse offset from "Image_UTC_Data" (Samsung)

A Samsung phone might provide the local time (e.g. 09:00) without any timezone or
offset information. If the file also includes the non-standard trailer tag
"TimeStamp" in "Image_UTC_Data", we can use the unix timestamp contained within to
deduce the offset.

As an example, if the local date/time is "2024-09-15T09:00" and the unix timestamp is
1726408800 (which is 2024-09-15T16:00 UTC), we know that the offset is -07:00.

The actual computation/fix is done in exiftool-vendored.

Also see
0f63a78090/lib/Image/ExifTool/Samsung.pm (L996-L1001)
https://github.com/photostructure/exiftool-vendored.js/issues/209

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 12:08:06 +00:00
renovate[bot]
076d8808bb chore(deps): update dependency ubuntu to v24 (#13079)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 11:17:58 +01:00
renovate[bot]
67ddba0b13 chore(deps): update typescript-projects (#13073)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 11:16:34 +01:00
Zack Pollard
3eccff4306 feat: support and feedback modal with third party support (#13056) 2024-10-01 11:15:31 +01:00
renovate[bot]
ecb5cb00eb chore(deps): update dependency flutter_lints to v5 (#13077)
* chore(deps): update dependency flutter_lints to v5

* lint

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-10-01 04:10:05 +00:00
martin
06048b6db9 feat: preload fonts (#13068) 2024-10-01 09:08:25 +07:00
renovate[bot]
f0ad6627a5 fix(deps): update machine-learning (#13070)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 21:54:28 -04:00
renovate[bot]
14e6d23eeb chore(deps): update dependency @types/node to ^20.16.9 (#13069)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 01:26:39 +00:00
renovate[bot]
d772cc6c6a chore(deps): update dependency lints to v5 (#13059)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-01 08:23:15 +07:00
Alex
fe33732958 chore(mobile): update photo_manager 3.5.0 (#13050) 2024-10-01 08:18:13 +07:00
Jason Rasmussen
a019fb670e refactor(server): config service (#13066)
* refactor(server): config service

* fix: function renaming

---------

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-09-30 17:31:21 -04:00
Jason Rasmussen
f63d251490 refactor(server): user core (#13063) 2024-09-30 16:04:24 -04:00
Jason Rasmussen
dfc2d5002b refactor(server): client events (#13062) 2024-09-30 15:50:34 -04:00
dependabot[bot]
47821cda35 chore(deps): bump docker/build-push-action from 6.7.0 to 6.9.0 (#13052)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.7.0 to 6.9.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.7.0...v6.9.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 14:16:04 -04:00
Fynn Petersen-Frey
15c04d3056 refactor(mobile): DB repository for asset, backup, sync service (#12953)
* refactor(mobile): DB repository for asset, backup, sync service

* review feedback

* fix bug found by Alex

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-09-30 21:37:30 +07:00
Jason Rasmussen
a2d457b01d refactor(server): events (#13003)
* refactor(server): events

* chore: better type

---------

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-09-30 10:35:11 -04:00
Alex
95c67949f7 fix(mobile): share to error (#13044) 2024-09-30 20:51:47 +07:00
renovate[bot]
5bcbe77fb6 chore(deps): update terraform cloudflare to v4.43.0 (#12860)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 12:02:30 +01:00
Mert
7adb35e59e fix(server): /search/random failing with certain options (#13040)
* fix relation handling, remove pagination

* update api, sql

* update mock
2024-09-30 00:29:35 -04:00
Mert
2f13db51df fix(server): "all" button for facial recognition deleting faces instead of unassigning them (#13042)
* unassign faces instead of deleting them

* formatting
2024-09-30 00:29:14 -04:00
Mert
9b309e84c9 docs: update config file (#13041)
update config file
2024-09-30 11:11:42 +07:00
Alex
fa9bb8074c feat(mobile): enhance download operations (#12973)
* add packages

* create download task

* show progress

* save video and image

* show progress info

* live photo wip

* download and link live photos

* Update list of assets

* wip

* correct progress

* add state to download

* revert unncessary change

* repository pattern

* translation

* remove unused code

* update method call from repository

* remove unused variable

* handle multiple livephotos download

* remove logging statement

* lint

* not removing all records
2024-09-29 08:22:02 +00:00
Mert
2bcd27e166 feat(server): generate all thumbnails for an asset in one job (#13012)
* wip

cleanup

add success logs, rename method

do thumbhash too

fixes

fix tests

handle `notify`

wip refactor

refactor

* update tests

* update sql

* pr feedback

* remove unused code

* formatting
2024-09-28 17:47:24 +00:00
Mert
995f0fda47 feat(server): separate quality for thumbnail and preview images (#13006)
* allow different thumbnail and preview quality, better config structure

* update web and api

* wording

* remove empty line?
2024-09-28 06:01:04 +00:00
Mert
4248594ac5 feat(server): better transcoding logs (#13000)
* better transcoding logs

* pr feedback
2024-09-27 18:10:39 -04:00
renovate[bot]
7579bc4359 fix(deps): update machine-learning (#12883)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-27 22:07:59 +00:00
github-actions
8bbcd5c31e chore: version v1.116.2 2024-09-27 18:17:49 +00:00
Alex
4ed1517e60 chore(mobile): post release task (#12991) 2024-09-27 14:13:24 -04:00
Zack Pollard
789937d4a2 fix: library pagination to 10k to avoid too many postgres query params (#12993) 2024-09-27 18:15:44 +01:00
bo0tzz
dbe542803f docs: update FAQ CLIP search explanation (#12986) 2024-09-27 13:07:00 -04:00
github-actions
7c15e11efc chore: version v1.116.1 2024-09-27 15:32:16 +00:00
Alex
03aa346020 fix(mobile): incorrect filename is retrieved during upload (#12990)
* fix(mobile): incorrect filename is retrieve during upload

* use the same convention to get local id

* revert previous change

* pr feedback
2024-09-27 22:28:31 +07:00
martin
3a37fc8bfd feat: no slideshow transition (#12989) 2024-09-27 15:05:07 +00:00
Jason Rasmussen
36ee72cd87 refactor(server): access env via repository (#12987) 2024-09-27 10:28:56 -04:00
Jason Rasmussen
12da250028 refactor: enums (#12988) 2024-09-27 10:28:42 -04:00
Ryan Ribeiro
5b282733fe chore(Brazilian README): fix broken image links and update translation (#12980) 2024-09-27 08:15:25 -04:00
251 changed files with 6353 additions and 3679 deletions

View File

@@ -88,7 +88,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@v6.7.0 uses: docker/build-push-action@v6.9.0
with: with:
file: cli/Dockerfile file: cli/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64

View File

@@ -22,7 +22,7 @@ concurrency:
jobs: jobs:
cleanup-images: cleanup-images:
name: Cleanup Stale Images Tags for ${{ matrix.primary-name }} name: Cleanup Stale Images Tags for ${{ matrix.primary-name }}
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -48,7 +48,7 @@ jobs:
cleanup-untagged-images: cleanup-untagged-images:
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }} name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
needs: needs:
- cleanup-images - cleanup-images
strategy: strategy:

View File

@@ -173,7 +173,7 @@ jobs:
fi fi
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@v6.7.0 uses: docker/build-push-action@v6.9.0
with: with:
context: ${{ env.context }} context: ${{ env.context }}
file: ${{ env.file }} file: ${{ env.file }}
@@ -264,7 +264,7 @@ jobs:
fi fi
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@v6.7.0 uses: docker/build-push-action@v6.9.0
with: with:
context: ${{ env.context }} context: ${{ env.context }}
file: ${{ env.file }} file: ${{ env.file }}

View File

@@ -9,7 +9,7 @@ on:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: false
jobs: jobs:
pre-job: pre-job:

180
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.20", "version": "2.2.22",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.20", "version": "2.2.22",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
@@ -24,7 +24,7 @@
"@types/cli-progress": "^3.11.0", "@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^20.16.5", "@types/node": "^20.16.9",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5", "@vitest/coverage-v8": "^2.0.5",
@@ -52,14 +52,14 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.116.0", "version": "1.116.2",
"dev": true, "dev": true,
"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"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.16.5", "@types/node": "^20.16.9",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
}, },
@@ -765,6 +765,16 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/@eslint/core": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz",
"integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/eslintrc": { "node_modules/@eslint/eslintrc": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
@@ -825,9 +835,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.10.0", "version": "9.11.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz",
"integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==", "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -845,9 +855,9 @@
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.1.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
"integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==", "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -1312,6 +1322,13 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true "dev": true
}, },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
"version": "4.17.0", "version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
@@ -1337,9 +1354,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.16.5", "version": "20.16.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
"integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1353,17 +1370,17 @@
"dev": true "dev": true
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
"integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==", "integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==",
"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.6.0", "@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/type-utils": "8.6.0", "@typescript-eslint/type-utils": "8.7.0",
"@typescript-eslint/utils": "8.6.0", "@typescript-eslint/utils": "8.7.0",
"@typescript-eslint/visitor-keys": "8.6.0", "@typescript-eslint/visitor-keys": "8.7.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",
@@ -1387,16 +1404,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz",
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==", "integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.6.0", "@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.6.0", "@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.6.0", "@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/visitor-keys": "8.6.0", "@typescript-eslint/visitor-keys": "8.7.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1416,14 +1433,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz",
"integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==", "integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.6.0", "@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.6.0" "@typescript-eslint/visitor-keys": "8.7.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1434,14 +1451,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz",
"integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==", "integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "8.6.0", "@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/utils": "8.6.0", "@typescript-eslint/utils": "8.7.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@@ -1459,9 +1476,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz",
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==", "integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1473,14 +1490,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz",
"integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==", "integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.6.0", "@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.6.0", "@typescript-eslint/visitor-keys": "8.7.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",
@@ -1502,16 +1519,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz",
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==", "integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==",
"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.6.0", "@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.6.0", "@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.6.0" "@typescript-eslint/typescript-estree": "8.7.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1525,13 +1542,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz",
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==", "integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.6.0", "@typescript-eslint/types": "8.7.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@@ -2170,21 +2187,24 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.10.0", "version": "9.11.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz",
"integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==", "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==",
"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.11.0", "@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.18.0", "@eslint/config-array": "^0.18.0",
"@eslint/core": "^0.6.0",
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
"@eslint/js": "9.10.0", "@eslint/js": "9.11.1",
"@eslint/plugin-kit": "^0.1.0", "@eslint/plugin-kit": "^0.2.0",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0", "@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4", "ajv": "^6.12.4",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.2", "cross-spawn": "^7.0.2",
@@ -2335,6 +2355,13 @@
"url": "https://opencollective.com/eslint" "url": "https://opencollective.com/eslint"
} }
}, },
"node_modules/eslint/node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
"node_modules/eslint/node_modules/brace-expansion": { "node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -2347,9 +2374,9 @@
} }
}, },
"node_modules/eslint/node_modules/eslint-visitor-keys": { "node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.0.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@@ -3121,10 +3148,11 @@
} }
}, },
"node_modules/mock-fs": { "node_modules/mock-fs": {
"version": "5.2.0", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz", "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.3.0.tgz",
"integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==", "integrity": "sha512-IMvz1X+RF7vf+ur7qUenXMR7/FSKSIqS3HqFHXcyNI7G0FbpFO8L5lfsUJhl+bhK1AiulVHWKUSxebWauPA+xQ==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
} }
@@ -3449,21 +3477,17 @@
} }
}, },
"node_modules/prettier-plugin-organize-imports": { "node_modules/prettier-plugin-organize-imports": {
"version": "4.0.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz", "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==", "integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@vue/language-plugin-pug": "^2.0.24",
"prettier": ">=2.0", "prettier": ">=2.0",
"typescript": ">=2.9", "typescript": ">=2.9",
"vue-tsc": "^2.0.24" "vue-tsc": "^2.1.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@vue/language-plugin-pug": {
"optional": true
},
"vue-tsc": { "vue-tsc": {
"optional": true "optional": true
} }
@@ -4155,9 +4179,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.6", "version": "5.4.8",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.20", "version": "2.2.22",
"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",
@@ -20,7 +20,7 @@
"@types/cli-progress": "^3.11.0", "@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^20.16.5", "@types/node": "^20.16.9",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5", "@vitest/coverage-v8": "^2.0.5",

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates. # Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" { provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.41.0" version = "4.43.0"
constraints = "4.41.0" constraints = "4.43.0"
hashes = [ hashes = [
"h1:0mc+YrjQrcctGrGYDmzlcqcgSv9MYB74rvMaZylIKC8=", "h1:2kDVLD36BOVgBzI9p0WIQ+xjFfMmjaItA0l8SyZWEPo=",
"h1:0zUx4vk4jOORQqn6xHBF7dO6N6bielFHdJ0mgF4Obn8=", "h1:2sGJDAwFEgO8+3y+2suYO+yrjNOzSsihad0hbM3+jPg=",
"h1:AsIZW3uLFNOZO7kL/K7/Y/S0IYxUV9Hz85NNk/3TTsA=", "h1:A1WPQFcdD+7FrFBFrKcx4CiSr75xSmsO93C0e5NBAeQ=",
"h1:FSgYM4+LHMbX/a4Y1kx7FPPWmXqS3/MQYzvjMJHHHWM=", "h1:BuXs/1ohmF4fWyOErY6vNbm7DaEIfbLSepSiZ2ol9I8=",
"h1:Tx6Nh3BWP1x9L3KK/Eyi+ET0T26g3+jf1jyiuqpNIis=", "h1:QPh+X19oyo808sqdeJaVqahZcQgcG1jCi3DA5zpjz6U=",
"h1:VRI9wu8P43xxfpeTndRwsisLnqncfnmEYMOEH5zH4pQ=", "h1:RI7c7dhSJoIkfou5b8ITRpM5MqsQD3FULj1h/rI4rJk=",
"h1:YxQqmiES/Yanq/VfGqBEqg+VIO7FGhO88aKoWFHyGIg=", "h1:gdI5JTCPjewdGq1bhGAs+V5qCcmJ73N2gtMfuFybJp4=",
"h1:ZWHiaesjgDLKWlfdNj0oKyj/DWdxcfsO6NINu39zfpY=", "h1:h4lnJpCIYZ7dsN9IO2mmwNdWNiQYEPoAEUjLF2sZ5kc=",
"h1:a2aCgDDBz3ccrr8YstIMl7VFnKo1xZAp+rOv59PPJ7U=", "h1:jTaExrX/eR7vGT5wayGqH8ZtXS2zyk0WmD3zbAKFIQU=",
"h1:aRyv8tB6wBAF9lKsLEdiHyCqnK5LfZq0FqMXCcUB4UU=", "h1:l5NKJUOQJ1mHl1eekeXaxUZ+g+8Yv4aGcIN9vuK6GL4=",
"h1:lXpuO7zv2uD2GzPE1ARxznreRAh+QHTc2lAJ7iOoFgY=", "h1:sNbvm66/2vc8B/khyioOO8eNaU8nb89x693AN7fQheU=",
"h1:sA1xq0QNQ4fH8SHXouYNq50xirVD18SamKQwPsBQrrY=", "h1:tXS4g1yE420AU4mvZ7RrYI+yYTutkRID3l+W0gBH4BM=",
"h1:v7sHvKq7oqMYPn47ULHFyIQsKD9o+6Xg/uHbxQUixEw=", "h1:vA+kES7uqmKA9K0U45IXR94jaTQZCHZLCHqMUeGxKMI=",
"h1:wo/x4atWyXuWGlfR6h5nH0YwBAmBwTRY27HtWP8ycLo=", "h1:zV131k79+ob9p4jrLDgztDNvZvt8fvrrzpn0nPikBw8=",
"zh:339d26e06dc6fb299ea8aad9476a60fd65bb1d40631ae8eeb81cddf2dd2bebc8", "zh:006d111d6eafe6eeb5df2f91bd0ca320f979bd71f8cd8c475f10b2bd94acba55",
"zh:3dec2ad96ac2c283fd34ce65781b55c4edbb4d5c5cb53da8e31537176c0ed562", "zh:031fbb5cac23a841dc18e270cbfcd3ce9f4ba504edbd3c78931f7ed9827220a8",
"zh:5f63a5f8080319a2fff09d4d49944829fa708723436520787cfb60725ced80cf", "zh:07a72fe8b55afee99529bf4169ab6abfac5eabcd10968c29101925bcd358b09f",
"zh:67162c28ccea71cb8141ed15c0637e35621354ebe14878e0b75a8f160fc5505d", "zh:0d14727d011c2d9df4c3058f527d2409223449ab48b46cbc86922eb553ef77c1",
"zh:6ac1e07f5347b6395aca690ed22101bb25e957d25f986f760ff673a7adfd5ef6", "zh:155ce1333672d26cd18a5866b0761489d91682beffee58e45c3a1b68e8491d3d",
"zh:70282a723c7b52fcabde2baad41c864ed3a8d69f0c4d27a6b6933cac434cffc6", "zh:35a2a1939a965335b29ebdbfd759d93a97c0f589d9cd218f537dee6f600e3fb9",
"zh:52912fe421e7d911431f77788db2ea13836efd65a2e82385adb52c6a84d4ee90",
"zh:57374318d9194ea1db08884b0541a9055823d5970ad48f9a57547ac231163007",
"zh:5fb942b9e2553c058fe09fe12fb39dd175cd6715bb41c059c1a70df2bfc64dc1",
"zh:63cabd2bda201b09b35a3279d1f813ab71394b9b90fc5cf8962a5eba207803bc",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:924cd23abc326c6b3914e2cd9c94c7832c2552e1e9ae258fb9fd9aedaa5f7ce7", "zh:978ee67d3d53970a5c474ab40b00adee97f4153b16804a2b6b7ee205ae69d18a",
"zh:a4b75e4c239879296259e7d54f1befbc7fdc16da2d62d1294e9f73add4cae61e", "zh:bbafdbef631b5c80570087817b42b16b1a76d556d692853a71c47fb48663cf00",
"zh:a6ceb08feb63b00c7141783b31e45a154c76fd8cdebbdf371074805f0053572d", "zh:be91b3f2a697cbbb41f65aad2600972d0ede1e962a7d8a00bb3177cb77d86666",
"zh:afae1843f9ba85f2f6d94108c65cf43a457e83531a632d44d863e935160cb2ba", "zh:efe168ad4aaa6156ce5a31d4e50e9d54d38ee5a5888412f9e690c0de5d619683",
"zh:bd6628ce60c778960a5755f7010b7e2cc5c6ff0341a21c175341b28058ec843d",
"zh:cd30866a1ff99d72b5fa1699db582fa4f25562e6ab21dcc6870324f3056108e0",
"zh:df5924cca691a8220aaaebb5cb55c3d6c32ff0a881f198695eff28155eb12b54",
"zh:e78d0696c941aba58df1cb36b8a0d25cd5f3963f01d9338fdbda74db58afdd49",
] ]
} }

View File

@@ -5,7 +5,7 @@ terraform {
required_providers { required_providers {
cloudflare = { cloudflare = {
source = "cloudflare/cloudflare" source = "cloudflare/cloudflare"
version = "4.41.0" version = "4.43.0"
} }
} }
} }

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates. # Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" { provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.41.0" version = "4.43.0"
constraints = "4.41.0" constraints = "4.43.0"
hashes = [ hashes = [
"h1:0mc+YrjQrcctGrGYDmzlcqcgSv9MYB74rvMaZylIKC8=", "h1:2kDVLD36BOVgBzI9p0WIQ+xjFfMmjaItA0l8SyZWEPo=",
"h1:0zUx4vk4jOORQqn6xHBF7dO6N6bielFHdJ0mgF4Obn8=", "h1:2sGJDAwFEgO8+3y+2suYO+yrjNOzSsihad0hbM3+jPg=",
"h1:AsIZW3uLFNOZO7kL/K7/Y/S0IYxUV9Hz85NNk/3TTsA=", "h1:A1WPQFcdD+7FrFBFrKcx4CiSr75xSmsO93C0e5NBAeQ=",
"h1:FSgYM4+LHMbX/a4Y1kx7FPPWmXqS3/MQYzvjMJHHHWM=", "h1:BuXs/1ohmF4fWyOErY6vNbm7DaEIfbLSepSiZ2ol9I8=",
"h1:Tx6Nh3BWP1x9L3KK/Eyi+ET0T26g3+jf1jyiuqpNIis=", "h1:QPh+X19oyo808sqdeJaVqahZcQgcG1jCi3DA5zpjz6U=",
"h1:VRI9wu8P43xxfpeTndRwsisLnqncfnmEYMOEH5zH4pQ=", "h1:RI7c7dhSJoIkfou5b8ITRpM5MqsQD3FULj1h/rI4rJk=",
"h1:YxQqmiES/Yanq/VfGqBEqg+VIO7FGhO88aKoWFHyGIg=", "h1:gdI5JTCPjewdGq1bhGAs+V5qCcmJ73N2gtMfuFybJp4=",
"h1:ZWHiaesjgDLKWlfdNj0oKyj/DWdxcfsO6NINu39zfpY=", "h1:h4lnJpCIYZ7dsN9IO2mmwNdWNiQYEPoAEUjLF2sZ5kc=",
"h1:a2aCgDDBz3ccrr8YstIMl7VFnKo1xZAp+rOv59PPJ7U=", "h1:jTaExrX/eR7vGT5wayGqH8ZtXS2zyk0WmD3zbAKFIQU=",
"h1:aRyv8tB6wBAF9lKsLEdiHyCqnK5LfZq0FqMXCcUB4UU=", "h1:l5NKJUOQJ1mHl1eekeXaxUZ+g+8Yv4aGcIN9vuK6GL4=",
"h1:lXpuO7zv2uD2GzPE1ARxznreRAh+QHTc2lAJ7iOoFgY=", "h1:sNbvm66/2vc8B/khyioOO8eNaU8nb89x693AN7fQheU=",
"h1:sA1xq0QNQ4fH8SHXouYNq50xirVD18SamKQwPsBQrrY=", "h1:tXS4g1yE420AU4mvZ7RrYI+yYTutkRID3l+W0gBH4BM=",
"h1:v7sHvKq7oqMYPn47ULHFyIQsKD9o+6Xg/uHbxQUixEw=", "h1:vA+kES7uqmKA9K0U45IXR94jaTQZCHZLCHqMUeGxKMI=",
"h1:wo/x4atWyXuWGlfR6h5nH0YwBAmBwTRY27HtWP8ycLo=", "h1:zV131k79+ob9p4jrLDgztDNvZvt8fvrrzpn0nPikBw8=",
"zh:339d26e06dc6fb299ea8aad9476a60fd65bb1d40631ae8eeb81cddf2dd2bebc8", "zh:006d111d6eafe6eeb5df2f91bd0ca320f979bd71f8cd8c475f10b2bd94acba55",
"zh:3dec2ad96ac2c283fd34ce65781b55c4edbb4d5c5cb53da8e31537176c0ed562", "zh:031fbb5cac23a841dc18e270cbfcd3ce9f4ba504edbd3c78931f7ed9827220a8",
"zh:5f63a5f8080319a2fff09d4d49944829fa708723436520787cfb60725ced80cf", "zh:07a72fe8b55afee99529bf4169ab6abfac5eabcd10968c29101925bcd358b09f",
"zh:67162c28ccea71cb8141ed15c0637e35621354ebe14878e0b75a8f160fc5505d", "zh:0d14727d011c2d9df4c3058f527d2409223449ab48b46cbc86922eb553ef77c1",
"zh:6ac1e07f5347b6395aca690ed22101bb25e957d25f986f760ff673a7adfd5ef6", "zh:155ce1333672d26cd18a5866b0761489d91682beffee58e45c3a1b68e8491d3d",
"zh:70282a723c7b52fcabde2baad41c864ed3a8d69f0c4d27a6b6933cac434cffc6", "zh:35a2a1939a965335b29ebdbfd759d93a97c0f589d9cd218f537dee6f600e3fb9",
"zh:52912fe421e7d911431f77788db2ea13836efd65a2e82385adb52c6a84d4ee90",
"zh:57374318d9194ea1db08884b0541a9055823d5970ad48f9a57547ac231163007",
"zh:5fb942b9e2553c058fe09fe12fb39dd175cd6715bb41c059c1a70df2bfc64dc1",
"zh:63cabd2bda201b09b35a3279d1f813ab71394b9b90fc5cf8962a5eba207803bc",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:924cd23abc326c6b3914e2cd9c94c7832c2552e1e9ae258fb9fd9aedaa5f7ce7", "zh:978ee67d3d53970a5c474ab40b00adee97f4153b16804a2b6b7ee205ae69d18a",
"zh:a4b75e4c239879296259e7d54f1befbc7fdc16da2d62d1294e9f73add4cae61e", "zh:bbafdbef631b5c80570087817b42b16b1a76d556d692853a71c47fb48663cf00",
"zh:a6ceb08feb63b00c7141783b31e45a154c76fd8cdebbdf371074805f0053572d", "zh:be91b3f2a697cbbb41f65aad2600972d0ede1e962a7d8a00bb3177cb77d86666",
"zh:afae1843f9ba85f2f6d94108c65cf43a457e83531a632d44d863e935160cb2ba", "zh:efe168ad4aaa6156ce5a31d4e50e9d54d38ee5a5888412f9e690c0de5d619683",
"zh:bd6628ce60c778960a5755f7010b7e2cc5c6ff0341a21c175341b28058ec843d",
"zh:cd30866a1ff99d72b5fa1699db582fa4f25562e6ab21dcc6870324f3056108e0",
"zh:df5924cca691a8220aaaebb5cb55c3d6c32ff0a881f198695eff28155eb12b54",
"zh:e78d0696c941aba58df1cb36b8a0d25cd5f3963f01d9338fdbda74db58afdd49",
] ]
} }

View File

@@ -5,7 +5,7 @@ terraform {
required_providers { required_providers {
cloudflare = { cloudflare = {
source = "cloudflare/cloudflare" source = "cloudflare/cloudflare"
version = "4.41.0" version = "4.43.0"
} }
} }
} }

View File

@@ -36,6 +36,10 @@ services:
IMMICH_BUILD_URL: https://github.com/immich-app/immich/actions/runs/9654404849 IMMICH_BUILD_URL: https://github.com/immich-app/immich/actions/runs/9654404849
IMMICH_BUILD_IMAGE: development IMMICH_BUILD_IMAGE: development
IMMICH_BUILD_IMAGE_URL: https://github.com/immich-app/immich/pkgs/container/immich-server IMMICH_BUILD_IMAGE_URL: https://github.com/immich-app/immich/pkgs/container/immich-server
IMMICH_THIRD_PARTY_SOURCE_URL: https://github.com/immich-app/immich/
IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues
IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://immich.app/docs
IMMICH_THIRD_PARTY_SUPPORT_URL: https://immich.app/docs/third-party
ulimits: ulimits:
nofile: nofile:
soft: 1048576 soft: 1048576

View File

@@ -91,7 +91,7 @@ services:
command: ['./run.sh', '-disable-reporting'] command: ['./run.sh', '-disable-reporting']
ports: ports:
- 3000:3000 - 3000:3000
image: grafana/grafana:11.2.0-ubuntu@sha256:8e2c13739563c3da9d45de96c6bcb63ba617cac8c571c060112c7fc8ad6914e9 image: grafana/grafana:11.2.1-ubuntu@sha256:b90c0fdc482913de7a55fe96539bf9e3c4fbcee835d0c2dffc59152bc3964ff7
volumes: volumes:
- grafana-data:/var/lib/grafana - grafana-data:/var/lib/grafana

View File

@@ -187,7 +187,7 @@ However, when the trash is emptied, the files will re-appear in the main timelin
### How does smart search work? ### How does smart search work?
Immich uses CLIP models. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip). Immich uses CLIP models. An ML model converts each image to an "embedding", which is essentially a string of numbers that semantically encodes what is in the image. The same is done for the text that you enter when you do a search, and that text embedding is then compared with those of the images to find similar ones. As such, there are no "tags", "labels", or "descriptions" generated that you can look at. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
### How does facial recognition work? ### How does facial recognition work?

View File

@@ -0,0 +1,47 @@
# System Integrity
## Folder checks
:::info
The folders considered for these checks include: `upload/`, `library/`, `thumbs/`, `encoded-video/`, `profile/`
:::
When Immich starts, it performs a series of checks in order to validate that it can read and write files to the volume mounts used by the storage system. If it cannot perform all the required operations, it will fail to start. The checks include:
- Creating an initial hidden file (`.immich`) in each folder
- Reading a hidden file (`.immich`) in each folder
- Overwriting a hidden file (`.immich`) in each folder
The checks are designed to catch the following situations:
- Incorrect permissions (cannot read/write files)
- Missing volume mount (`.immich` files should exist, but are missing)
### Common issues
:::note
`.immich` files serve as markers and help keep track of volume mounts being used by Immich. Except for the situations listed below, they should never be manually created or deleted.
:::
#### Missing `.immich` files
```
Verifying system mount folder checks (enabled=true)
...
ENOENT: no such file or directory, open 'upload/encoded-video/.immich'
```
The above error messages show that the server has previously (successfully) written `.immich` files to each folder, but now does not detect them. This could be because any of the following:
- Permission error - unable to read the file, but it exists
- File does not exist - volume mount has changed and should be corrected
- File does not exist - user manually deleted it and should be manually re-created (`touch .immich`)
- File does not exist - user restored from a backup, but did not restore each folder (user should restore all folders or manually create `.immich` in any missing folders)
### Ignoring the checks
The checks are designed to catch common problems that we have seen users have in the past, but if you want to disable them you can set the following environment variable:
```
IMMICH_IGNORE_MOUNT_CHECK_ERRORS=true
```

View File

@@ -20,6 +20,7 @@ The default configuration looks like this:
"acceptedVideoCodecs": ["h264"], "acceptedVideoCodecs": ["h264"],
"targetAudioCodec": "aac", "targetAudioCodec": "aac",
"acceptedAudioCodecs": ["aac", "mp3", "libopus"], "acceptedAudioCodecs": ["aac", "mp3", "libopus"],
"acceptedContainers": ["mov", "ogg", "webm"],
"targetResolution": "720", "targetResolution": "720",
"maxBitrate": "0", "maxBitrate": "0",
"bframes": -1, "bframes": -1,
@@ -32,7 +33,8 @@ The default configuration looks like this:
"preferredHwDevice": "auto", "preferredHwDevice": "auto",
"transcode": "required", "transcode": "required",
"tonemap": "hable", "tonemap": "hable",
"accel": "disabled" "accel": "disabled",
"accelDecode": false
}, },
"job": { "job": {
"backgroundTask": { "backgroundTask": {
@@ -60,10 +62,13 @@ The default configuration looks like this:
"concurrency": 5 "concurrency": 5
}, },
"thumbnailGeneration": { "thumbnailGeneration": {
"concurrency": 5 "concurrency": 3
}, },
"videoConversion": { "videoConversion": {
"concurrency": 1 "concurrency": 1
},
"notifications": {
"concurrency": 5
} }
}, },
"logging": { "logging": {
@@ -78,40 +83,46 @@ The default configuration looks like this:
"modelName": "ViT-B-32__openai" "modelName": "ViT-B-32__openai"
}, },
"duplicateDetection": { "duplicateDetection": {
"enabled": false, "enabled": true,
"maxDistance": 0.03 "maxDistance": 0.01
}, },
"facialRecognition": { "facialRecognition": {
"enabled": true, "enabled": true,
"modelName": "buffalo_l", "modelName": "buffalo_l",
"minScore": 0.7, "minScore": 0.7,
"maxDistance": 0.6, "maxDistance": 0.5,
"minFaces": 3 "minFaces": 3
} }
}, },
"map": { "map": {
"enabled": true, "enabled": true,
"lightStyle": "", "lightStyle": "https://tiles.immich.cloud/v1/style/light.json",
"darkStyle": "" "darkStyle": "https://tiles.immich.cloud/v1/style/dark.json"
}, },
"reverseGeocoding": { "reverseGeocoding": {
"enabled": true "enabled": true
}, },
"metadata": {
"faces": {
"import": false
}
},
"oauth": { "oauth": {
"enabled": false, "autoLaunch": false,
"issuerUrl": "", "autoRegister": true,
"buttonText": "Login with OAuth",
"clientId": "", "clientId": "",
"clientSecret": "", "clientSecret": "",
"defaultStorageQuota": 0,
"enabled": false,
"issuerUrl": "",
"mobileOverrideEnabled": false,
"mobileRedirectUri": "",
"scope": "openid email profile", "scope": "openid email profile",
"signingAlgorithm": "RS256", "signingAlgorithm": "RS256",
"profileSigningAlgorithm": "none",
"storageLabelClaim": "preferred_username", "storageLabelClaim": "preferred_username",
"storageQuotaClaim": "immich_quota", "storageQuotaClaim": "immich_quota"
"defaultStorageQuota": 0,
"buttonText": "Login with OAuth",
"autoRegister": true,
"autoLaunch": false,
"mobileOverrideEnabled": false,
"mobileRedirectUri": ""
}, },
"passwordLogin": { "passwordLogin": {
"enabled": true "enabled": true
@@ -122,11 +133,16 @@ The default configuration looks like this:
"template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}" "template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}"
}, },
"image": { "image": {
"thumbnailFormat": "webp", "thumbnail": {
"thumbnailSize": 250, "format": "webp",
"previewFormat": "jpeg", "size": 250,
"previewSize": 1440, "quality": 80
"quality": 80, },
"preview": {
"format": "jpeg",
"size": 1440,
"quality": 80
},
"colorspace": "p3", "colorspace": "p3",
"extractEmbedded": false "extractEmbedded": false
}, },
@@ -140,23 +156,35 @@ The default configuration looks like this:
"theme": { "theme": {
"customCss": "" "customCss": ""
}, },
"user": {
"deleteDelay": 7
},
"library": { "library": {
"scan": { "scan": {
"enabled": true, "enabled": true,
"cronExpression": "0 0 * * *" "cronExpression": "0 0 * * *"
}, },
"watch": { "watch": {
"enabled": false, "enabled": false
"usePolling": false,
"interval": 10000
} }
}, },
"server": { "server": {
"externalDomain": "", "externalDomain": "",
"loginPageMessage": "" "loginPageMessage": ""
},
"notifications": {
"smtp": {
"enabled": false,
"from": "",
"replyTo": "",
"transport": {
"ignoreCert": false,
"host": "",
"port": 587,
"username": "",
"password": ""
}
}
},
"user": {
"deleteDelay": 7
} }
} }
``` ```

View File

@@ -42,6 +42,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices | | `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | | `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
| `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api | | `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api |
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/docs/administration/system-integrity) | | server | api, microservices |
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. \*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution. `TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.

29
docs/package-lock.json generated
View File

@@ -12715,9 +12715,9 @@
} }
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
@@ -12829,9 +12829,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.40", "version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -12849,8 +12849,8 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"picocolors": "^1.0.1", "picocolors": "^1.1.0",
"source-map-js": "^1.2.0" "source-map-js": "^1.2.1"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
@@ -15670,9 +15670,10 @@
} }
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.0", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -16091,9 +16092,9 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.12", "version": "3.4.13",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
"integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==", "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",

View File

@@ -1,4 +1,12 @@
[ [
{
"label": "v1.116.2",
"url": "https://v1.116.2.archive.immich.app"
},
{
"label": "v1.116.1",
"url": "https://v1.116.1.archive.immich.app"
},
{ {
"label": "v1.116.0", "label": "v1.116.0",
"url": "https://v1.116.0.archive.immich.app" "url": "https://v1.116.0.archive.immich.app"

249
e2e/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.116.0", "version": "1.116.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.116.0", "version": "1.116.2",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
@@ -15,7 +15,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^20.16.5", "@types/node": "^20.16.9",
"@types/oidc-provider": "^8.5.1", "@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0", "@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
@@ -45,7 +45,7 @@
}, },
"../cli": { "../cli": {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.20", "version": "2.2.22",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
@@ -64,7 +64,7 @@
"@types/cli-progress": "^3.11.0", "@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^20.16.5", "@types/node": "^20.16.9",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5", "@vitest/coverage-v8": "^2.0.5",
@@ -92,14 +92,14 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.116.0", "version": "1.116.2",
"dev": true, "dev": true,
"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"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.16.5", "@types/node": "^20.16.9",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
}, },
@@ -784,6 +784,16 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@eslint/core": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz",
"integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/eslintrc": { "node_modules/@eslint/eslintrc": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
@@ -822,9 +832,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.10.0", "version": "9.11.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz",
"integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==", "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -842,9 +852,9 @@
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.1.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
"integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==", "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -1121,10 +1131,11 @@
} }
}, },
"node_modules/@photostructure/tz-lookup": { "node_modules/@photostructure/tz-lookup": {
"version": "10.0.0", "version": "11.0.0",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-10.0.0.tgz", "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-11.0.0.tgz",
"integrity": "sha512-8ZAjoj/irCuvUlyEinQ/HB6A8hP3bD1dgTOZvfl1b9nAwqniutFDHOQRcGM6Crea68bOwPj010f0Z4KkmuLHEA==", "integrity": "sha512-QMV5/dWtY/MdVPXZs/EApqzyhnqDq1keYEqpS+Xj2uidyaqw2Nk/fWcsszdruIXjdqp1VoWNzsgrO6bUHU1mFw==",
"dev": true "dev": true,
"license": "CC0-1.0"
}, },
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
@@ -1149,13 +1160,13 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.47.1", "version": "1.47.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.1.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz",
"integrity": "sha512-dbWpcNQZ5nj16m+A5UNScYx7HX5trIy7g4phrcitn+Nk83S32EBX/CLU4hiF4RGKX/yRc93AAqtfaXB7JWBd4Q==", "integrity": "sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.47.1" "playwright": "1.47.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -1519,6 +1530,13 @@
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"dev": true "dev": true
}, },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/keygrip": { "node_modules/@types/keygrip": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz",
@@ -1569,9 +1587,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.16.5", "version": "20.16.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
"integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1733,17 +1751,17 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
"integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==", "integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==",
"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.6.0", "@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/type-utils": "8.6.0", "@typescript-eslint/type-utils": "8.7.0",
"@typescript-eslint/utils": "8.6.0", "@typescript-eslint/utils": "8.7.0",
"@typescript-eslint/visitor-keys": "8.6.0", "@typescript-eslint/visitor-keys": "8.7.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",
@@ -1767,16 +1785,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz",
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==", "integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.6.0", "@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.6.0", "@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.6.0", "@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/visitor-keys": "8.6.0", "@typescript-eslint/visitor-keys": "8.7.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1796,14 +1814,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz",
"integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==", "integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.6.0", "@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.6.0" "@typescript-eslint/visitor-keys": "8.7.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1814,14 +1832,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz",
"integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==", "integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "8.6.0", "@typescript-eslint/typescript-estree": "8.7.0",
"@typescript-eslint/utils": "8.6.0", "@typescript-eslint/utils": "8.7.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@@ -1839,9 +1857,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz",
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==", "integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1853,14 +1871,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz",
"integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==", "integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.6.0", "@typescript-eslint/types": "8.7.0",
"@typescript-eslint/visitor-keys": "8.6.0", "@typescript-eslint/visitor-keys": "8.7.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",
@@ -1908,16 +1926,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz",
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==", "integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==",
"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.6.0", "@typescript-eslint/scope-manager": "8.7.0",
"@typescript-eslint/types": "8.6.0", "@typescript-eslint/types": "8.7.0",
"@typescript-eslint/typescript-estree": "8.6.0" "@typescript-eslint/typescript-estree": "8.7.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1931,13 +1949,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.6.0", "version": "8.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz",
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==", "integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.6.0", "@typescript-eslint/types": "8.7.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@@ -2854,16 +2872,17 @@
} }
}, },
"node_modules/engine.io-client": { "node_modules/engine.io-client": {
"version": "6.5.4", "version": "6.6.1",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz",
"integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@socket.io/component-emitter": "~3.1.0", "@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1", "debug": "~4.3.1",
"engine.io-parser": "~5.2.1", "engine.io-parser": "~5.2.1",
"ws": "~8.17.1", "ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.0.0" "xmlhttprequest-ssl": "~2.1.1"
} }
}, },
"node_modules/engine.io-parser": { "node_modules/engine.io-parser": {
@@ -2972,21 +2991,24 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.10.0", "version": "9.11.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz",
"integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==", "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==",
"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.11.0", "@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.18.0", "@eslint/config-array": "^0.18.0",
"@eslint/core": "^0.6.0",
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
"@eslint/js": "9.10.0", "@eslint/js": "9.11.1",
"@eslint/plugin-kit": "^0.1.0", "@eslint/plugin-kit": "^0.2.0",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0", "@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4", "ajv": "^6.12.4",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.2", "cross-spawn": "^7.0.2",
@@ -3138,9 +3160,9 @@
} }
}, },
"node_modules/eslint/node_modules/eslint-visitor-keys": { "node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.0.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@@ -3246,27 +3268,27 @@
} }
}, },
"node_modules/exiftool-vendored": { "node_modules/exiftool-vendored": {
"version": "28.2.1", "version": "28.3.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz", "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.3.0.tgz",
"integrity": "sha512-D3YsKErr3BbjKeJzUVsv6CVZ+SQNgAJKPFWVLXu0CBtr24FNuE3CZBXWKWysGu0MjzeDCNwQrQI5+bXUFeiYVA==", "integrity": "sha512-2DOSOvj5c1gkbKtubAnlGglxdYp9h55n0GxjK2nypVivoaCdgP/le3MOZRKgEUNObfJHmYHj4u/NnYVneu/gUw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@photostructure/tz-lookup": "^10.0.0", "@photostructure/tz-lookup": "^11.0.0",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"batch-cluster": "^13.0.0", "batch-cluster": "^13.0.0",
"he": "^1.2.0", "he": "^1.2.0",
"luxon": "^3.5.0" "luxon": "^3.5.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"exiftool-vendored.exe": "12.91.0", "exiftool-vendored.exe": "12.96.0",
"exiftool-vendored.pl": "12.91.0" "exiftool-vendored.pl": "12.96.0"
} }
}, },
"node_modules/exiftool-vendored.exe": { "node_modules/exiftool-vendored.exe": {
"version": "12.91.0", "version": "12.96.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.91.0.tgz", "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.96.0.tgz",
"integrity": "sha512-nxcoGBaJL/D+Wb0jVe8qwyV8QZpRcCzU0aCKhG0S1XNGWGjJJJ4QV851aobcfDwI4NluFOdqkjTSf32pVijvHg==", "integrity": "sha512-pKPN9F/Evw2yyO5/+ml3spbXIqejzOxyF7jEnj8tLU2JPSmIlziPUZ75XIhcPbilX86jVKmuiso7FUDicOg8pQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -3275,9 +3297,9 @@
] ]
}, },
"node_modules/exiftool-vendored.pl": { "node_modules/exiftool-vendored.pl": {
"version": "12.91.0", "version": "12.96.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.91.0.tgz", "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.96.0.tgz",
"integrity": "sha512-GZMy9+Jiv8/C7R4uYe1kWtXsAaJdgVezTwYa+wDeoqvReHiX2t5uzkCrzWdjo4LGl5mPQkyKhN7/uPLYk5Ak6w==", "integrity": "sha512-v4nGnovAMBsTfOWhwAcOiRiq/8kuJOo3GUMHNpug7Mr4jLz3tmWEo7DdNyOYmpcvWbA6smOTG0SmwsrY8fsW+A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -4142,9 +4164,9 @@
} }
}, },
"node_modules/jose": { "node_modules/jose": {
"version": "5.9.2", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.9.2.tgz", "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.3.tgz",
"integrity": "sha512-ILI2xx/I57b20sd7rHZvgiiQrmp2mcotwsAH+5ajbpFQbrYVQdNHYlQhoA5cFb78CgtBOxtC05TeA+mcgkuCqQ==", "integrity": "sha512-egLIoYSpcd+QUF+UHgobt5YzI2Pkw/H39ou9suW687MY6PmCwPmkNV/4TNjn1p2tX5xO3j0d0sq5hiYE24bSlg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@@ -5135,13 +5157,13 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.47.1", "version": "1.47.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.1.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz",
"integrity": "sha512-SUEKi6947IqYbKxRiqnbUobVZY4bF1uu+ZnZNJX9DfU1tlf2UhWfvVjLf01pQx9URsOr18bFVUKXmanYWhbfkw==", "integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.47.1" "playwright-core": "1.47.2"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -5154,9 +5176,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.47.1", "version": "1.47.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.1.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz",
"integrity": "sha512-i1iyJdLftqtt51mEk6AhYFaAJCDx0xQ/O5NU8EKaWFgMjItPVma542Nh/Aq8aLCjIJSzjaiEQGW/nyqLkGF1OQ==", "integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@@ -5296,21 +5318,17 @@
} }
}, },
"node_modules/prettier-plugin-organize-imports": { "node_modules/prettier-plugin-organize-imports": {
"version": "4.0.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz", "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==", "integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@vue/language-plugin-pug": "^2.0.24",
"prettier": ">=2.0", "prettier": ">=2.0",
"typescript": ">=2.9", "typescript": ">=2.9",
"vue-tsc": "^2.0.24" "vue-tsc": "^2.1.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@vue/language-plugin-pug": {
"optional": true
},
"vue-tsc": { "vue-tsc": {
"optional": true "optional": true
} }
@@ -5796,14 +5814,15 @@
} }
}, },
"node_modules/socket.io-client": { "node_modules/socket.io-client": {
"version": "4.7.5", "version": "4.8.0",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz",
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@socket.io/component-emitter": "~3.1.0", "@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2", "debug": "~4.3.2",
"engine.io-client": "~6.5.2", "engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4" "socket.io-parser": "~4.2.4"
}, },
"engines": { "engines": {
@@ -6732,9 +6751,9 @@
} }
}, },
"node_modules/xmlhttprequest-ssl": { "node_modules/xmlhttprequest-ssl": {
"version": "2.0.0", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"

View File

@@ -1,6 +1,6 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.116.0", "version": "1.116.2",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@@ -25,7 +25,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^20.16.5", "@types/node": "^20.16.9",
"@types/oidc-provider": "^8.5.1", "@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0", "@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",

View File

@@ -76,7 +76,6 @@ describe('/asset', () => {
let user2Assets: AssetMediaResponseDto[]; let user2Assets: AssetMediaResponseDto[];
let locationAsset: AssetMediaResponseDto; let locationAsset: AssetMediaResponseDto;
let ratingAsset: AssetMediaResponseDto; let ratingAsset: AssetMediaResponseDto;
let facesAsset: AssetMediaResponseDto;
const setupTests = async () => { const setupTests = async () => {
await utils.resetDatabase(); await utils.resetDatabase();
@@ -236,7 +235,7 @@ describe('/asset', () => {
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) }); await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
// asset faces // asset faces
facesAsset = await utils.createAsset(admin.accessToken, { const facesAsset = await utils.createAsset(admin.accessToken, {
assetData: { assetData: {
filename: 'portrait.jpg', filename: 'portrait.jpg',
bytes: await readFile(facesAssetFilepath), bytes: await readFile(facesAssetFilepath),

View File

@@ -1,6 +1,6 @@
ARG DEVICE=cpu ARG DEVICE=cpu
FROM python:3.11-bookworm@sha256:157a371e60389919fe4a72dff71ce86eaa5234f59114c23b0b346d0d02c74d39 AS builder-cpu FROM python:3.11-bookworm@sha256:3cdce69fd5663ca47c420ec4d4df8e3545519a4030372f7d2064fb1be2279844 AS builder-cpu
FROM builder-cpu AS builder-openvino FROM builder-cpu AS builder-openvino
@@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv
COPY poetry.lock pyproject.toml ./ COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
FROM python:3.11-slim-bookworm@sha256:669bbd08353610485a94d5d0c976b4b6498c55280fe42c00f7581f85ee9f3121 AS prod-cpu FROM python:3.11-slim-bookworm@sha256:5501a4fe605abe24de87c2f3d6cf9fd760354416a0cad0296cf284fddcdca9e2 AS prod-cpu
FROM prod-cpu AS prod-openvino FROM prod-cpu AS prod-openvino

View File

@@ -1,4 +1,4 @@
FROM mambaorg/micromamba:bookworm-slim@sha256:5f32c5742e2248f2ca07ccae6861371321aba37372bf8e1a80d6f728f1ab4418 AS builder FROM mambaorg/micromamba:bookworm-slim@sha256:e3797091302382ea841498bc93a7b0a50f7c1448333d5e946d2d1608d0c5f43d AS builder
ENV TRANSFORMERS_CACHE=/cache \ ENV TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \

View File

@@ -2,13 +2,13 @@
[[package]] [[package]]
name = "aiocache" name = "aiocache"
version = "0.12.2" version = "0.12.3"
description = "multi backend asyncio cache" description = "multi backend asyncio cache"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "aiocache-0.12.2-py2.py3-none-any.whl", hash = "sha256:9b6fa30634ab0bfc3ecc44928a91ff07c6ea16d27d55469636b296ebc6eb5918"}, {file = "aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d"},
{file = "aiocache-0.12.2.tar.gz", hash = "sha256:b41c9a145b050a5dcbae1599f847db6dd445193b1f3bd172d8e0fe0cb9e96684"}, {file = "aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713"},
] ]
[package.extras] [package.extras]
@@ -680,13 +680,13 @@ test = ["pytest (>=6)"]
[[package]] [[package]]
name = "fastapi-slim" name = "fastapi-slim"
version = "0.114.2" version = "0.115.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "fastapi_slim-0.114.2-py3-none-any.whl", hash = "sha256:52ae76c53a30ad0fa96beb84c1bf4bef9c72e88c2f7c0473e836f01d7ac3ca6b"}, {file = "fastapi_slim-0.115.0-py3-none-any.whl", hash = "sha256:27ab44da95b622e68be7a19f06df1960a320b9d94e689b0adfc055bb26ee9be7"},
{file = "fastapi_slim-0.114.2.tar.gz", hash = "sha256:76d0a450826fb0fa740268be55ef04c44807da87a94fbbf5f16338b5a4a2d321"}, {file = "fastapi_slim-0.115.0.tar.gz", hash = "sha256:b4b962ca2aa0a31010dafdad3d4da99d368a5591223304c6fb385712fad7feb6"},
] ]
[package.dependencies] [package.dependencies]
@@ -1237,13 +1237,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "huggingface-hub" name = "huggingface-hub"
version = "0.25.0" version = "0.25.1"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false optional = false
python-versions = ">=3.8.0" python-versions = ">=3.8.0"
files = [ files = [
{file = "huggingface_hub-0.25.0-py3-none-any.whl", hash = "sha256:e2f357b35d72d5012cfd127108c4e14abcd61ba4ebc90a5a374dc2456cb34e12"}, {file = "huggingface_hub-0.25.1-py3-none-any.whl", hash = "sha256:a5158ded931b3188f54ea9028097312cb0acd50bffaaa2612014c3c526b44972"},
{file = "huggingface_hub-0.25.0.tar.gz", hash = "sha256:fb5fbe6c12fcd99d187ec7db95db9110fb1a20505f23040a5449a717c1a0db4d"}, {file = "huggingface_hub-0.25.1.tar.gz", hash = "sha256:9ff7cb327343211fbd06e2b149b8f362fd1e389454f3f14c6db75a4999ee20ff"},
] ]
[package.dependencies] [package.dependencies]
@@ -1531,13 +1531,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]] [[package]]
name = "locust" name = "locust"
version = "2.31.6" version = "2.31.8"
description = "Developer-friendly load testing framework" description = "Developer-friendly load testing framework"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "locust-2.31.6-py3-none-any.whl", hash = "sha256:004c963c7a588dc15d57d710cdc6a262d85b57936d7fad3c38ac0657aa98fc3b"}, {file = "locust-2.31.8-py3-none-any.whl", hash = "sha256:4194e3d4a0472f1206c51532ed527017f3da1a7d1037ca4b2f0735d5dcd2f78f"},
{file = "locust-2.31.6.tar.gz", hash = "sha256:03b6da0491d6a0b905692d9ac128d9deec403f40dc605c481a90dbab5126318c"}, {file = "locust-2.31.8.tar.gz", hash = "sha256:b240c0d3e1724317d9211e81e99fbe42a3469071ef4d34d2ae6a727776d56377"},
] ]
[package.dependencies] [package.dependencies]
@@ -2037,22 +2037,22 @@ reference = "cuda12"
[[package]] [[package]]
name = "onnxruntime-openvino" name = "onnxruntime-openvino"
version = "1.18.0" version = "1.19.0"
description = "ONNX Runtime is a runtime accelerator for Machine Learning models" description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "onnxruntime_openvino-1.18.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:565b874d21bcd48126da7d62f57db019f5ec0e1f82ae9b0740afa2ad91f8d331"}, {file = "onnxruntime_openvino-1.19.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8c5658da819b26d9f35f95204e1bdfb74a100a7533e74edab3af6316c1e316e8"},
{file = "onnxruntime_openvino-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:7f1931060f710a6c8e32121bb73044c4772ef5925802fc8776d3fe1e87ab3f75"}, {file = "onnxruntime_openvino-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb8de2a60cf78db6e201b0a489479995d166938e9c53b01ff342dc7f5f8251ff"},
{file = "onnxruntime_openvino-1.18.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb1723d386f70a8e26398d983ebe35d2c25ba56e9cdb382670ebbf1f5139f8ba"}, {file = "onnxruntime_openvino-1.19.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f3a0b954026286421b3a769c746c403e8f141f3887d1dd601beb7c4dbf77488a"},
{file = "onnxruntime_openvino-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:874a1e263dd86674593e5a879257650b06a8609c4d5768c3d8ed8dc4ae874b9c"}, {file = "onnxruntime_openvino-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:12330922ecdb694ea28dbdcf08c172e47a5a84fee603040691341336ee3e42bc"},
{file = "onnxruntime_openvino-1.18.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:597eb18f3de7ead69b08a242d74c4573b28bbfba40ca2a1a40f75bf7a834808e"}, {file = "onnxruntime_openvino-1.19.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:be00502b1a46ba1891cbe49049033745f71c0b99df6d24b979f5b4084b9567d0"},
] ]
[package.dependencies] [package.dependencies]
coloredlogs = "*" coloredlogs = "*"
flatbuffers = "*" flatbuffers = "*"
numpy = ">=1.26.4" numpy = ">=1.21.6"
packaging = "*" packaging = "*"
protobuf = "*" protobuf = "*"
sympy = "*" sympy = "*"
@@ -2576,18 +2576,15 @@ cli = ["click (>=5.0)"]
[[package]] [[package]]
name = "python-multipart" name = "python-multipart"
version = "0.0.9" version = "0.0.10"
description = "A streaming multipart parser for Python" description = "A streaming multipart parser for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, {file = "python_multipart-0.0.10-py3-none-any.whl", hash = "sha256:2b06ad9e8d50c7a8db80e3b56dab590137b323410605af2be20d62a5f1ba1dc8"},
{file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, {file = "python_multipart-0.0.10.tar.gz", hash = "sha256:46eb3c6ce6fdda5fb1a03c7e11d490e407c6930a2703fe7aef4da71c374688fa"},
] ]
[package.extras]
dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"]
[[package]] [[package]]
name = "pywin32" name = "pywin32"
version = "306" version = "306"
@@ -2834,29 +2831,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.6.6" version = "0.6.8"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "ruff-0.6.6-py3-none-linux_armv6l.whl", hash = "sha256:f5bc5398457484fc0374425b43b030e4668ed4d2da8ee7fdda0e926c9f11ccfb"}, {file = "ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2"},
{file = "ruff-0.6.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:515a698254c9c47bb84335281a170213b3ee5eb47feebe903e1be10087a167ce"}, {file = "ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c"},
{file = "ruff-0.6.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6bb1b4995775f1837ab70f26698dd73852bbb82e8f70b175d2713c0354fe9182"}, {file = "ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69c546f412dfae8bb9cc4f27f0e45cdd554e42fecbb34f03312b93368e1cd0a6"}, {file = "ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59627e97364329e4eae7d86fa7980c10e2b129e2293d25c478ebcb861b3e3fd6"}, {file = "ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94c3f78c3d32190aafbb6bc5410c96cfed0a88aadb49c3f852bbc2aa9783a7d8"}, {file = "ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:704da526c1e137f38c8a067a4a975fe6834b9f8ba7dbc5fd7503d58148851b8f"}, {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efeede5815a24104579a0f6320660536c5ffc1c91ae94f8c65659af915fb9de9"}, {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e368aef0cc02ca3593eae2fb8186b81c9c2b3f39acaaa1108eb6b4d04617e61f"}, {file = "ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2653fc3b2a9315bd809725c88dd2446550099728d077a04191febb5ea79a4f79"}, {file = "ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0"},
{file = "ruff-0.6.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bb858cd9ce2d062503337c5b9784d7b583bcf9d1a43c4df6ccb5eab774fbafcb"}, {file = "ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750"},
{file = "ruff-0.6.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:488f8e15c01ea9afb8c0ba35d55bd951f484d0c1b7c5fd746ce3c47ccdedce68"}, {file = "ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce"},
{file = "ruff-0.6.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:aefb0bd15f1cfa4c9c227b6120573bb3d6c4ee3b29fb54a5ad58f03859bc43c6"}, {file = "ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa"},
{file = "ruff-0.6.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a4c0698cc780bcb2c61496cbd56b6a3ac0ad858c966652f7dbf4ceb029252fbe"}, {file = "ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44"},
{file = "ruff-0.6.6-py3-none-win32.whl", hash = "sha256:aadf81ddc8ab5b62da7aae78a91ec933cbae9f8f1663ec0325dae2c364e4ad84"}, {file = "ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a"},
{file = "ruff-0.6.6-py3-none-win_amd64.whl", hash = "sha256:0adb801771bc1f1b8cf4e0a6fdc30776e7c1894810ff3b344e50da82ef50eeb1"}, {file = "ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263"},
{file = "ruff-0.6.6-py3-none-win_arm64.whl", hash = "sha256:4b4d32c137bc781c298964dd4e52f07d6f7d57c03eae97a72d97856844aa510a"}, {file = "ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc"},
{file = "ruff-0.6.6.tar.gz", hash = "sha256:0fc030b6fd14814d69ac0196396f6761921bd20831725c7361e1b8100b818034"}, {file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"},
] ]
[[package]] [[package]]

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "machine-learning" name = "machine-learning"
version = "1.116.0" version = "1.116.2"
description = "" description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"] authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md" readme = "README.md"

View File

@@ -64,19 +64,19 @@ custom_lint:
allowed: allowed:
# required / wanted # required / wanted
- lib/entities/*.entity.dart - lib/entities/*.entity.dart
- lib/repositories/{album,asset,backup,exif_info,user}.repository.dart - lib/repositories/{album,asset,backup,database,etag,exif_info,user}.repository.dart
# acceptable exceptions for the time being # acceptable exceptions for the time being (until Isar is fully replaced)
- integration_test/test_utils/general_helper.dart - integration_test/test_utils/general_helper.dart
- lib/main.dart - lib/main.dart
- lib/routing/router.dart
- lib/utils/{db,migration,renderlist_generator}.dart
- test/**.dart
# refactor to make the providers and services testable
- lib/pages/common/album_asset_selection.page.dart - lib/pages/common/album_asset_selection.page.dart
- lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart - lib/routing/router.dart
- lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,backup/manual_upload,search/all_motion_photos,search/recently_added_asset}.provider.dart - lib/services/immich_logger.service.dart # not really a service... more a util
- lib/services/{asset,background,backup,immich_logger,sync}.service.dart - lib/utils/{db,migration,renderlist_generator}.dart
- lib/widgets/asset_grid/asset_grid_data_structure.dart - lib/widgets/asset_grid/asset_grid_data_structure.dart
- test/**.dart
# refactor the remaining providers
- lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart
- lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,search/all_motion_photos,search/recently_added_asset}.provider.dart
- import_rule_openapi: - import_rule_openapi:
message: openapi must only be used through ApiRepositories message: openapi must only be used through ApiRepositories

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" => 160, "android.injected.version.code" => 161,
"android.injected.version.name" => "1.116.0", "android.injected.version.name" => "1.116.2",
} }
) )
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')

View File

@@ -588,5 +588,16 @@
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"viewer_remove_from_stack": "Remove from Stack", "viewer_remove_from_stack": "Remove from Stack",
"viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_stack_use_as_main_asset": "Use as Main Asset",
"viewer_unstack": "Un-Stack" "viewer_unstack": "Un-Stack",
} "downloading_media": "Downloading media",
"download_finished": "Download finished",
"download_filename": "file: {}",
"downloading": "Downloading...",
"download_complete": "Download complete",
"download_failed": "Download failed",
"download_canceled": "Download canceled",
"download_paused": "Download paused",
"download_enqueue": "Download enqueued",
"download_notfound": "Download not found",
"download_waiting_to_retry": "Waiting to retry"
}

View File

@@ -186,10 +186,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: lints name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "5.0.0"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@@ -367,4 +367,4 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.4.0 <4.0.0" dart: ">=3.5.0 <4.0.0"

View File

@@ -11,4 +11,4 @@ dependencies:
glob: ^2.1.2 glob: ^2.1.2
dev_dependencies: dev_dependencies:
lints: ^4.0.0 lints: ^5.0.0

View File

@@ -1,4 +1,6 @@
PODS: PODS:
- background_downloader (0.0.1):
- Flutter
- connectivity_plus (0.0.1): - connectivity_plus (0.0.1):
- Flutter - Flutter
- ReachabilitySwift - ReachabilitySwift
@@ -99,6 +101,7 @@ PODS:
- Flutter - Flutter
DEPENDENCIES: DEPENDENCIES:
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
@@ -137,6 +140,8 @@ SPEC REPOS:
- Toast - Toast
EXTERNAL SOURCES: EXTERNAL SOURCES:
background_downloader:
:path: ".symlinks/plugins/background_downloader/ios"
connectivity_plus: connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios" :path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus: device_info_plus:
@@ -189,6 +194,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/wakelock_plus/ios" :path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
background_downloader: 9f788ffc5de45acf87d6380e91ca0841066c18cf
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c

View File

@@ -401,7 +401,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 = 176; CURRENT_PROJECT_VERSION = 177;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 176; CURRENT_PROJECT_VERSION = 177;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -571,7 +571,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 176; CURRENT_PROJECT_VERSION = 177;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -58,11 +58,11 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.116.0</string> <string>1.116.1</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>176</string> <string>177</string>
<key>FLTEnableImpeller</key> <key>FLTEnableImpeller</key>
<true/> <true/>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -0,0 +1 @@
{"client":{"name":"basic","version":0,"file-system":"device-agnostic","perform-ownership-analysis":"no"},"targets":{"":["<all>"]},"commands":{"<all>":{"tool":"phony","inputs":["<WorkspaceHeaderMapVFSFilesWritten>"],"outputs":["<all>"]},"P0:::Gate WorkspaceHeaderMapVFSFilesWritten":{"tool":"phony","inputs":[],"outputs":["<WorkspaceHeaderMapVFSFilesWritten>"]}}}

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Release" desc "iOS Release"
lane :release do lane :release do
increment_version_number( increment_version_number(
version_number: "1.116.0" version_number: "1.116.2"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,

View File

@@ -70,19 +70,6 @@ extension AssetListExtension on Iterable<Asset> {
} }
return this; return this;
} }
/// Filters out offline assets and returns those that are still accessible by the Immich server
/// TODO: isOffline is removed from Immich, so this method is not useful anymore
Iterable<Asset> nonOfflineOnly({
void Function()? errorCallback,
}) {
final bool onlyLive = every((e) => false);
if (!onlyLive) {
if (errorCallback != null) errorCallback();
return where((a) => false);
}
return this;
}
} }
extension SortedByProperty<T> on Iterable<T> { extension SortedByProperty<T> on Iterable<T> {

View File

@@ -1,21 +1,43 @@
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
abstract interface class IAlbumRepository { abstract interface class IAlbumRepository implements IDatabaseRepository {
Future<int> count({bool? local});
Future<Album> create(Album album); Future<Album> create(Album album);
Future<Album?> getById(int id);
Future<Album?> get(int id);
Future<Album?> getByName( Future<Album?> getByName(
String name, { String name, {
bool? shared, bool? shared,
bool? remote, bool? remote,
}); });
Future<List<Album>> getAll({
bool? shared,
bool? remote,
int? ownerId,
AlbumSort? sortBy,
});
Future<Album> update(Album album); Future<Album> update(Album album);
Future<void> delete(int albumId); Future<void> delete(int albumId);
Future<List<Album>> getAll({bool? shared});
Future<void> deleteAllLocal();
Future<int> count({bool? local});
Future<void> addUsers(Album album, List<User> users);
Future<void> removeUsers(Album album, List<User> users); Future<void> removeUsers(Album album, List<User> users);
Future<void> addAssets(Album album, List<Asset> assets); Future<void> addAssets(Album album, List<Asset> assets);
Future<void> removeAssets(Album album, List<Asset> assets); Future<void> removeAssets(Album album, List<Asset> assets);
Future<Album> recalculateMetadata(Album album); Future<Album> recalculateMetadata(Album album);
} }
enum AlbumSort { remoteId, localId }

View File

@@ -1,27 +1,62 @@
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/device_asset.entity.dart'; import 'package:immich_mobile/entities/device_asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/database.interface.dart';
abstract interface class IAssetRepository { abstract interface class IAssetRepository implements IDatabaseRepository {
Future<Asset?> getByRemoteId(String id); Future<Asset?> getByRemoteId(String id);
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids);
Future<List<Asset>> getByAlbum(Album album, {User? notOwnedBy}); Future<Asset?> getByOwnerIdChecksum(int ownerId, String checksum);
Future<void> deleteById(List<int> ids);
Future<List<Asset>> getAllByRemoteId(
Iterable<String> ids, {
AssetState? state,
});
Future<List<Asset?>> getAllByOwnerIdChecksum(
List<int> ids,
List<String> checksums,
);
Future<List<Asset>> getAll({ Future<List<Asset>> getAll({
required int ownerId, required int ownerId,
bool? remote, AssetState? state,
int limit = 100, AssetSort? sortBy,
int? limit,
}); });
Future<List<Asset>> getAllLocal();
Future<List<Asset>> getByAlbum(
Album album, {
Iterable<int> notOwnedBy = const [],
int? ownerId,
AssetState? state,
AssetSort? sortBy,
});
Future<Asset> update(Asset asset);
Future<List<Asset>> updateAll(List<Asset> assets); Future<List<Asset>> updateAll(List<Asset> assets);
Future<void> deleteAllByRemoteId(List<String> ids, {AssetState? state});
Future<void> deleteById(List<int> ids);
Future<List<Asset>> getMatches({ Future<List<Asset>> getMatches({
required List<Asset> assets, required List<Asset> assets,
required int ownerId, required int ownerId,
bool? remote, AssetState? state,
int limit = 100, int limit = 100,
}); });
Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids); Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids);
Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets); Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets);
Future<void> upsertDuplicatedAssets(Iterable<String> duplicatedAssets);
Future<List<String>> getAllDuplicatedAssetIds();
} }
enum AssetSort { checksum, ownerIdChecksum }

View File

@@ -4,4 +4,7 @@ abstract interface class IAssetMediaRepository {
Future<List<String>> deleteAll(List<String> ids); Future<List<String>> deleteAll(List<String> ids);
Future<Asset?> get(String id); Future<Asset?> get(String id);
/// Obtaining the correct original filename of the asset
Future<String?> getOriginalFilename(String id);
} }

View File

@@ -1,5 +1,16 @@
import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
abstract interface class IBackupRepository implements IDatabaseRepository {
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort});
abstract interface class IBackupRepository {
Future<List<String>> getIdsBySelection(BackupSelection backup); Future<List<String>> getIdsBySelection(BackupSelection backup);
Future<List<BackupAlbum>> getAllBySelection(BackupSelection backup);
Future<void> updateAll(List<BackupAlbum> backupAlbums);
Future<void> deleteAll(List<int> ids);
} }
enum BackupAlbumSort { id }

View File

@@ -0,0 +1,3 @@
abstract interface class IDatabaseRepository {
Future<T> transaction<T>(Future<T> Function() callback);
}

View File

@@ -0,0 +1,14 @@
import 'package:background_downloader/background_downloader.dart';
abstract interface class IDownloadRepository {
void Function(TaskStatusUpdate)? onImageDownloadStatus;
void Function(TaskStatusUpdate)? onVideoDownloadStatus;
void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
void Function(TaskProgressUpdate)? onTaskProgress;
Future<List<TaskRecord>> getLiveVideoTasks();
Future<bool> download(DownloadTask task);
Future<bool> cancel(String id);
Future<void> deleteAllTrackingRecords();
Future<void> deleteRecordsWithIds(List<String> id);
}

View File

@@ -0,0 +1,14 @@
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
abstract interface class IETagRepository implements IDatabaseRepository {
Future<ETag?> get(int id);
Future<ETag?> getById(String id);
Future<List<String>> getAllIds();
Future<void> upsertAll(List<ETag> etags);
Future<void> deleteByIds(List<String> ids);
}

View File

@@ -1,9 +1,12 @@
import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
abstract interface class IExifInfoRepository { abstract interface class IExifInfoRepository implements IDatabaseRepository {
Future<ExifInfo?> get(int id); Future<ExifInfo?> get(int id);
Future<ExifInfo> update(ExifInfo exifInfo); Future<ExifInfo> update(ExifInfo exifInfo);
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos);
Future<void> delete(int id); Future<void> delete(int id);
} }

View File

@@ -1,8 +1,23 @@
import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
abstract interface class IUserRepository { abstract interface class IUserRepository implements IDatabaseRepository {
Future<List<User>> getByIds(List<String> ids);
Future<User?> get(String id); Future<User?> get(String id);
Future<List<User>> getAll({bool self = true});
Future<List<User>> getByIds(List<String> ids);
Future<List<User>> getAll({bool self = true, UserSort? sortBy});
/// Returns all users whose assets can be accessed (self+partners)
Future<List<User>> getAllAccessible();
Future<List<User>> upsertAll(List<User> users);
Future<User> update(User user); Future<User> update(User user);
Future<void> deleteById(List<int> ids);
Future<User> me();
} }
enum UserSort { id }

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@@ -9,6 +10,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/utils/download.dart';
import 'package:timezone/data/latest.dart'; import 'package:timezone/data/latest.dart';
import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/background.service.dart';
@@ -72,7 +74,6 @@ Future<void> initApp() async {
var log = Logger("ImmichErrorLogger"); var log = Logger("ImmichErrorLogger");
FlutterError.onError = (details) { FlutterError.onError = (details) {
debugPrint("FlutterError - Catch all: $details");
FlutterError.presentError(details); FlutterError.presentError(details);
log.severe( log.severe(
'FlutterError - Catch all', 'FlutterError - Catch all',
@@ -82,11 +83,29 @@ Future<void> initApp() async {
}; };
PlatformDispatcher.instance.onError = (error, stack) { PlatformDispatcher.instance.onError = (error, stack) {
debugPrint("FlutterError - Catch all: $error");
log.severe('PlatformDispatcher - Catch all', error, stack); log.severe('PlatformDispatcher - Catch all', error, stack);
return true; return true;
}; };
initializeTimeZones(); initializeTimeZones();
FileDownloader().configureNotification(
running: TaskNotification(
'downloading_media'.tr(),
'file: {filename}',
),
complete: TaskNotification(
'download_finished'.tr(),
'file: {filename}',
),
progressBar: true,
);
FileDownloader().trackTasksInGroup(
downloadGroupLivePhoto,
markDownloadedComplete: false,
);
} }
Future<Isar> loadDb() async { Future<Isar> loadDb() async {
@@ -188,8 +207,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var router = ref.watch(appRouterProvider); final router = ref.watch(appRouterProvider);
var immichTheme = ref.watch(immichThemeProvider); final immichTheme = ref.watch(immichThemeProvider);
return MaterialApp( return MaterialApp(
localizationsDelegates: context.localizationDelegates, localizationsDelegates: context.localizationDelegates,

View File

@@ -1,55 +0,0 @@
import 'dart:convert';
enum DownloadAssetStatus { idle, loading, success, error }
class AssetViewerPageState {
// enum
final DownloadAssetStatus downloadAssetStatus;
AssetViewerPageState({
required this.downloadAssetStatus,
});
AssetViewerPageState copyWith({
DownloadAssetStatus? downloadAssetStatus,
}) {
return AssetViewerPageState(
downloadAssetStatus: downloadAssetStatus ?? this.downloadAssetStatus,
);
}
Map<String, dynamic> toMap() {
final result = <String, dynamic>{};
result.addAll({'downloadAssetStatus': downloadAssetStatus.index});
return result;
}
factory AssetViewerPageState.fromMap(Map<String, dynamic> map) {
return AssetViewerPageState(
downloadAssetStatus:
DownloadAssetStatus.values[map['downloadAssetStatus'] ?? 0],
);
}
String toJson() => json.encode(toMap());
factory AssetViewerPageState.fromJson(String source) =>
AssetViewerPageState.fromMap(json.decode(source));
@override
String toString() =>
'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is AssetViewerPageState &&
other.downloadAssetStatus == downloadAssetStatus;
}
@override
int get hashCode => downloadAssetStatus.hashCode;
}

View File

@@ -0,0 +1,109 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
import 'package:background_downloader/background_downloader.dart';
import 'package:collection/collection.dart';
class DownloadInfo {
final String fileName;
final double progress;
// enum
final TaskStatus status;
DownloadInfo({
required this.fileName,
required this.progress,
required this.status,
});
DownloadInfo copyWith({
String? fileName,
double? progress,
TaskStatus? status,
}) {
return DownloadInfo(
fileName: fileName ?? this.fileName,
progress: progress ?? this.progress,
status: status ?? this.status,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'fileName': fileName,
'progress': progress,
'status': status.index,
};
}
factory DownloadInfo.fromMap(Map<String, dynamic> map) {
return DownloadInfo(
fileName: map['fileName'] as String,
progress: map['progress'] as double,
status: TaskStatus.values[map['status'] as int],
);
}
String toJson() => json.encode(toMap());
factory DownloadInfo.fromJson(String source) =>
DownloadInfo.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() =>
'DownloadInfo(fileName: $fileName, progress: $progress, status: $status)';
@override
bool operator ==(covariant DownloadInfo other) {
if (identical(this, other)) return true;
return other.fileName == fileName &&
other.progress == progress &&
other.status == status;
}
@override
int get hashCode => fileName.hashCode ^ progress.hashCode ^ status.hashCode;
}
class DownloadState {
// enum
final TaskStatus downloadStatus;
final Map<String, DownloadInfo> taskProgress;
final bool showProgress;
DownloadState({
required this.downloadStatus,
required this.taskProgress,
required this.showProgress,
});
DownloadState copyWith({
TaskStatus? downloadStatus,
Map<String, DownloadInfo>? taskProgress,
bool? showProgress,
}) {
return DownloadState(
downloadStatus: downloadStatus ?? this.downloadStatus,
taskProgress: taskProgress ?? this.taskProgress,
showProgress: showProgress ?? this.showProgress,
);
}
@override
String toString() =>
'DownloadState(downloadStatus: $downloadStatus, taskProgress: $taskProgress, showProgress: $showProgress)';
@override
bool operator ==(covariant DownloadState other) {
if (identical(this, other)) return true;
final mapEquals = const DeepCollectionEquality().equals;
return other.downloadStatus == downloadStatus &&
mapEquals(other.taskProgress, taskProgress) &&
other.showProgress == showProgress;
}
@override
int get hashCode =>
downloadStatus.hashCode ^ taskProgress.hashCode ^ showProgress.hashCode;
}

View File

@@ -0,0 +1,60 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';
enum LivePhotosPart {
video,
image,
}
class LivePhotosMetadata {
// enum
LivePhotosPart part;
String id;
LivePhotosMetadata({
required this.part,
required this.id,
});
LivePhotosMetadata copyWith({
LivePhotosPart? part,
String? id,
}) {
return LivePhotosMetadata(
part: part ?? this.part,
id: id ?? this.id,
);
}
Map<String, dynamic> toMap() {
return <String, dynamic>{
'part': part.index,
'id': id,
};
}
factory LivePhotosMetadata.fromMap(Map<String, dynamic> map) {
return LivePhotosMetadata(
part: LivePhotosPart.values[map['part'] as int],
id: map['id'] as String,
);
}
String toJson() => json.encode(toMap());
factory LivePhotosMetadata.fromJson(String source) =>
LivePhotosMetadata.fromMap(json.decode(source) as Map<String, dynamic>);
@override
String toString() => 'LivePhotosMetadata(part: $part, id: $id)';
@override
bool operator ==(covariant LivePhotosMetadata other) {
if (identical(this, other)) return true;
return other.part == part && other.id == id;
}
@override
int get hashCode => part.hashCode ^ id.hashCode;
}

View File

@@ -0,0 +1,150 @@
import 'package:background_downloader/background_downloader.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
class DownloadPanel extends ConsumerWidget {
const DownloadPanel({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final showProgress = ref.watch(
downloadStateProvider.select((state) => state.showProgress),
);
final tasks = ref
.watch(
downloadStateProvider.select((state) => state.taskProgress),
)
.entries
.toList();
onCancelDownload(String id) {
ref.watch(downloadStateProvider.notifier).cancelDownload(id);
}
return Positioned(
bottom: 140,
left: 16,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: showProgress
? ConstrainedBox(
constraints:
BoxConstraints.loose(Size(context.width - 32, 300)),
child: ListView.builder(
shrinkWrap: true,
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return DownloadTaskTile(
progress: task.value.progress,
fileName: task.value.fileName,
status: task.value.status,
onCancelDownload: () => onCancelDownload(task.key),
);
},
),
)
: const SizedBox.shrink(key: ValueKey('no_progress')),
),
);
}
}
class DownloadTaskTile extends StatelessWidget {
final double progress;
final String fileName;
final TaskStatus status;
final VoidCallback onCancelDownload;
const DownloadTaskTile({
super.key,
required this.progress,
required this.fileName,
required this.status,
required this.onCancelDownload,
});
@override
Widget build(BuildContext context) {
final progressPercent = (progress * 100).round();
getStatusText() {
switch (status) {
case TaskStatus.running:
return 'downloading'.tr();
case TaskStatus.complete:
return 'download_complete'.tr();
case TaskStatus.failed:
return 'download_failed'.tr();
case TaskStatus.canceled:
return 'download_canceled'.tr();
case TaskStatus.paused:
return 'download_paused'.tr();
case TaskStatus.enqueued:
return 'download_enqueue'.tr();
case TaskStatus.notFound:
return 'download_notfound'.tr();
case TaskStatus.waitingToRetry:
return 'download_waiting_to_retry'.tr();
}
}
return SizedBox(
key: const ValueKey('download_progress'),
width: MediaQuery.of(context).size.width - 32,
child: Card(
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: ListTile(
minVerticalPadding: 18,
leading: const Icon(Icons.video_file_outlined),
title: Text(
getStatusText(),
style: context.textTheme.labelLarge,
),
trailing: IconButton(
icon: Icon(Icons.close, color: context.colorScheme.onError),
onPressed: onCancelDownload,
style: ElevatedButton.styleFrom(
backgroundColor: context.colorScheme.error.withAlpha(200),
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fileName,
style: context.textTheme.labelMedium,
),
Row(
children: [
Expanded(
child: LinearProgressIndicator(
minHeight: 8.0,
value: progress,
borderRadius:
const BorderRadius.all(Radius.circular(10.0)),
),
),
const SizedBox(width: 8),
Text(
'$progressPercent%',
style: context.textTheme.labelSmall,
),
],
),
],
),
),
),
);
}
}

View File

@@ -11,6 +11,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/pages/common/download_panel.dart';
import 'package:immich_mobile/pages/common/video_viewer.page.dart'; import 'package:immich_mobile/pages/common/video_viewer.page.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
@@ -421,6 +422,7 @@ class GalleryViewerPage extends HookConsumerWidget {
], ],
), ),
), ),
const DownloadPanel(),
], ],
), ),
), ),

View File

@@ -275,28 +275,14 @@ class AssetNotifier extends StateNotifier<bool> {
return isSuccess ? remote.toList() : []; return isSuccess ? remote.toList() : [];
} }
Future<void> toggleFavorite(List<Asset> assets, [bool? status]) async { Future<void> toggleFavorite(List<Asset> assets, [bool? status]) {
status ??= !assets.every((a) => a.isFavorite); status ??= !assets.every((a) => a.isFavorite);
final newAssets = await _assetService.changeFavoriteStatus(assets, status); return _assetService.changeFavoriteStatus(assets, status);
for (Asset? newAsset in newAssets) {
if (newAsset == null) {
log.severe("Change favorite status failed for asset");
continue;
}
}
} }
Future<void> toggleArchive(List<Asset> assets, [bool? status]) async { Future<void> toggleArchive(List<Asset> assets, [bool? status]) {
status ??= !assets.every((a) => a.isArchived); status ??= !assets.every((a) => a.isArchived);
final newAssets = await _assetService.changeArchiveStatus(assets, status); return _assetService.changeArchiveStatus(assets, status);
int i = 0;
for (Asset oldAsset in assets) {
final newAsset = newAssets[i++];
if (newAsset == null) {
log.severe("Change archive status failed for asset ${oldAsset.id}");
continue;
}
}
} }
} }

View File

@@ -0,0 +1,191 @@
import 'package:background_downloader/background_downloader.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/download/download_state.model.dart';
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
import 'package:immich_mobile/services/download.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/services/share.service.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/share_dialog.dart';
class DownloadStateNotifier extends StateNotifier<DownloadState> {
final DownloadService _downloadService;
final ShareService _shareService;
DownloadStateNotifier(
this._downloadService,
this._shareService,
) : super(
DownloadState(
downloadStatus: TaskStatus.complete,
showProgress: false,
taskProgress: <String, DownloadInfo>{},
),
) {
_downloadService.onImageDownloadStatus = _downloadImageCallback;
_downloadService.onVideoDownloadStatus = _downloadVideoCallback;
_downloadService.onLivePhotoDownloadStatus = _downloadLivePhotoCallback;
_downloadService.onTaskProgress = _taskProgressCallback;
}
void _updateDownloadStatus(String taskId, TaskStatus status) {
if (status == TaskStatus.canceled) {
return;
}
state = state.copyWith(
taskProgress: <String, DownloadInfo>{}
..addAll(state.taskProgress)
..addAll({
taskId: DownloadInfo(
progress: state.taskProgress[taskId]?.progress ?? 0,
fileName: state.taskProgress[taskId]?.fileName ?? '',
status: status,
),
}),
);
}
// Download live photo callback
void _downloadLivePhotoCallback(TaskStatusUpdate update) {
_updateDownloadStatus(update.task.taskId, update.status);
switch (update.status) {
case TaskStatus.complete:
if (update.task.metaData.isEmpty) {
return;
}
final livePhotosId =
LivePhotosMetadata.fromJson(update.task.metaData).id;
_downloadService.saveLivePhotos(update.task, livePhotosId);
_onDownloadComplete(update.task.taskId);
break;
default:
break;
}
}
// Download image callback
void _downloadImageCallback(TaskStatusUpdate update) {
_updateDownloadStatus(update.task.taskId, update.status);
switch (update.status) {
case TaskStatus.complete:
_downloadService.saveImage(update.task);
_onDownloadComplete(update.task.taskId);
break;
default:
break;
}
}
// Download video callback
void _downloadVideoCallback(TaskStatusUpdate update) {
_updateDownloadStatus(update.task.taskId, update.status);
switch (update.status) {
case TaskStatus.complete:
_downloadService.saveVideo(update.task);
_onDownloadComplete(update.task.taskId);
break;
default:
break;
}
}
void _taskProgressCallback(TaskProgressUpdate update) {
// Ignore if the task is cancled or completed
if (update.progress == -2 || update.progress == -1) {
return;
}
state = state.copyWith(
showProgress: true,
taskProgress: <String, DownloadInfo>{}
..addAll(state.taskProgress)
..addAll({
update.task.taskId: DownloadInfo(
progress: update.progress,
fileName: update.task.filename,
status: TaskStatus.running,
),
}),
);
}
void _onDownloadComplete(String id) {
Future.delayed(const Duration(seconds: 2), () {
state = state.copyWith(
taskProgress: <String, DownloadInfo>{}
..addAll(state.taskProgress)
..remove(id),
);
if (state.taskProgress.isEmpty) {
state = state.copyWith(
showProgress: false,
);
}
});
}
void downloadAsset(Asset asset, BuildContext context) async {
await _downloadService.download(asset);
}
void cancelDownload(String id) async {
final isCanceled = await _downloadService.cancelDownload(id);
if (isCanceled) {
state = state.copyWith(
taskProgress: <String, DownloadInfo>{}
..addAll(state.taskProgress)
..remove(id),
);
}
if (state.taskProgress.isEmpty) {
state = state.copyWith(
showProgress: false,
);
}
}
void shareAsset(Asset asset, BuildContext context) async {
showDialog(
context: context,
builder: (BuildContext buildContext) {
_shareService.shareAsset(asset, context).then(
(bool status) {
if (!status) {
ImmichToast.show(
context: context,
msg: 'image_viewer_page_state_provider_share_error'.tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
}
buildContext.pop();
},
);
return const ShareDialog();
},
barrierDismissible: false,
);
}
}
final downloadStateProvider =
StateNotifierProvider<DownloadStateNotifier, DownloadState>(
((ref) => DownloadStateNotifier(
ref.watch(downloadServiceProvider),
ref.watch(shareServiceProvider),
)),
);

View File

@@ -1,99 +0,0 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/models/asset_viewer/asset_viewer_page_state.model.dart';
import 'package:immich_mobile/services/image_viewer.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/services/share.service.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/share_dialog.dart';
class ImageViewerStateNotifier extends StateNotifier<AssetViewerPageState> {
final ImageViewerService _imageViewerService;
final ShareService _shareService;
final AlbumService _albumService;
ImageViewerStateNotifier(
this._imageViewerService,
this._shareService,
this._albumService,
) : super(
AssetViewerPageState(
downloadAssetStatus: DownloadAssetStatus.idle,
),
);
void downloadAsset(Asset asset, BuildContext context) async {
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
ImmichToast.show(
context: context,
msg: 'download_started'.tr(),
toastType: ToastType.info,
gravity: ToastGravity.BOTTOM,
);
bool isSuccess = await _imageViewerService.downloadAsset(asset);
if (isSuccess) {
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.success);
ImmichToast.show(
context: context,
msg: Platform.isAndroid
? 'download_sucess_android'.tr()
: 'download_sucess'.tr(),
toastType: ToastType.success,
gravity: ToastGravity.BOTTOM,
);
_albumService.refreshDeviceAlbums();
} else {
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error);
ImmichToast.show(
context: context,
msg: 'download_error'.tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
}
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
}
void shareAsset(Asset asset, BuildContext context) async {
showDialog(
context: context,
builder: (BuildContext buildContext) {
_shareService.shareAsset(asset, context).then(
(bool status) {
if (!status) {
ImmichToast.show(
context: context,
msg: 'image_viewer_page_state_provider_share_error'.tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
}
buildContext.pop();
},
);
return const ShareDialog();
},
barrierDismissible: false,
);
}
}
final imageViewerStateProvider =
StateNotifierProvider<ImageViewerStateNotifier, AssetViewerPageState>(
((ref) => ImageViewerStateNotifier(
ref.watch(imageViewerServiceProvider),
ref.watch(shareServiceProvider),
ref.watch(albumServiceProvider),
)),
);

View File

@@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart'; import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/backup.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart'; import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart';
@@ -17,6 +18,7 @@ import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart'; import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/background.service.dart';
import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/backup.service.dart';
@@ -45,6 +47,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
this._db, this._db,
this._albumMediaRepository, this._albumMediaRepository,
this._fileMediaRepository, this._fileMediaRepository,
this._backupRepository,
this.ref, this.ref,
) : super( ) : super(
BackUpState( BackUpState(
@@ -95,6 +98,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
final Isar _db; final Isar _db;
final IAlbumMediaRepository _albumMediaRepository; final IAlbumMediaRepository _albumMediaRepository;
final IFileMediaRepository _fileMediaRepository; final IFileMediaRepository _fileMediaRepository;
final IBackupRepository _backupRepository;
final Ref ref; final Ref ref;
/// ///
@@ -255,9 +259,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
state = state.copyWith(availableAlbums: availableAlbums); state = state.copyWith(availableAlbums: availableAlbums);
final List<BackupAlbum> excludedBackupAlbums = final List<BackupAlbum> excludedBackupAlbums =
await _backupService.excludedAlbumsQuery().findAll(); await _backupRepository.getAllBySelection(BackupSelection.exclude);
final List<BackupAlbum> selectedBackupAlbums = final List<BackupAlbum> selectedBackupAlbums =
await _backupService.selectedAlbumsQuery().findAll(); await _backupRepository.getAllBySelection(BackupSelection.select);
final Set<AvailableAlbum> selectedAlbums = {}; final Set<AvailableAlbum> selectedAlbums = {};
for (final BackupAlbum ba in selectedBackupAlbums) { for (final BackupAlbum ba in selectedBackupAlbums) {
@@ -767,6 +771,7 @@ final backupProvider =
ref.watch(dbProvider), ref.watch(dbProvider),
ref.watch(albumMediaRepositoryProvider), ref.watch(albumMediaRepositoryProvider),
ref.watch(fileMediaRepositoryProvider), ref.watch(fileMediaRepositoryProvider),
ref.watch(backupRepositoryProvider),
ref, ref,
); );
}); });

View File

@@ -6,8 +6,10 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/background.service.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart';
@@ -25,7 +27,6 @@ import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/services/local_notification.service.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/backup_progress.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
@@ -36,6 +37,7 @@ final manualUploadProvider =
ref.watch(localNotificationService), ref.watch(localNotificationService),
ref.watch(backupProvider.notifier), ref.watch(backupProvider.notifier),
ref.watch(backupServiceProvider), ref.watch(backupServiceProvider),
ref.watch(backupRepositoryProvider),
ref, ref,
); );
}); });
@@ -45,12 +47,14 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
final LocalNotificationService _localNotificationService; final LocalNotificationService _localNotificationService;
final BackupNotifier _backupProvider; final BackupNotifier _backupProvider;
final BackupService _backupService; final BackupService _backupService;
final BackupRepository _backupRepository;
final Ref ref; final Ref ref;
ManualUploadNotifier( ManualUploadNotifier(
this._localNotificationService, this._localNotificationService,
this._backupProvider, this._backupProvider,
this._backupService, this._backupService,
this._backupRepository,
this.ref, this.ref,
) : super( ) : super(
ManualUploadState( ManualUploadState(
@@ -206,9 +210,9 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
} }
final selectedBackupAlbums = final selectedBackupAlbums =
_backupService.selectedAlbumsQuery().findAllSync(); await _backupRepository.getAllBySelection(BackupSelection.select);
final excludedBackupAlbums = final excludedBackupAlbums =
_backupService.excludedAlbumsQuery().findAllSync(); await _backupRepository.getAllBySelection(BackupSelection.exclude);
// Get candidates from selected albums and excluded albums // Get candidates from selected albums and excluded albums
Set<BackupCandidate> candidates = Set<BackupCandidate> candidates =

View File

@@ -3,14 +3,14 @@ import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/activity_api.interface.dart'; import 'package:immich_mobile/interfaces/activity_api.interface.dart';
import 'package:immich_mobile/models/activities/activity.model.dart'; import 'package:immich_mobile/models/activities/activity.model.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart'; import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
final activityApiRepositoryProvider = Provider( final activityApiRepositoryProvider = Provider(
(ref) => ActivityApiRepository(ref.watch(apiServiceProvider).activitiesApi), (ref) => ActivityApiRepository(ref.watch(apiServiceProvider).activitiesApi),
); );
class ActivityApiRepository extends BaseApiRepository class ActivityApiRepository extends ApiRepository
implements IActivityApiRepository { implements IActivityApiRepository {
final ActivitiesApi _api; final ActivitiesApi _api;

View File

@@ -4,32 +4,36 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/album.interface.dart'; import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
final albumRepositoryProvider = final albumRepositoryProvider =
Provider((ref) => AlbumRepository(ref.watch(dbProvider))); Provider((ref) => AlbumRepository(ref.watch(dbProvider)));
class AlbumRepository implements IAlbumRepository { class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
final Isar _db; AlbumRepository(super.db);
AlbumRepository(
this._db,
);
@override @override
Future<int> count({bool? local}) { Future<int> count({bool? local}) {
if (local == true) return _db.albums.where().localIdIsNotNull().count(); final baseQuery = db.albums.where();
if (local == false) return _db.albums.where().remoteIdIsNotNull().count(); final QueryBuilder<Album, Album, QAfterWhereClause> query;
return _db.albums.count(); switch (local) {
case null:
query = baseQuery.noOp();
case true:
query = baseQuery.localIdIsNotNull();
case false:
query = baseQuery.remoteIdIsNotNull();
}
return query.count();
} }
@override @override
Future<Album> create(Album album) => Future<Album> create(Album album) => txn(() => db.albums.store(album));
_db.writeTxn(() => _db.albums.store(album));
@override @override
Future<Album?> getByName(String name, {bool? shared, bool? remote}) { Future<Album?> getByName(String name, {bool? shared, bool? remote}) {
var query = _db.albums.filter().nameEqualTo(name); var query = db.albums.filter().nameEqualTo(name);
if (shared != null) { if (shared != null) {
query = query.sharedEqualTo(shared); query = query.sharedEqualTo(shared);
} }
@@ -42,37 +46,61 @@ class AlbumRepository implements IAlbumRepository {
} }
@override @override
Future<Album> update(Album album) => Future<Album> update(Album album) => txn(() => db.albums.store(album));
_db.writeTxn(() => _db.albums.store(album));
@override @override
Future<void> delete(int albumId) => Future<void> delete(int albumId) => txn(() => db.albums.delete(albumId));
_db.writeTxn(() => _db.albums.delete(albumId));
@override @override
Future<List<Album>> getAll({bool? shared}) { Future<List<Album>> getAll({
final baseQuery = _db.albums.filter(); bool? shared,
QueryBuilder<Album, Album, QAfterFilterCondition>? query; bool? remote,
if (shared != null) { int? ownerId,
query = baseQuery.sharedEqualTo(true); AlbumSort? sortBy,
}) {
final baseQuery = db.albums.where();
final QueryBuilder<Album, Album, QAfterWhereClause> afterWhere;
if (remote == null) {
afterWhere = baseQuery.noOp();
} else if (remote) {
afterWhere = baseQuery.remoteIdIsNotNull();
} else {
afterWhere = baseQuery.localIdIsNotNull();
} }
return query?.findAll() ?? _db.albums.where().findAll(); QueryBuilder<Album, Album, QAfterFilterCondition> filterQuery =
afterWhere.filter().noOp();
if (shared != null) {
filterQuery = filterQuery.sharedEqualTo(true);
}
if (ownerId != null) {
filterQuery = filterQuery.owner((q) => q.isarIdEqualTo(ownerId));
}
final QueryBuilder<Album, Album, QAfterSortBy> query;
switch (sortBy) {
case null:
query = filterQuery.noOp();
case AlbumSort.remoteId:
query = filterQuery.sortByRemoteId();
case AlbumSort.localId:
query = filterQuery.sortByLocalId();
}
return query.findAll();
} }
@override @override
Future<Album?> getById(int id) => _db.albums.get(id); Future<Album?> get(int id) => db.albums.get(id);
@override @override
Future<void> removeUsers(Album album, List<User> users) => Future<void> removeUsers(Album album, List<User> users) =>
_db.writeTxn(() => album.sharedUsers.update(unlink: users)); txn(() => album.sharedUsers.update(unlink: users));
@override @override
Future<void> addAssets(Album album, List<Asset> assets) => Future<void> addAssets(Album album, List<Asset> assets) =>
_db.writeTxn(() => album.assets.update(link: assets)); txn(() => album.assets.update(link: assets));
@override @override
Future<void> removeAssets(Album album, List<Asset> assets) => Future<void> removeAssets(Album album, List<Asset> assets) =>
_db.writeTxn(() => album.assets.update(unlink: assets)); txn(() => album.assets.update(unlink: assets));
@override @override
Future<Album> recalculateMetadata(Album album) async { Future<Album> recalculateMetadata(Album album) async {
@@ -82,4 +110,12 @@ class AlbumRepository implements IAlbumRepository {
await album.assets.filter().updatedAtProperty().max(); await album.assets.filter().updatedAtProperty().max();
return album; return album;
} }
@override
Future<void> addUsers(Album album, List<User> users) =>
txn(() => album.sharedUsers.update(link: users));
@override
Future<void> deleteAllLocal() =>
txn(() => db.albums.where().localIdIsNotNull().deleteAll());
} }

View File

@@ -4,15 +4,14 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart'; import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart'; import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
final albumApiRepositoryProvider = Provider( final albumApiRepositoryProvider = Provider(
(ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi), (ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
); );
class AlbumApiRepository extends BaseApiRepository class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
implements IAlbumApiRepository {
final AlbumsApi _api; final AlbumsApi _api;
AlbumApiRepository(this._api); AlbumApiRepository(this._api);
@@ -26,7 +25,7 @@ class AlbumApiRepository extends BaseApiRepository
@override @override
Future<List<Album>> getAll({bool? shared}) async { Future<List<Album>> getAll({bool? shared}) async {
final dtos = await checkNull(_api.getAllAlbums(shared: shared)); final dtos = await checkNull(_api.getAllAlbums(shared: shared));
return dtos.map(_toAlbum).toList().cast(); return dtos.map(_toAlbum).toList();
} }
@override @override

View File

@@ -1,8 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/constants/errors.dart'; import 'package:immich_mobile/constants/errors.dart';
abstract class BaseApiRepository { abstract class ApiRepository {
@protected
Future<T> checkNull<T>(Future<T?> future) async { Future<T> checkNull<T>(Future<T?> future) async {
final response = await future; final response = await future;
if (response == null) throw NoResponseDtoError(); if (response == null) throw NoResponseDtoError();

View File

@@ -5,78 +5,145 @@ 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';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/device_asset.entity.dart'; import 'package:immich_mobile/entities/device_asset.entity.dart';
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
final assetRepositoryProvider = final assetRepositoryProvider =
Provider((ref) => AssetRepository(ref.watch(dbProvider))); Provider((ref) => AssetRepository(ref.watch(dbProvider)));
class AssetRepository implements IAssetRepository { class AssetRepository extends DatabaseRepository implements IAssetRepository {
final Isar _db; AssetRepository(super.db);
AssetRepository(
this._db,
);
@override @override
Future<List<Asset>> getByAlbum(Album album, {User? notOwnedBy}) { Future<List<Asset>> getByAlbum(
Album album, {
Iterable<int> notOwnedBy = const [],
int? ownerId,
AssetState? state,
AssetSort? sortBy,
}) {
var query = album.assets.filter(); var query = album.assets.filter();
if (notOwnedBy != null) { if (notOwnedBy.length == 1) {
query = query.not().ownerIdEqualTo(notOwnedBy.isarId); query = query.not().ownerIdEqualTo(notOwnedBy.first);
} else if (notOwnedBy.isNotEmpty) {
query =
query.not().anyOf(notOwnedBy, (q, int id) => q.ownerIdEqualTo(id));
} }
return query.findAll(); if (ownerId != null) {
query = query.ownerIdEqualTo(ownerId);
}
switch (state) {
case null:
break;
case AssetState.local:
query = query.remoteIdIsNull();
case AssetState.remote:
query = query.localIdIsNull();
case AssetState.merged:
query = query.localIdIsNotNull().remoteIdIsNotNull();
}
final QueryBuilder<Asset, Asset, QAfterSortBy> sortedQuery;
switch (sortBy) {
case null:
sortedQuery = query.noOp();
case AssetSort.checksum:
sortedQuery = query.sortByChecksum();
case AssetSort.ownerIdChecksum:
sortedQuery = query.sortByOwnerId().thenByChecksum();
}
return sortedQuery.findAll();
} }
@override @override
Future<void> deleteById(List<int> ids) => Future<void> deleteById(List<int> ids) => txn(() async {
_db.writeTxn(() => _db.assets.deleteAll(ids)); await db.assets.deleteAll(ids);
await db.exifInfos.deleteAll(ids);
});
@override @override
Future<Asset?> getByRemoteId(String id) => _db.assets.getByRemoteId(id); Future<Asset?> getByRemoteId(String id) => db.assets.getByRemoteId(id);
@override @override
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) => Future<List<Asset>> getAllByRemoteId(
_db.assets.getAllByRemoteId(ids); Iterable<String> ids, {
AssetState? state,
}) =>
_getAllByRemoteIdImpl(ids, state).findAll();
QueryBuilder<Asset, Asset, QAfterFilterCondition> _getAllByRemoteIdImpl(
Iterable<String> ids,
AssetState? state,
) {
final query = db.assets.remote(ids).filter();
switch (state) {
case null:
return query.noOp();
case AssetState.local:
return query.remoteIdIsNull();
case AssetState.remote:
return query.localIdIsNull();
case AssetState.merged:
return query.localIdIsNotEmpty().remoteIdIsNotNull();
}
}
@override @override
Future<List<Asset>> getAll({ Future<List<Asset>> getAll({
required int ownerId, required int ownerId,
bool? remote, AssetState? state,
int limit = 100, AssetSort? sortBy,
int? limit,
}) { }) {
if (remote == null) { final baseQuery = db.assets.where();
return _db.assets final QueryBuilder<Asset, Asset, QAfterFilterCondition> filteredQuery;
.where() switch (state) {
.ownerIdEqualToAnyChecksum(ownerId) case null:
.limit(limit) filteredQuery = baseQuery.ownerIdEqualToAnyChecksum(ownerId).noOp();
.findAll(); case AssetState.local:
} filteredQuery = baseQuery
final QueryBuilder<Asset, Asset, QAfterFilterCondition> query; .remoteIdIsNull()
if (remote) { .filter()
query = _db.assets .localIdIsNotNull()
.where() .ownerIdEqualTo(ownerId);
.localIdIsNull() case AssetState.remote:
.filter() filteredQuery = baseQuery
.remoteIdIsNotNull() .localIdIsNull()
.ownerIdEqualTo(ownerId); .filter()
} else { .remoteIdIsNotNull()
query = _db.assets .ownerIdEqualTo(ownerId);
.where() case AssetState.merged:
.remoteIdIsNull() filteredQuery = baseQuery
.filter() .ownerIdEqualToAnyChecksum(ownerId)
.localIdIsNotNull() .filter()
.ownerIdEqualTo(ownerId); .remoteIdIsNotNull()
.localIdIsNotNull();
} }
return query.limit(limit).findAll(); final QueryBuilder<Asset, Asset, QAfterSortBy> query;
switch (sortBy) {
case null:
query = filteredQuery.noOp();
case AssetSort.checksum:
query = filteredQuery.sortByChecksum();
case AssetSort.ownerIdChecksum:
query = filteredQuery.sortByOwnerId().thenByChecksum();
}
return limit == null ? query.findAll() : query.limit(limit).findAll();
} }
@override @override
Future<List<Asset>> updateAll(List<Asset> assets) async { Future<List<Asset>> updateAll(List<Asset> assets) async {
await _db.writeTxn(() => _db.assets.putAll(assets)); await txn(() => db.assets.putAll(assets));
return assets; return assets;
} }
@@ -84,16 +151,20 @@ class AssetRepository implements IAssetRepository {
Future<List<Asset>> getMatches({ Future<List<Asset>> getMatches({
required List<Asset> assets, required List<Asset> assets,
required int ownerId, required int ownerId,
bool? remote, AssetState? state,
int limit = 100, int limit = 100,
}) { }) {
final baseQuery = db.assets.where();
final QueryBuilder<Asset, Asset, QAfterFilterCondition> query; final QueryBuilder<Asset, Asset, QAfterFilterCondition> query;
if (remote == null) { switch (state) {
query = _db.assets.filter().remoteIdIsNotNull().or().localIdIsNotNull(); case null:
} else if (remote) { query = baseQuery.noOp();
query = _db.assets.where().localIdIsNull().filter().remoteIdIsNotNull(); case AssetState.local:
} else { query = baseQuery.remoteIdIsNull().filter().localIdIsNotNull();
query = _db.assets.where().remoteIdIsNull().filter().localIdIsNotNull(); case AssetState.remote:
query = baseQuery.localIdIsNull().filter().remoteIdIsNotNull();
case AssetState.merged:
query = baseQuery.localIdIsNotNull().filter().remoteIdIsNotNull();
} }
return _getMatchesImpl(query, ownerId, assets, limit); return _getMatchesImpl(query, ownerId, assets, limit);
} }
@@ -101,16 +172,50 @@ class AssetRepository implements IAssetRepository {
@override @override
Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids) => Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids) =>
Platform.isAndroid Platform.isAndroid
? _db.androidDeviceAssets.getAll(ids.cast()) ? db.androidDeviceAssets.getAll(ids.cast())
: _db.iOSDeviceAssets.getAllById(ids.cast()); : db.iOSDeviceAssets.getAllById(ids.cast());
@override @override
Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets) => Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets) => txn(
_db.writeTxn(
() => Platform.isAndroid () => Platform.isAndroid
? _db.androidDeviceAssets.putAll(deviceAssets.cast()) ? db.androidDeviceAssets.putAll(deviceAssets.cast())
: _db.iOSDeviceAssets.putAll(deviceAssets.cast()), : db.iOSDeviceAssets.putAll(deviceAssets.cast()),
); );
@override
Future<Asset> update(Asset asset) async {
await txn(() => asset.put(db));
return asset;
}
@override
Future<void> upsertDuplicatedAssets(Iterable<String> duplicatedAssets) => txn(
() => db.duplicatedAssets
.putAll(duplicatedAssets.map(DuplicatedAsset.new).toList()),
);
@override
Future<List<String>> getAllDuplicatedAssetIds() =>
db.duplicatedAssets.where().idProperty().findAll();
@override
Future<Asset?> getByOwnerIdChecksum(int ownerId, String checksum) =>
db.assets.getByOwnerIdChecksum(ownerId, checksum);
@override
Future<List<Asset?>> getAllByOwnerIdChecksum(
List<int> ids,
List<String> checksums,
) =>
db.assets.getAllByOwnerIdChecksum(ids, checksums);
@override
Future<List<Asset>> getAllLocal() =>
db.assets.where().localIdIsNotNull().findAll();
@override
Future<void> deleteAllByRemoteId(List<String> ids, {AssetState? state}) =>
txn(() => _getAllByRemoteIdImpl(ids, state).deleteAll());
} }
Future<List<Asset>> _getMatchesImpl( Future<List<Asset>> _getMatchesImpl(

View File

@@ -2,7 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart'; import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart'; import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
final assetApiRepositoryProvider = Provider( final assetApiRepositoryProvider = Provider(
@@ -12,8 +12,7 @@ final assetApiRepositoryProvider = Provider(
), ),
); );
class AssetApiRepository extends BaseApiRepository class AssetApiRepository extends ApiRepository implements IAssetApiRepository {
implements IAssetApiRepository {
final AssetsApi _api; final AssetsApi _api;
final SearchApi _searchApi; final SearchApi _searchApi;

View File

@@ -43,4 +43,17 @@ class AssetMediaRepository implements IAssetMediaRepository {
asset.local = local; asset.local = local;
return asset; return asset;
} }
@override
Future<String?> getOriginalFilename(String id) async {
final entity = await AssetEntity.fromId(id);
if (entity == null) {
return null;
}
// titleAsync gets the correct original filename for some assets on iOS
// otherwise using the `entity.title` would return a random GUID
return await entity.titleAsync;
}
} }

View File

@@ -2,19 +2,41 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/interfaces/backup.interface.dart'; import 'package:immich_mobile/interfaces/backup.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
final backupRepositoryProvider = final backupRepositoryProvider =
Provider((ref) => BackupRepository(ref.watch(dbProvider))); Provider((ref) => BackupRepository(ref.watch(dbProvider)));
class BackupRepository implements IBackupRepository { class BackupRepository extends DatabaseRepository implements IBackupRepository {
final Isar _db; BackupRepository(super.db);
BackupRepository( @override
this._db, Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort}) {
); final baseQuery = db.backupAlbums.where();
final QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> query;
switch (sort) {
case null:
query = baseQuery.noOp();
case BackupAlbumSort.id:
query = baseQuery.sortById();
}
return query.findAll();
}
@override @override
Future<List<String>> getIdsBySelection(BackupSelection backup) => Future<List<String>> getIdsBySelection(BackupSelection backup) =>
_db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll(); db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll();
@override
Future<List<BackupAlbum>> getAllBySelection(BackupSelection backup) =>
db.backupAlbums.filter().selectionEqualTo(backup).findAll();
@override
Future<void> deleteAll(List<int> ids) =>
txn(() => db.backupAlbums.deleteAll(ids));
@override
Future<void> updateAll(List<BackupAlbum> backupAlbums) =>
txn(() => db.backupAlbums.putAll(backupAlbums));
} }

View File

@@ -0,0 +1,28 @@
import 'dart:async';
import 'package:immich_mobile/interfaces/database.interface.dart';
import 'package:isar/isar.dart';
/// copied from Isar; needed to check if an async transaction is already active
const Symbol _zoneTxn = #zoneTxn;
abstract class DatabaseRepository implements IDatabaseRepository {
final Isar db;
DatabaseRepository(this.db);
bool get inTxn => Zone.current[_zoneTxn] != null;
Future<T> txn<T>(Future<T> Function() callback) =>
inTxn ? callback() : transaction(callback);
@override
Future<T> transaction<T>(Future<T> Function() callback) =>
db.writeTxn(callback);
}
extension Asd<T> on QueryBuilder<T, dynamic, dynamic> {
QueryBuilder<T, T, O> noOp<O>() {
// ignore: invalid_use_of_protected_member
return QueryBuilder.apply(this, (query) => query);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:background_downloader/background_downloader.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/download.interface.dart';
import 'package:immich_mobile/utils/download.dart';
final downloadRepositoryProvider = Provider((ref) => DownloadRepository());
class DownloadRepository implements IDownloadRepository {
@override
void Function(TaskStatusUpdate)? onImageDownloadStatus;
@override
void Function(TaskStatusUpdate)? onVideoDownloadStatus;
@override
void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
@override
void Function(TaskProgressUpdate)? onTaskProgress;
DownloadRepository() {
FileDownloader().registerCallbacks(
group: downloadGroupImage,
taskStatusCallback: (update) => onImageDownloadStatus?.call(update),
taskProgressCallback: (update) => onTaskProgress?.call(update),
);
FileDownloader().registerCallbacks(
group: downloadGroupVideo,
taskStatusCallback: (update) => onVideoDownloadStatus?.call(update),
taskProgressCallback: (update) => onTaskProgress?.call(update),
);
FileDownloader().registerCallbacks(
group: downloadGroupLivePhoto,
taskStatusCallback: (update) => onLivePhotoDownloadStatus?.call(update),
taskProgressCallback: (update) => onTaskProgress?.call(update),
);
}
@override
Future<bool> download(DownloadTask task) {
return FileDownloader().enqueue(task);
}
@override
Future<void> deleteAllTrackingRecords() {
return FileDownloader().database.deleteAllRecords();
}
@override
Future<bool> cancel(String id) {
return FileDownloader().cancelTaskWithId(id);
}
@override
Future<List<TaskRecord>> getLiveVideoTasks() {
return FileDownloader().database.allRecordsWithStatus(
TaskStatus.complete,
group: downloadGroupLivePhoto,
);
}
@override
Future<void> deleteRecordsWithIds(List<String> ids) {
return FileDownloader().database.deleteRecordsWithIds(ids);
}
}

View File

@@ -0,0 +1,29 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:isar/isar.dart';
final etagRepositoryProvider =
Provider((ref) => ETagRepository(ref.watch(dbProvider)));
class ETagRepository extends DatabaseRepository implements IETagRepository {
ETagRepository(super.db);
@override
Future<List<String>> getAllIds() => db.eTags.where().idProperty().findAll();
@override
Future<ETag?> get(int id) => db.eTags.get(id);
@override
Future<void> upsertAll(List<ETag> etags) => txn(() => db.eTags.putAll(etags));
@override
Future<void> deleteByIds(List<String> ids) =>
txn(() => db.eTags.deleteAllById(ids));
@override
Future<ETag?> getById(String id) => db.eTags.getById(id);
}

View File

@@ -2,27 +2,30 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart'; import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart';
import 'package:isar/isar.dart'; import 'package:immich_mobile/repositories/database.repository.dart';
final exifInfoRepositoryProvider = final exifInfoRepositoryProvider =
Provider((ref) => ExifInfoRepository(ref.watch(dbProvider))); Provider((ref) => ExifInfoRepository(ref.watch(dbProvider)));
class ExifInfoRepository implements IExifInfoRepository { class ExifInfoRepository extends DatabaseRepository
final Isar _db; implements IExifInfoRepository {
ExifInfoRepository(super.db);
ExifInfoRepository(
this._db,
);
@override @override
Future<void> delete(int id) => _db.exifInfos.delete(id); Future<void> delete(int id) => txn(() => db.exifInfos.delete(id));
@override @override
Future<ExifInfo?> get(int id) => _db.exifInfos.get(id); Future<ExifInfo?> get(int id) => db.exifInfos.get(id);
@override @override
Future<ExifInfo> update(ExifInfo exifInfo) async { Future<ExifInfo> update(ExifInfo exifInfo) async {
await _db.writeTxn(() => _db.exifInfos.put(exifInfo)); await txn(() => db.exifInfos.put(exifInfo));
return exifInfo; return exifInfo;
} }
@override
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos) async {
await txn(() => db.exifInfos.putAll(exifInfos));
return exifInfos;
}
} }

View File

@@ -16,8 +16,12 @@ class FileMediaRepository implements IFileMediaRepository {
required String title, required String title,
String? relativePath, String? relativePath,
}) async { }) async {
final entity = await PhotoManager.editor final entity = await PhotoManager.editor.saveImage(
.saveImage(data, title: title, relativePath: relativePath); data,
filename: title,
title: title,
relativePath: relativePath,
);
return AssetMediaRepository.toAsset(entity); return AssetMediaRepository.toAsset(entity);
} }

View File

@@ -2,7 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart'; import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart'; import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
final partnerApiRepositoryProvider = Provider( final partnerApiRepositoryProvider = Provider(
@@ -11,7 +11,7 @@ final partnerApiRepositoryProvider = Provider(
), ),
); );
class PartnerApiRepository extends BaseApiRepository class PartnerApiRepository extends ApiRepository
implements IPartnerApiRepository { implements IPartnerApiRepository {
final PartnersApi _api; final PartnersApi _api;

View File

@@ -1,14 +1,14 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart'; import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart'; import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
final personApiRepositoryProvider = Provider( final personApiRepositoryProvider = Provider(
(ref) => PersonApiRepository(ref.watch(apiServiceProvider).peopleApi), (ref) => PersonApiRepository(ref.watch(apiServiceProvider).peopleApi),
); );
class PersonApiRepository extends BaseApiRepository class PersonApiRepository extends ApiRepository
implements IPersonApiRepository { implements IPersonApiRepository {
final PeopleApi _api; final PeopleApi _api;

View File

@@ -3,37 +3,61 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/user.interface.dart'; import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
final userRepositoryProvider = final userRepositoryProvider =
Provider((ref) => UserRepository(ref.watch(dbProvider))); Provider((ref) => UserRepository(ref.watch(dbProvider)));
class UserRepository implements IUserRepository { class UserRepository extends DatabaseRepository implements IUserRepository {
final Isar _db; UserRepository(super.db);
UserRepository(
this._db,
);
@override @override
Future<List<User>> getByIds(List<String> ids) async => Future<List<User>> getByIds(List<String> ids) async =>
(await _db.users.getAllById(ids)).cast(); (await db.users.getAllById(ids)).nonNulls.toList();
@override @override
Future<User?> get(String id) => _db.users.getById(id); Future<User?> get(String id) => db.users.getById(id);
@override @override
Future<List<User>> getAll({bool self = true}) { Future<List<User>> getAll({bool self = true, UserSort? sortBy}) {
if (self) { final baseQuery = db.users.where();
return _db.users.where().findAll();
}
final int userId = Store.get(StoreKey.currentUser).isarId; final int userId = Store.get(StoreKey.currentUser).isarId;
return _db.users.where().isarIdNotEqualTo(userId).findAll(); final QueryBuilder<User, User, QAfterWhereClause> afterWhere =
self ? baseQuery.noOp() : baseQuery.isarIdNotEqualTo(userId);
final QueryBuilder<User, User, QAfterSortBy> query;
switch (sortBy) {
case null:
query = afterWhere.noOp();
case UserSort.id:
query = afterWhere.sortById();
}
return query.findAll();
} }
@override @override
Future<User> update(User user) async { Future<User> update(User user) async {
await _db.writeTxn(() => _db.users.put(user)); await txn(() => db.users.put(user));
return user; return user;
} }
@override
Future<User> me() => Future.value(Store.get(StoreKey.currentUser));
@override
Future<void> deleteById(List<int> ids) => txn(() => db.users.deleteAll(ids));
@override
Future<List<User>> upsertAll(List<User> users) async {
await txn(() => db.users.putAll(users));
return users;
}
@override
Future<List<User>> getAllAccessible() => db.users
.filter()
.isPartnerSharedWithEqualTo(true)
.or()
.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)
.findAll();
} }

View File

@@ -5,7 +5,7 @@ import 'package:http/http.dart';
import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/user_api.interface.dart'; import 'package:immich_mobile/interfaces/user_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart'; import 'package:immich_mobile/repositories/api.repository.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
final userApiRepositoryProvider = Provider( final userApiRepositoryProvider = Provider(
@@ -14,8 +14,7 @@ final userApiRepositoryProvider = Provider(
), ),
); );
class UserApiRepository extends BaseApiRepository class UserApiRepository extends ApiRepository implements IUserApiRepository {
implements IUserApiRepository {
final UsersApi _api; final UsersApi _api;
UserApiRepository(this._api); UserApiRepository(this._api);

View File

@@ -243,14 +243,15 @@ class AlbumService {
int albumId, { int albumId, {
List<Asset> add = const [], List<Asset> add = const [],
List<Asset> remove = const [], List<Asset> remove = const [],
}) async { }) =>
final album = await _albumRepository.getById(albumId); _albumRepository.transaction(() async {
if (album == null) return; final album = await _albumRepository.get(albumId);
await _albumRepository.addAssets(album, add); if (album == null) return;
await _albumRepository.removeAssets(album, remove); await _albumRepository.addAssets(album, add);
await _albumRepository.recalculateMetadata(album); await _albumRepository.removeAssets(album, remove);
await _albumRepository.update(album); await _albumRepository.recalculateMetadata(album);
} await _albumRepository.update(album);
});
Future<bool> addAdditionalUserToAlbum( Future<bool> addAdditionalUserToAlbum(
List<String> sharedUserIds, List<String> sharedUserIds,
@@ -285,20 +286,20 @@ class AlbumService {
Future<bool> deleteAlbum(Album album) async { Future<bool> deleteAlbum(Album album) async {
try { try {
final user = Store.get(StoreKey.currentUser); final userId = Store.get(StoreKey.currentUser).isarId;
if (album.owner.value?.isarId == user.isarId) { if (album.owner.value?.isarId == userId) {
await _albumApiRepository.delete(album.remoteId!); await _albumApiRepository.delete(album.remoteId!);
} }
if (album.shared) { if (album.shared) {
final foreignAssets = final foreignAssets =
await _assetRepository.getByAlbum(album, notOwnedBy: user); await _assetRepository.getByAlbum(album, notOwnedBy: [userId]);
await _albumRepository.delete(album.id); await _albumRepository.delete(album.id);
final List<Album> albums = await _albumRepository.getAll(shared: true); final List<Album> albums = await _albumRepository.getAll(shared: true);
final List<Asset> existing = []; final List<Asset> existing = [];
for (Album album in albums) { for (Album album in albums) {
existing.addAll( existing.addAll(
await _assetRepository.getByAlbum(album, notOwnedBy: user), await _assetRepository.getByAlbum(album, notOwnedBy: [userId]),
); );
} }
final List<int> idsToRemove = final List<int> idsToRemove =
@@ -357,7 +358,7 @@ class AlbumService {
album.sharedUsers.remove(user); album.sharedUsers.remove(user);
await _albumRepository.removeUsers(album, [user]); await _albumRepository.removeUsers(album, [user]);
final a = await _albumRepository.getById(album.id); final a = await _albumRepository.get(album.id);
// trigger watcher // trigger watcher
await _albumRepository.update(a!); await _albumRepository.update(a!);

View File

@@ -1,27 +1,30 @@
// ignore_for_file: null_argument_to_non_null_type
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart'; import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/interfaces/backup.interface.dart';
import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart'; import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/repositories/etag.repository.dart';
import 'package:immich_mobile/repositories/exif_info.repository.dart'; import 'package:immich_mobile/repositories/exif_info.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/backup.service.dart';
import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/services/user.service.dart'; import 'package:immich_mobile/services/user.service.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@@ -29,48 +32,54 @@ import 'package:openapi/api.dart';
final assetServiceProvider = Provider( final assetServiceProvider = Provider(
(ref) => AssetService( (ref) => AssetService(
ref.watch(assetApiRepositoryProvider), ref.watch(assetApiRepositoryProvider),
ref.watch(assetRepositoryProvider),
ref.watch(exifInfoRepositoryProvider), ref.watch(exifInfoRepositoryProvider),
ref.watch(userRepositoryProvider),
ref.watch(etagRepositoryProvider),
ref.watch(backupRepositoryProvider),
ref.watch(apiServiceProvider), ref.watch(apiServiceProvider),
ref.watch(syncServiceProvider), ref.watch(syncServiceProvider),
ref.watch(userServiceProvider), ref.watch(userServiceProvider),
ref.watch(backupServiceProvider), ref.watch(backupServiceProvider),
ref.watch(albumServiceProvider), ref.watch(albumServiceProvider),
ref.watch(dbProvider),
), ),
); );
class AssetService { class AssetService {
final IAssetApiRepository _assetApiRepository; final IAssetApiRepository _assetApiRepository;
final IAssetRepository _assetRepository;
final IExifInfoRepository _exifInfoRepository; final IExifInfoRepository _exifInfoRepository;
final IUserRepository _userRepository;
final IETagRepository _etagRepository;
final IBackupRepository _backupRepository;
final ApiService _apiService; final ApiService _apiService;
final SyncService _syncService; final SyncService _syncService;
final UserService _userService; final UserService _userService;
final BackupService _backupService; final BackupService _backupService;
final AlbumService _albumService; final AlbumService _albumService;
final log = Logger('AssetService'); final log = Logger('AssetService');
final Isar _db;
AssetService( AssetService(
this._assetApiRepository, this._assetApiRepository,
this._assetRepository,
this._exifInfoRepository, this._exifInfoRepository,
this._userRepository,
this._etagRepository,
this._backupRepository,
this._apiService, this._apiService,
this._syncService, this._syncService,
this._userService, this._userService,
this._backupService, this._backupService,
this._albumService, this._albumService,
this._db,
); );
/// Checks the server for updated assets and updates the local database if /// Checks the server for updated assets and updates the local database if
/// required. Returns `true` if there were any changes. /// required. Returns `true` if there were any changes.
Future<bool> refreshRemoteAssets() async { Future<bool> refreshRemoteAssets() async {
final syncedUserIds = await _db.eTags.where().idProperty().findAll(); final syncedUserIds = await _etagRepository.getAllIds();
final List<User> syncedUsers = syncedUserIds.isEmpty final List<User> syncedUsers = syncedUserIds.isEmpty
? [] ? []
: await _db.users : await _userRepository.getByIds(syncedUserIds);
.where()
.anyOf(syncedUserIds, (q, id) => q.idEqualTo(id))
.findAll();
final Stopwatch sw = Stopwatch()..start(); final Stopwatch sw = Stopwatch()..start();
final bool changes = await _syncService.syncRemoteAssetsToDb( final bool changes = await _syncService.syncRemoteAssetsToDb(
users: syncedUsers, users: syncedUsers,
@@ -175,7 +184,7 @@ class AssetService {
/// Loads the exif information from the database. If there is none, loads /// Loads the exif information from the database. If there is none, loads
/// the exif info from the server (remote assets only) /// the exif info from the server (remote assets only)
Future<Asset> loadExif(Asset a) async { Future<Asset> loadExif(Asset a) async {
a.exifInfo ??= await _db.exifInfos.get(a.id); a.exifInfo ??= await _exifInfoRepository.get(a.id);
// fileSize is always filled on the server but not set on client // fileSize is always filled on the server but not set on client
if (a.exifInfo?.fileSize == null) { if (a.exifInfo?.fileSize == null) {
if (a.isRemote) { if (a.isRemote) {
@@ -185,7 +194,7 @@ class AssetService {
a.exifInfo = newExif; a.exifInfo = newExif;
if (newExif != a.exifInfo) { if (newExif != a.exifInfo) {
if (a.isInDb) { if (a.isInDb) {
_db.writeTxn(() => a.put(_db)); _assetRepository.transaction(() => _assetRepository.update(a));
} else { } else {
debugPrint("[loadExif] parameter Asset is not from DB!"); debugPrint("[loadExif] parameter Asset is not from DB!");
} }
@@ -214,7 +223,7 @@ class AssetService {
); );
} }
Future<List<Asset?>> changeFavoriteStatus( Future<List<Asset>> changeFavoriteStatus(
List<Asset> assets, List<Asset> assets,
bool isFavorite, bool isFavorite,
) async { ) async {
@@ -230,11 +239,11 @@ class AssetService {
return assets; return assets;
} catch (error, stack) { } catch (error, stack) {
log.severe("Error while changing favorite status", error, stack); log.severe("Error while changing favorite status", error, stack);
return Future.value(null); return [];
} }
} }
Future<List<Asset?>> changeArchiveStatus( Future<List<Asset>> changeArchiveStatus(
List<Asset> assets, List<Asset> assets,
bool isArchived, bool isArchived,
) async { ) async {
@@ -250,11 +259,11 @@ class AssetService {
return assets; return assets;
} catch (error, stack) { } catch (error, stack) {
log.severe("Error while changing archive status", error, stack); log.severe("Error while changing archive status", error, stack);
return Future.value(null); return [];
} }
} }
Future<List<Asset?>> changeDateTime( Future<List<Asset>?> changeDateTime(
List<Asset> assets, List<Asset> assets,
String updatedDt, String updatedDt,
) async { ) async {
@@ -278,7 +287,7 @@ class AssetService {
} }
} }
Future<List<Asset?>> changeLocation( Future<List<Asset>?> changeLocation(
List<Asset> assets, List<Asset> assets,
LatLng location, LatLng location,
) async { ) async {
@@ -307,10 +316,10 @@ class AssetService {
Future<void> syncUploadedAssetToAlbums() async { Future<void> syncUploadedAssetToAlbums() async {
try { try {
final [selectedAlbums, excludedAlbums] = await Future.wait([ final selectedAlbums =
_backupService.selectedAlbumsQuery().findAll(), await _backupRepository.getAllBySelection(BackupSelection.select);
_backupService.excludedAlbumsQuery().findAll(), final excludedAlbums =
]); await _backupRepository.getAllBySelection(BackupSelection.exclude);
final candidates = await _backupService.buildUploadCandidates( final candidates = await _backupService.buildUploadCandidates(
selectedAlbums, selectedAlbums,
@@ -319,12 +328,11 @@ class AssetService {
); );
await refreshRemoteAssets(); await refreshRemoteAssets();
final remoteAssets = await _db.assets final owner = await _userRepository.me();
.where() final remoteAssets = await _assetRepository.getAll(
.localIdIsNotNull() ownerId: owner.isarId,
.filter() state: AssetState.merged,
.remoteIdIsNotNull() );
.findAll();
/// Map<AlbumName, [AssetId]> /// Map<AlbumName, [AssetId]>
Map<String, List<String>> assetToAlbums = {}; Map<String, List<String>> assetToAlbums = {};

View File

@@ -9,14 +9,18 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/backup.interface.dart';
import 'package:immich_mobile/main.dart'; import 'package:immich_mobile/main.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
import 'package:immich_mobile/repositories/album.repository.dart'; import 'package:immich_mobile/repositories/album.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart'; import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart'; import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/etag.repository.dart';
import 'package:immich_mobile/repositories/exif_info.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart'; import 'package:immich_mobile/repositories/user.repository.dart';
@@ -37,7 +41,6 @@ import 'package:immich_mobile/services/user.service.dart';
import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/backup_progress.dart';
import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/diff.dart';
import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
import 'package:isar/isar.dart';
import 'package:path_provider_ios/path_provider_ios.dart'; import 'package:path_provider_ios/path_provider_ios.dart';
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
@@ -356,7 +359,7 @@ class BackgroundService {
} }
Future<bool> _onAssetsChanged() async { Future<bool> _onAssetsChanged() async {
final Isar db = await loadDb(); final db = await loadDb();
HttpOverrides.global = HttpSSLCertOverride(); HttpOverrides.global = HttpSSLCertOverride();
ApiService apiService = ApiService(); ApiService apiService = ApiService();
@@ -365,9 +368,12 @@ class BackgroundService {
AppSettingsService settingsService = AppSettingsService(); AppSettingsService settingsService = AppSettingsService();
AlbumRepository albumRepository = AlbumRepository(db); AlbumRepository albumRepository = AlbumRepository(db);
AssetRepository assetRepository = AssetRepository(db); AssetRepository assetRepository = AssetRepository(db);
BackupRepository backupAlbumRepository = BackupRepository(db); BackupRepository backupRepository = BackupRepository(db);
ExifInfoRepository exifInfoRepository = ExifInfoRepository(db);
ETagRepository eTagRepository = ETagRepository(db);
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository(); AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
FileMediaRepository fileMediaRepository = FileMediaRepository(); FileMediaRepository fileMediaRepository = FileMediaRepository();
AssetMediaRepository assetMediaRepository = AssetMediaRepository();
UserRepository userRepository = UserRepository(db); UserRepository userRepository = UserRepository(db);
UserApiRepository userApiRepository = UserApiRepository userApiRepository =
UserApiRepository(apiService.usersApi); UserApiRepository(apiService.usersApi);
@@ -380,11 +386,15 @@ class BackgroundService {
EntityService entityService = EntityService entityService =
EntityService(assetRepository, userRepository); EntityService(assetRepository, userRepository);
SyncService syncSerive = SyncService( SyncService syncSerive = SyncService(
db,
hashService, hashService,
entityService, entityService,
albumMediaRepository, albumMediaRepository,
albumApiRepository, albumApiRepository,
albumRepository,
assetRepository,
exifInfoRepository,
userRepository,
eTagRepository,
); );
UserService userService = UserService( UserService userService = UserService(
partnerApiRepository, partnerApiRepository,
@@ -398,21 +408,24 @@ class BackgroundService {
entityService, entityService,
albumRepository, albumRepository,
assetRepository, assetRepository,
backupAlbumRepository, backupRepository,
albumMediaRepository, albumMediaRepository,
albumApiRepository, albumApiRepository,
); );
BackupService backupService = BackupService( BackupService backupService = BackupService(
apiService, apiService,
db,
settingService, settingService,
albumService, albumService,
albumMediaRepository, albumMediaRepository,
fileMediaRepository, fileMediaRepository,
assetRepository,
assetMediaRepository,
); );
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync(); final selectedAlbums =
final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync(); await backupRepository.getAllBySelection(BackupSelection.select);
final excludedAlbums =
await backupRepository.getAllBySelection(BackupSelection.exclude);
if (selectedAlbums.isEmpty) { if (selectedAlbums.isEmpty) {
return true; return true;
} }
@@ -430,28 +443,28 @@ class BackgroundService {
await Store.delete(StoreKey.backupFailedSince); await Store.delete(StoreKey.backupFailedSince);
final backupAlbums = [...selectedAlbums, ...excludedAlbums]; final backupAlbums = [...selectedAlbums, ...excludedAlbums];
backupAlbums.sortBy((e) => e.id); backupAlbums.sortBy((e) => e.id);
db.writeTxnSync(() {
final dbAlbums = db.backupAlbums.where().sortById().findAllSync(); final dbAlbums =
final List<int> toDelete = []; await backupRepository.getAll(sort: BackupAlbumSort.id);
final List<BackupAlbum> toUpsert = []; final List<int> toDelete = [];
// stores the most recent `lastBackup` per album but always keeps the `selection` from the most recent DB state final List<BackupAlbum> toUpsert = [];
diffSortedListsSync( // stores the most recent `lastBackup` per album but always keeps the `selection` from the most recent DB state
dbAlbums, diffSortedListsSync(
backupAlbums, dbAlbums,
compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), backupAlbums,
both: (BackupAlbum a, BackupAlbum b) { compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id),
a.lastBackup = a.lastBackup.isAfter(b.lastBackup) both: (BackupAlbum a, BackupAlbum b) {
? a.lastBackup a.lastBackup = a.lastBackup.isAfter(b.lastBackup)
: b.lastBackup; ? a.lastBackup
toUpsert.add(a); : b.lastBackup;
return true; toUpsert.add(a);
}, return true;
onlyFirst: (BackupAlbum a) => toUpsert.add(a), },
onlySecond: (BackupAlbum b) => toDelete.add(b.isarId), onlyFirst: (BackupAlbum a) => toUpsert.add(a),
); onlySecond: (BackupAlbum b) => toDelete.add(b.isarId),
db.backupAlbums.deleteAllSync(toDelete); );
db.backupAlbums.putAllSync(toUpsert); await backupRepository.deleteAll(toDelete);
}); await backupRepository.updateAll(toUpsert);
} else if (Store.tryGet(StoreKey.backupFailedSince) == null) { } else if (Store.tryGet(StoreKey.backupFailedSince) == null) {
Store.put(StoreKey.backupFailedSince, DateTime.now()); Store.put(StoreKey.backupFailedSince, DateTime.now());
return false; return false;

View File

@@ -9,9 +9,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart'; import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart'; import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
@@ -19,13 +20,13 @@ import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart'; import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
@@ -35,31 +36,34 @@ import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
final backupServiceProvider = Provider( final backupServiceProvider = Provider(
(ref) => BackupService( (ref) => BackupService(
ref.watch(apiServiceProvider), ref.watch(apiServiceProvider),
ref.watch(dbProvider),
ref.watch(appSettingsServiceProvider), ref.watch(appSettingsServiceProvider),
ref.watch(albumServiceProvider), ref.watch(albumServiceProvider),
ref.watch(albumMediaRepositoryProvider), ref.watch(albumMediaRepositoryProvider),
ref.watch(fileMediaRepositoryProvider), ref.watch(fileMediaRepositoryProvider),
ref.watch(assetRepositoryProvider),
ref.watch(assetMediaRepositoryProvider),
), ),
); );
class BackupService { class BackupService {
final httpClient = http.Client(); final httpClient = http.Client();
final ApiService _apiService; final ApiService _apiService;
final Isar _db;
final Logger _log = Logger("BackupService"); final Logger _log = Logger("BackupService");
final AppSettingsService _appSetting; final AppSettingsService _appSetting;
final AlbumService _albumService; final AlbumService _albumService;
final IAlbumMediaRepository _albumMediaRepository; final IAlbumMediaRepository _albumMediaRepository;
final IFileMediaRepository _fileMediaRepository; final IFileMediaRepository _fileMediaRepository;
final IAssetRepository _assetRepository;
final IAssetMediaRepository _assetMediaRepository;
BackupService( BackupService(
this._apiService, this._apiService,
this._db,
this._appSetting, this._appSetting,
this._albumService, this._albumService,
this._albumMediaRepository, this._albumMediaRepository,
this._fileMediaRepository, this._fileMediaRepository,
this._assetRepository,
this._assetMediaRepository,
); );
Future<List<String>?> getDeviceBackupAsset() async { Future<List<String>?> getDeviceBackupAsset() async {
@@ -73,24 +77,17 @@ class BackupService {
} }
} }
Future<void> _saveDuplicatedAssetIds(List<String> deviceAssetIds) { Future<void> _saveDuplicatedAssetIds(List<String> deviceAssetIds) =>
final duplicates = deviceAssetIds.map((id) => DuplicatedAsset(id)).toList(); _assetRepository.transaction(
return _db.writeTxn(() => _db.duplicatedAssets.putAll(duplicates)); () => _assetRepository.upsertDuplicatedAssets(deviceAssetIds),
} );
/// Get duplicated asset id from database /// Get duplicated asset id from database
Future<Set<String>> getDuplicatedAssetIds() async { Future<Set<String>> getDuplicatedAssetIds() async {
final duplicates = await _db.duplicatedAssets.where().findAll(); final duplicates = await _assetRepository.getAllDuplicatedAssetIds();
return duplicates.map((e) => e.id).toSet(); return duplicates.toSet();
} }
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
selectedAlbumsQuery() =>
_db.backupAlbums.filter().selectionEqualTo(BackupSelection.select);
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
excludedAlbumsQuery() =>
_db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude);
/// Returns all assets newer than the last successful backup per album /// Returns all assets newer than the last successful backup per album
/// if `useTimeFilter` is set to true, all assets will be returned /// if `useTimeFilter` is set to true, all assets will be returned
Future<Set<BackupCandidate>> buildUploadCandidates( Future<Set<BackupCandidate>> buildUploadCandidates(
@@ -329,7 +326,9 @@ class BackupService {
} }
if (file != null) { if (file != null) {
String originalFileName = asset.fileName; String? originalFileName =
await _assetMediaRepository.getOriginalFilename(asset.localId!);
originalFileName ??= asset.fileName;
if (asset.local!.isLivePhoto) { if (asset.local!.isLivePhoto) {
if (livePhotoFile == null) { if (livePhotoFile == null) {

View File

@@ -34,19 +34,19 @@ class BackupVerificationService {
final owner = Store.get(StoreKey.currentUser).isarId; final owner = Store.get(StoreKey.currentUser).isarId;
final List<Asset> onlyLocal = await _assetRepository.getAll( final List<Asset> onlyLocal = await _assetRepository.getAll(
ownerId: owner, ownerId: owner,
remote: false, state: AssetState.local,
limit: limit, limit: limit,
); );
final List<Asset> remoteMatches = await _assetRepository.getMatches( final List<Asset> remoteMatches = await _assetRepository.getMatches(
assets: onlyLocal, assets: onlyLocal,
ownerId: owner, ownerId: owner,
remote: true, state: AssetState.remote,
limit: limit, limit: limit,
); );
final List<Asset> localMatches = await _assetRepository.getMatches( final List<Asset> localMatches = await _assetRepository.getMatches(
assets: remoteMatches, assets: remoteMatches,
ownerId: owner, ownerId: owner,
remote: false, state: AssetState.local,
limit: limit, limit: limit,
); );

View File

@@ -0,0 +1,193 @@
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/download.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
import 'package:immich_mobile/repositories/download.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/download.dart';
final downloadServiceProvider = Provider(
(ref) => DownloadService(
ref.watch(fileMediaRepositoryProvider),
ref.watch(downloadRepositoryProvider),
),
);
class DownloadService {
final IDownloadRepository _downloadRepository;
final IFileMediaRepository _fileMediaRepository;
void Function(TaskStatusUpdate)? onImageDownloadStatus;
void Function(TaskStatusUpdate)? onVideoDownloadStatus;
void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus;
void Function(TaskProgressUpdate)? onTaskProgress;
DownloadService(
this._fileMediaRepository,
this._downloadRepository,
) {
_downloadRepository.onImageDownloadStatus = _onImageDownloadCallback;
_downloadRepository.onVideoDownloadStatus = _onVideoDownloadCallback;
_downloadRepository.onLivePhotoDownloadStatus =
_onLivePhotoDownloadCallback;
_downloadRepository.onTaskProgress = _onTaskProgressCallback;
}
void _onTaskProgressCallback(TaskProgressUpdate update) {
onTaskProgress?.call(update);
}
void _onImageDownloadCallback(TaskStatusUpdate update) {
onImageDownloadStatus?.call(update);
}
void _onVideoDownloadCallback(TaskStatusUpdate update) {
onVideoDownloadStatus?.call(update);
}
void _onLivePhotoDownloadCallback(TaskStatusUpdate update) {
onLivePhotoDownloadStatus?.call(update);
}
Future<bool> saveImage(Task task) async {
final filePath = await task.filePath();
final title = task.filename;
final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
final data = await File(filePath).readAsBytes();
final Asset? resultAsset = await _fileMediaRepository.saveImage(
data,
title: title,
relativePath: relativePath,
);
return resultAsset != null;
}
Future<bool> saveVideo(Task task) async {
final filePath = await task.filePath();
final title = task.filename;
final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
final file = File(filePath);
final Asset? resultAsset = await _fileMediaRepository.saveVideo(
file,
title: title,
relativePath: relativePath,
);
return resultAsset != null;
}
Future<bool> saveLivePhotos(
Task task,
String livePhotosId,
) async {
try {
final records = await _downloadRepository.getLiveVideoTasks();
if (records.length < 2) {
return false;
}
final imageRecord = records.firstWhere(
(record) {
final metadata = LivePhotosMetadata.fromJson(record.task.metaData);
return metadata.id == livePhotosId &&
metadata.part == LivePhotosPart.image;
},
);
final videoRecord = records.firstWhere((record) {
final metadata = LivePhotosMetadata.fromJson(record.task.metaData);
return metadata.id == livePhotosId &&
metadata.part == LivePhotosPart.video;
});
final imageFilePath = await imageRecord.task.filePath();
final videoFilePath = await videoRecord.task.filePath();
final resultAsset = await _fileMediaRepository.saveLivePhoto(
image: File(imageFilePath),
video: File(videoFilePath),
title: task.filename,
);
await _downloadRepository.deleteRecordsWithIds([
imageRecord.task.taskId,
videoRecord.task.taskId,
]);
return resultAsset != null;
} catch (error) {
debugPrint("Error saving live photo: $error");
return false;
}
}
Future<bool> cancelDownload(String id) async {
return await FileDownloader().cancelTaskWithId(id);
}
Future<void> download(Asset asset) async {
if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) {
await _downloadRepository.download(
_buildDownloadTask(
asset.remoteId!,
asset.fileName,
group: downloadGroupLivePhoto,
metadata: LivePhotosMetadata(
part: LivePhotosPart.image,
id: asset.remoteId!,
).toJson(),
),
);
await _downloadRepository.download(
_buildDownloadTask(
asset.livePhotoVideoId!,
asset.fileName.toUpperCase().replaceAll(".HEIC", '.MOV'),
group: downloadGroupLivePhoto,
metadata: LivePhotosMetadata(
part: LivePhotosPart.video,
id: asset.remoteId!,
).toJson(),
),
);
} else {
await _downloadRepository.download(
_buildDownloadTask(
asset.remoteId!,
asset.fileName,
group: asset.isImage ? downloadGroupImage : downloadGroupVideo,
),
);
}
}
DownloadTask _buildDownloadTask(
String id,
String filename, {
String? group,
String? metadata,
}) {
final path = r'/assets/{id}/original'.replaceAll('{id}', id);
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final headers = ApiService.getRequestHeaders();
return DownloadTask(
taskId: id,
url: serverEndpoint + path,
headers: headers,
filename: filename,
updates: Updates.statusAndProgress,
group: group ?? '',
metaData: metadata ?? '',
);
}
}

View File

@@ -130,7 +130,9 @@ class HashService {
final validHashes = anyNull final validHashes = anyNull
? toAdd.where((e) => e.hash.length == 20).toList(growable: false) ? toAdd.where((e) => e.hash.length == 20).toList(growable: false)
: toAdd; : toAdd;
await _assetRepository.upsertDeviceAssets(validHashes);
await _assetRepository
.transaction(() => _assetRepository.upsertDeviceAssets(validHashes));
_log.fine("Hashed ${validHashes.length}/${toHash.length} assets"); _log.fine("Hashed ${validHashes.length}/${toHash.length} assets");
} }

View File

@@ -1,117 +0,0 @@
import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/response_extensions.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
final imageViewerServiceProvider = Provider(
(ref) => ImageViewerService(
ref.watch(apiServiceProvider),
ref.watch(fileMediaRepositoryProvider),
),
);
class ImageViewerService {
final ApiService _apiService;
final IFileMediaRepository _fileMediaRepository;
final Logger _log = Logger("ImageViewerService");
ImageViewerService(this._apiService, this._fileMediaRepository);
Future<bool> downloadAsset(Asset asset) async {
File? imageFile;
File? videoFile;
try {
// Download LivePhotos image and motion part
if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) {
var imageResponse =
await _apiService.assetsApi.downloadAssetWithHttpInfo(
asset.remoteId!,
);
var motionResponse =
await _apiService.assetsApi.downloadAssetWithHttpInfo(
asset.livePhotoVideoId!,
);
if (imageResponse.statusCode != 200 ||
motionResponse.statusCode != 200) {
final failedResponse =
imageResponse.statusCode != 200 ? imageResponse : motionResponse;
_log.severe(
"Motion asset download failed",
failedResponse.toLoggerString(),
);
return false;
}
Asset? resultAsset;
final tempDir = await getTemporaryDirectory();
videoFile = await File('${tempDir.path}/livephoto.mov').create();
imageFile = await File('${tempDir.path}/livephoto.heic').create();
videoFile.writeAsBytesSync(motionResponse.bodyBytes);
imageFile.writeAsBytesSync(imageResponse.bodyBytes);
resultAsset = await _fileMediaRepository.saveLivePhoto(
image: imageFile,
video: videoFile,
title: asset.fileName,
);
if (resultAsset == null) {
_log.warning(
"Asset cannot be saved as a live photo. This is most likely a motion photo. Saving only the image file",
);
resultAsset = await _fileMediaRepository
.saveImage(imageResponse.bodyBytes, title: asset.fileName);
}
return resultAsset != null;
} else {
var res = await _apiService.assetsApi
.downloadAssetWithHttpInfo(asset.remoteId!);
if (res.statusCode != 200) {
_log.severe("Asset download failed", res.toLoggerString());
return false;
}
final Asset? resultAsset;
final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
if (asset.isImage) {
resultAsset = await _fileMediaRepository.saveImage(
res.bodyBytes,
title: asset.fileName,
relativePath: relativePath,
);
} else {
final tempDir = await getTemporaryDirectory();
videoFile = await File('${tempDir.path}/${asset.fileName}').create();
videoFile.writeAsBytesSync(res.bodyBytes);
resultAsset = await _fileMediaRepository.saveVideo(
videoFile,
title: asset.fileName,
relativePath: relativePath,
);
}
return resultAsset != null;
}
} catch (error, stack) {
_log.severe("Error saving downloaded asset", error, stack);
return false;
} finally {
// Clear temp files
imageFile?.delete();
videoFile?.delete();
}
}
}

View File

@@ -61,7 +61,8 @@ class StackService {
removeAssets.add(asset); removeAssets.add(asset);
} }
await _assetRepository.updateAll(removeAssets); await _assetRepository
.transaction(() => _assetRepository.updateAll(removeAssets));
} catch (error) { } catch (error) {
debugPrint("Error while deleting stack: $error"); debugPrint("Error while deleting stack: $error");
} }

View File

@@ -5,48 +5,66 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart'; import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart'; import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/etag.interface.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/repositories/album.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart'; import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart'; import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/etag.repository.dart';
import 'package:immich_mobile/repositories/exif_info.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/services/hash.service.dart';
import 'package:immich_mobile/utils/async_mutex.dart'; import 'package:immich_mobile/utils/async_mutex.dart';
import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart';
import 'package:immich_mobile/utils/datetime_comparison.dart'; import 'package:immich_mobile/utils/datetime_comparison.dart';
import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
final syncServiceProvider = Provider( final syncServiceProvider = Provider(
(ref) => SyncService( (ref) => SyncService(
ref.watch(dbProvider),
ref.watch(hashServiceProvider), ref.watch(hashServiceProvider),
ref.watch(entityServiceProvider), ref.watch(entityServiceProvider),
ref.watch(albumMediaRepositoryProvider), ref.watch(albumMediaRepositoryProvider),
ref.watch(albumApiRepositoryProvider), ref.watch(albumApiRepositoryProvider),
ref.watch(albumRepositoryProvider),
ref.watch(assetRepositoryProvider),
ref.watch(exifInfoRepositoryProvider),
ref.watch(userRepositoryProvider),
ref.watch(etagRepositoryProvider),
), ),
); );
class SyncService { class SyncService {
final Isar _db;
final HashService _hashService; final HashService _hashService;
final EntityService _entityService; final EntityService _entityService;
final IAlbumMediaRepository _albumMediaRepository; final IAlbumMediaRepository _albumMediaRepository;
final IAlbumApiRepository _albumApiRepository; final IAlbumApiRepository _albumApiRepository;
final IAlbumRepository _albumRepository;
final IAssetRepository _assetRepository;
final IExifInfoRepository _exifInfoRepository;
final IUserRepository _userRepository;
final IETagRepository _eTagRepository;
final AsyncMutex _lock = AsyncMutex(); final AsyncMutex _lock = AsyncMutex();
final Logger _log = Logger('SyncService'); final Logger _log = Logger('SyncService');
SyncService( SyncService(
this._db,
this._hashService, this._hashService,
this._entityService, this._entityService,
this._albumMediaRepository, this._albumMediaRepository,
this._albumApiRepository, this._albumApiRepository,
this._albumRepository,
this._assetRepository,
this._exifInfoRepository,
this._userRepository,
this._eTagRepository,
); );
// public methods: // public methods:
@@ -119,7 +137,7 @@ class SyncService {
/// Returns `true`if there were any changes /// Returns `true`if there were any changes
Future<bool> _syncUsersFromServer(List<User> users) async { Future<bool> _syncUsersFromServer(List<User> users) async {
users.sortBy((u) => u.id); users.sortBy((u) => u.id);
final dbUsers = await _db.users.where().sortById().findAll(); final dbUsers = await _userRepository.getAll(sortBy: UserSort.id);
assert(dbUsers.isSortedBy((u) => u.id), "dbUsers not sorted!"); assert(dbUsers.isSortedBy((u) => u.id), "dbUsers not sorted!");
final List<int> toDelete = []; final List<int> toDelete = [];
final List<User> toUpsert = []; final List<User> toUpsert = [];
@@ -141,9 +159,9 @@ class SyncService {
onlySecond: (User b) => toDelete.add(b.isarId), onlySecond: (User b) => toDelete.add(b.isarId),
); );
if (changes) { if (changes) {
await _db.writeTxn(() async { await _userRepository.transaction(() async {
await _db.users.deleteAll(toDelete); await _userRepository.deleteById(toDelete);
await _db.users.putAll(toUpsert); await _userRepository.upsertAll(toUpsert);
}); });
} }
return changes; return changes;
@@ -152,15 +170,15 @@ class SyncService {
/// Syncs a new asset to the db. Returns `true` if successful /// Syncs a new asset to the db. Returns `true` if successful
Future<bool> _syncNewAssetToDb(Asset a) async { Future<bool> _syncNewAssetToDb(Asset a) async {
final Asset? inDb = final Asset? inDb =
await _db.assets.getByOwnerIdChecksum(a.ownerId, a.checksum); await _assetRepository.getByOwnerIdChecksum(a.ownerId, a.checksum);
if (inDb != null) { if (inDb != null) {
// unify local/remote assets by replacing the // unify local/remote assets by replacing the
// local-only asset in the DB with a local&remote asset // local-only asset in the DB with a local&remote asset
a = inDb.updatedCopy(a); a = inDb.updatedCopy(a);
} }
try { try {
await _db.writeTxn(() => a.put(_db)); await _assetRepository.update(a);
} on IsarError catch (e) { } catch (e) {
_log.severe("Failed to put new asset into db", e); _log.severe("Failed to put new asset into db", e);
return false; return false;
} }
@@ -175,9 +193,9 @@ class SyncService {
DateTime since, DateTime since,
) getChangedAssets, ) getChangedAssets,
) async { ) async {
final currentUser = Store.get(StoreKey.currentUser); final currentUser = await _userRepository.me();
final DateTime? since = final DateTime? since =
_db.eTags.getSync(currentUser.isarId)?.time?.toUtc(); (await _eTagRepository.get(currentUser.isarId))?.time?.toUtc();
if (since == null) return null; if (since == null) return null;
final DateTime now = DateTime.now(); final DateTime now = DateTime.now();
final (toUpsert, toDelete) = await getChangedAssets(users, since); final (toUpsert, toDelete) = await getChangedAssets(users, since);
@@ -198,7 +216,7 @@ class SyncService {
return true; return true;
} }
return false; return false;
} on IsarError catch (e) { } catch (e) {
_log.severe("Failed to sync remote assets to db", e); _log.severe("Failed to sync remote assets to db", e);
} }
return null; return null;
@@ -206,23 +224,21 @@ class SyncService {
/// Deletes remote-only assets, updates merged assets to be local-only /// Deletes remote-only assets, updates merged assets to be local-only
Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) { Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) {
return _db.writeTxn(() async { return _assetRepository.transaction(() async {
final idsToRemove = await _db.assets await _assetRepository.deleteAllByRemoteId(
.remote(idsToDelete) idsToDelete,
.filter() state: AssetState.remote,
.localIdIsNull() );
.idProperty() final merged = await _assetRepository.getAllByRemoteId(
.findAll(); idsToDelete,
await _db.assets.deleteAll(idsToRemove); state: AssetState.merged,
await _db.exifInfos.deleteAll(idsToRemove); );
final onlyLocal = await _db.assets.remote(idsToDelete).findAll(); if (merged.isEmpty) return;
if (onlyLocal.isNotEmpty) { for (final Asset asset in merged) {
for (final Asset a in onlyLocal) { asset.remoteId = null;
a.remoteId = null; asset.isTrashed = false;
a.isTrashed = false;
}
await _db.assets.putAll(onlyLocal);
} }
await _assetRepository.updateAll(merged);
}); });
} }
@@ -237,12 +253,7 @@ class SyncService {
return false; return false;
} }
await _syncUsersFromServer(serverUsers); await _syncUsersFromServer(serverUsers);
final List<User> users = await _db.users final List<User> users = await _userRepository.getAllAccessible();
.filter()
.isPartnerSharedWithEqualTo(true)
.or()
.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)
.findAll();
bool changes = false; bool changes = false;
for (User u in users) { for (User u in users) {
changes |= await _syncRemoteAssetsForUser(u, loadAssets); changes |= await _syncRemoteAssetsForUser(u, loadAssets);
@@ -259,11 +270,10 @@ class SyncService {
if (remote == null) { if (remote == null) {
return false; return false;
} }
final List<Asset> inDb = await _db.assets final List<Asset> inDb = await _assetRepository.getAll(
.where() ownerId: user.isarId,
.ownerIdEqualToAnyChecksum(user.isarId) sortBy: AssetSort.checksum,
.sortByChecksum() );
.findAll();
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
remote.sort(Asset.compareByChecksum); remote.sort(Asset.compareByChecksum);
@@ -278,9 +288,9 @@ class SyncService {
} }
final idsToDelete = toRemove.map((e) => e.id).toList(); final idsToDelete = toRemove.map((e) => e.id).toList();
try { try {
await _db.writeTxn(() => _db.assets.deleteAll(idsToDelete)); await _assetRepository.deleteById(idsToDelete);
await upsertAssetsWithExif(toAdd + toUpdate); await upsertAssetsWithExif(toAdd + toUpdate);
} on IsarError catch (e) { } catch (e) {
_log.severe("Failed to sync remote assets to db", e); _log.severe("Failed to sync remote assets to db", e);
} }
await _updateUserAssetsETag([user], now); await _updateUserAssetsETag([user], now);
@@ -289,12 +299,12 @@ class SyncService {
Future<void> _updateUserAssetsETag(List<User> users, DateTime time) { Future<void> _updateUserAssetsETag(List<User> users, DateTime time) {
final etags = users.map((u) => ETag(id: u.id, time: time)).toList(); final etags = users.map((u) => ETag(id: u.id, time: time)).toList();
return _db.writeTxn(() => _db.eTags.putAll(etags)); return _eTagRepository.upsertAll(etags);
} }
Future<void> _clearUserAssetsETag(List<User> users) { Future<void> _clearUserAssetsETag(List<User> users) {
final ids = users.map((u) => u.id).toList(); final ids = users.map((u) => u.id).toList();
return _db.writeTxn(() => _db.eTags.deleteAllById(ids)); return _eTagRepository.deleteByIds(ids);
} }
/// Syncs remote albums to the database /// Syncs remote albums to the database
@@ -305,15 +315,13 @@ class SyncService {
) async { ) async {
remoteAlbums.sortBy((e) => e.remoteId!); remoteAlbums.sortBy((e) => e.remoteId!);
final baseQuery = _db.albums.where().remoteIdIsNotNull().filter(); final User me = await _userRepository.me();
final QueryBuilder<Album, Album, QAfterFilterCondition> query; final List<Album> dbAlbums = await _albumRepository.getAll(
if (isShared) { remote: true,
query = baseQuery.sharedEqualTo(true); shared: isShared ? true : null,
} else { ownerId: isShared ? null : me.isarId,
final User me = Store.get(StoreKey.currentUser); sortBy: AlbumSort.remoteId,
query = baseQuery.owner((q) => q.isarIdEqualTo(me.isarId)); );
}
final List<Album> dbAlbums = await query.sortByRemoteId().findAll();
assert(dbAlbums.isSortedBy((e) => e.remoteId!), "dbAlbums not sorted!"); assert(dbAlbums.isSortedBy((e) => e.remoteId!), "dbAlbums not sorted!");
final List<Asset> toDelete = []; final List<Asset> toDelete = [];
@@ -333,10 +341,7 @@ class SyncService {
if (isShared && toDelete.isNotEmpty) { if (isShared && toDelete.isNotEmpty) {
final List<int> idsToRemove = sharedAssetsToRemove(toDelete, existing); final List<int> idsToRemove = sharedAssetsToRemove(toDelete, existing);
if (idsToRemove.isNotEmpty) { if (idsToRemove.isNotEmpty) {
await _db.writeTxn(() async { await _assetRepository.deleteById(idsToRemove);
await _db.assets.deleteAll(idsToRemove);
await _db.exifInfos.deleteAll(idsToRemove);
});
} }
} else { } else {
assert(toDelete.isEmpty); assert(toDelete.isEmpty);
@@ -360,8 +365,11 @@ class SyncService {
// i.e. it will always be null. Save it here. // i.e. it will always be null. Save it here.
final originalDto = dto; final originalDto = dto;
dto = await _albumApiRepository.get(dto.remoteId!); dto = await _albumApiRepository.get(dto.remoteId!);
final assetsInDb =
await album.assets.filter().sortByOwnerId().thenByChecksum().findAll(); final assetsInDb = await _assetRepository.getByAlbum(
album,
sortBy: AssetSort.ownerIdChecksum,
);
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!"); assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
final List<Asset> assetsOnRemote = dto.remoteAssets.toList(); final List<Asset> assetsOnRemote = dto.remoteAssets.toList();
assetsOnRemote.sort(Asset.compareByOwnerChecksum); assetsOnRemote.sort(Asset.compareByOwnerChecksum);
@@ -391,7 +399,7 @@ class SyncService {
final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd); final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
await upsertAssetsWithExif(updated); await upsertAssetsWithExif(updated);
final assetsToLink = existingInDb + updated; final assetsToLink = existingInDb + updated;
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>(); final usersToLink = await _userRepository.getByIds(userIdsToAdd);
album.name = dto.name; album.name = dto.name;
album.shared = dto.shared; album.shared = dto.shared;
@@ -402,32 +410,33 @@ class SyncService {
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp; album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
album.shared = dto.shared; album.shared = dto.shared;
album.activityEnabled = dto.activityEnabled; album.activityEnabled = dto.activityEnabled;
if (album.thumbnail.value?.remoteId != dto.remoteThumbnailAssetId) { final remoteThumbnailAssetId = dto.remoteThumbnailAssetId;
album.thumbnail.value = await _db.assets if (remoteThumbnailAssetId != null &&
.where() album.thumbnail.value?.remoteId != remoteThumbnailAssetId) {
.remoteIdEqualTo(dto.remoteThumbnailAssetId) album.thumbnail.value =
.findFirst(); await _assetRepository.getByRemoteId(remoteThumbnailAssetId);
} }
// write & commit all changes to DB // write & commit all changes to DB
try { try {
await _db.writeTxn(() async { await _assetRepository.transaction(() async {
await _db.assets.putAll(toUpdate); await _assetRepository.updateAll(toUpdate);
await album.thumbnail.save(); await _albumRepository.addUsers(album, usersToLink);
await album.sharedUsers await _albumRepository.removeUsers(album, usersToUnlink);
.update(link: usersToLink, unlink: usersToUnlink); await _albumRepository.addAssets(album, assetsToLink);
await album.assets.update(link: assetsToLink, unlink: toUnlink.cast()); await _albumRepository.removeAssets(album, toUnlink);
await _db.albums.put(album); await _albumRepository.recalculateMetadata(album);
await _albumRepository.update(album);
}); });
_log.info("Synced changes of remote album ${album.name} to DB"); _log.info("Synced changes of remote album ${album.name} to DB");
} on IsarError catch (e) { } catch (e) {
_log.severe("Failed to sync remote album to database", e); _log.severe("Failed to sync remote album to database", e);
} }
if (album.shared || dto.shared) { if (album.shared || dto.shared) {
final userId = Store.get(StoreKey.currentUser).isarId; final userId = (await _userRepository.me()).isarId;
final foreign = final foreign =
await album.assets.filter().not().ownerIdEqualTo(userId).findAll(); await _assetRepository.getByAlbum(album, notOwnedBy: [userId]);
existing.addAll(foreign); existing.addAll(foreign);
// delete assets in DB unless they belong to this user or part of some other shared album // delete assets in DB unless they belong to this user or part of some other shared album
@@ -456,7 +465,7 @@ class SyncService {
await upsertAssetsWithExif(updated); await upsertAssetsWithExif(updated);
await _entityService.fillAlbumWithDatabaseEntities(album); await _entityService.fillAlbumWithDatabaseEntities(album);
await _db.writeTxn(() => _db.albums.store(album)); await _albumRepository.create(album);
} else { } else {
_log.warning( _log.warning(
"Failed to add album from server: assetCount ${album.remoteAssetCount} != " "Failed to add album from server: assetCount ${album.remoteAssetCount} != "
@@ -474,27 +483,18 @@ class SyncService {
_log.info("Removing local album $album from DB"); _log.info("Removing local album $album from DB");
// delete assets in DB unless they are remote or part of some other album // delete assets in DB unless they are remote or part of some other album
deleteCandidates.addAll( deleteCandidates.addAll(
await album.assets.filter().remoteIdIsNull().findAll(), await _assetRepository.getByAlbum(album, state: AssetState.local),
); );
} else if (album.shared) { } else if (album.shared) {
final User user = Store.get(StoreKey.currentUser);
// delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner // delete assets in DB unless they belong to this user or are part of some other shared album or belong to a partner
final userIds = await _db.users final userIds =
.filter() (await _userRepository.getAllAccessible()).map((user) => user.isarId);
.isPartnerSharedWithEqualTo(true) final orphanedAssets =
.isarIdProperty() await _assetRepository.getByAlbum(album, notOwnedBy: userIds);
.findAll();
userIds.add(user.isarId);
final orphanedAssets = await album.assets
.filter()
.not()
.anyOf(userIds, (q, int id) => q.ownerIdEqualTo(id))
.findAll();
deleteCandidates.addAll(orphanedAssets); deleteCandidates.addAll(orphanedAssets);
} }
try { try {
final bool ok = await _db.writeTxn(() => _db.albums.delete(album.id)); await _albumRepository.delete(album.id);
assert(ok);
_log.info("Removed local album $album from DB"); _log.info("Removed local album $album from DB");
} catch (e) { } catch (e) {
_log.severe("Failed to remove local album $album from DB", e); _log.severe("Failed to remove local album $album from DB", e);
@@ -509,7 +509,7 @@ class SyncService {
]) async { ]) async {
onDevice.sort((a, b) => a.id.compareTo(b.id)); onDevice.sort((a, b) => a.id.compareTo(b.id));
final inDb = final inDb =
await _db.albums.where().localIdIsNotNull().sortByLocalId().findAll(); await _albumRepository.getAll(remote: false, sortBy: AlbumSort.localId);
final List<Asset> deleteCandidates = []; final List<Asset> deleteCandidates = [];
final List<Asset> existing = []; final List<Asset> existing = [];
assert(inDb.isSorted((a, b) => a.localId!.compareTo(b.localId!)), "sort!"); assert(inDb.isSorted((a, b) => a.localId!.compareTo(b.localId!)), "sort!");
@@ -536,10 +536,9 @@ class SyncService {
"${toDelete.length} assets to delete, ${toUpdate.length} to update", "${toDelete.length} assets to delete, ${toUpdate.length} to update",
); );
if (toDelete.isNotEmpty || toUpdate.isNotEmpty) { if (toDelete.isNotEmpty || toUpdate.isNotEmpty) {
await _db.writeTxn(() async { await _assetRepository.transaction(() async {
await _db.assets.deleteAll(toDelete); await _assetRepository.deleteById(toDelete);
await _db.exifInfos.deleteAll(toDelete); await _assetRepository.updateAll(toUpdate);
await _db.assets.putAll(toUpdate);
}); });
_log.info( _log.info(
"Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB", "Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB",
@@ -570,13 +569,13 @@ class SyncService {
await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) { await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) {
return true; return true;
} }
// general case, e.g. some assets have been deleted or there are excluded albums on iOS // general case, e.g. some assets have been deleted or there are excluded albums on iOS
final inDb = await dbAlbum.assets final inDb = await _assetRepository.getByAlbum(
.filter() dbAlbum,
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId) ownerId: (await _userRepository.me()).isarId,
.sortByChecksum() sortBy: AssetSort.checksum,
.findAll(); );
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
final int assetCountOnDevice = final int assetCountOnDevice =
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
@@ -597,15 +596,14 @@ class SyncService {
"Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync.", "Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync.",
); );
if (assetCountOnDevice != if (assetCountOnDevice !=
_db.eTags.getByIdSync(deviceAlbum.eTagKeyAssetCount)?.assetCount) { (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
await _db.writeTxn( ?.assetCount) {
() => _db.eTags.put( await _eTagRepository.upsertAll([
ETag( ETag(
id: deviceAlbum.eTagKeyAssetCount, id: deviceAlbum.eTagKeyAssetCount,
assetCount: assetCountOnDevice, assetCount: assetCountOnDevice,
),
), ),
); ]);
} }
return false; return false;
} }
@@ -625,23 +623,21 @@ class SyncService {
dbAlbum.thumbnail.value = null; dbAlbum.thumbnail.value = null;
} }
try { try {
await _db.writeTxn(() async { await _assetRepository.transaction(() async {
await _db.assets.putAll(updated); await _assetRepository.updateAll(updated + toUpdate);
await _db.assets.putAll(toUpdate); await _albumRepository.addAssets(dbAlbum, existingInDb + updated);
await dbAlbum.assets await _albumRepository.removeAssets(dbAlbum, toDelete);
.update(link: existingInDb + updated, unlink: toDelete); await _albumRepository.recalculateMetadata(dbAlbum);
await _db.albums.put(dbAlbum); await _albumRepository.update(dbAlbum);
dbAlbum.thumbnail.value ??= await dbAlbum.assets.filter().findFirst(); await _eTagRepository.upsertAll([
await dbAlbum.thumbnail.save();
await _db.eTags.put(
ETag( ETag(
id: deviceAlbum.eTagKeyAssetCount, id: deviceAlbum.eTagKeyAssetCount,
assetCount: assetCountOnDevice, assetCount: assetCountOnDevice,
), ),
); ]);
}); });
_log.info("Synced changes of local album ${deviceAlbum.name} to DB"); _log.info("Synced changes of local album ${deviceAlbum.name} to DB");
} on IsarError catch (e) { } catch (e) {
_log.severe("Failed to update synced album ${deviceAlbum.name} in DB", e); _log.severe("Failed to update synced album ${deviceAlbum.name} in DB", e);
} }
@@ -657,7 +653,8 @@ class SyncService {
final int totalOnDevice = final int totalOnDevice =
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
final int lastKnownTotal = final int lastKnownTotal =
(await _db.eTags.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount ?? (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
?.assetCount ??
0; 0;
if (totalOnDevice <= lastKnownTotal) { if (totalOnDevice <= lastKnownTotal) {
return false; return false;
@@ -675,16 +672,17 @@ class SyncService {
_removeDuplicates(newAssets); _removeDuplicates(newAssets);
final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets); final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets);
try { try {
await _db.writeTxn(() async { await _assetRepository.transaction(() async {
await _db.assets.putAll(updated); await _assetRepository.updateAll(updated);
await dbAlbum.assets.update(link: existingInDb + updated); await _albumRepository.addAssets(dbAlbum, existingInDb + updated);
await _db.albums.put(dbAlbum); await _albumRepository.recalculateMetadata(dbAlbum);
await _db.eTags.put( await _albumRepository.update(dbAlbum);
ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice), await _eTagRepository.upsertAll(
[ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice)],
); );
}); });
_log.info("Fast synced local album ${deviceAlbum.name} to DB"); _log.info("Fast synced local album ${deviceAlbum.name} to DB");
} on IsarError catch (e) { } catch (e) {
_log.severe( _log.severe(
"Failed to fast sync local album ${deviceAlbum.name} to DB", "Failed to fast sync local album ${deviceAlbum.name} to DB",
e, e,
@@ -719,9 +717,9 @@ class SyncService {
final thumb = existingInDb.firstOrNull ?? updated.firstOrNull; final thumb = existingInDb.firstOrNull ?? updated.firstOrNull;
album.thumbnail.value = thumb; album.thumbnail.value = thumb;
try { try {
await _db.writeTxn(() => _db.albums.store(album)); await _albumRepository.create(album);
_log.info("Added a new local album to DB: ${album.name}"); _log.info("Added a new local album to DB: ${album.name}");
} on IsarError catch (e) { } catch (e) {
_log.severe("Failed to add new local album ${album.name} to DB", e); _log.severe("Failed to add new local album ${album.name} to DB", e);
} }
} }
@@ -732,7 +730,7 @@ class SyncService {
) async { ) async {
if (assets.isEmpty) return ([].cast<Asset>(), [].cast<Asset>()); if (assets.isEmpty) return ([].cast<Asset>(), [].cast<Asset>());
final List<Asset?> inDb = await _db.assets.getAllByOwnerIdChecksum( final List<Asset?> inDb = await _assetRepository.getAllByOwnerIdChecksum(
assets.map((a) => a.ownerId).toInt64List(), assets.map((a) => a.ownerId).toInt64List(),
assets.map((a) => a.checksum).toList(growable: false), assets.map((a) => a.checksum).toList(growable: false),
); );
@@ -746,7 +744,7 @@ class SyncService {
} }
if (b.canUpdate(assets[i])) { if (b.canUpdate(assets[i])) {
final updated = b.updatedCopy(assets[i]); final updated = b.updatedCopy(assets[i]);
assert(updated.id != Isar.autoIncrement); assert(updated.isInDb);
toUpsert.add(updated); toUpsert.add(updated);
} else { } else {
existing.add(b); existing.add(b);
@@ -758,24 +756,22 @@ class SyncService {
/// 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)
Future<void> upsertAssetsWithExif(List<Asset> assets) async { Future<void> upsertAssetsWithExif(List<Asset> assets) async {
if (assets.isEmpty) { if (assets.isEmpty) return;
return; final exifInfos = assets.map((e) => e.exifInfo).nonNulls.toList();
}
final exifInfos = assets.map((e) => e.exifInfo).whereNotNull().toList();
try { try {
await _db.writeTxn(() async { await _assetRepository.transaction(() async {
await _db.assets.putAll(assets); await _assetRepository.updateAll(assets);
for (final Asset added in assets) { for (final Asset added in assets) {
added.exifInfo?.id = added.id; added.exifInfo?.id = added.id;
} }
await _db.exifInfos.putAll(exifInfos); await _exifInfoRepository.updateAll(exifInfos);
}); });
_log.info("Upserted ${assets.length} assets into the DB"); _log.info("Upserted ${assets.length} assets into the DB");
} on IsarError catch (e) { } catch (e) {
_log.severe("Failed to upsert ${assets.length} assets into the DB", e); _log.severe("Failed to upsert ${assets.length} assets into the DB", e);
// give details on the errors // give details on the errors
assets.sort(Asset.compareByOwnerChecksum); assets.sort(Asset.compareByOwnerChecksum);
final inDb = await _db.assets.getAllByOwnerIdChecksum( final inDb = await _assetRepository.getAllByOwnerIdChecksum(
assets.map((e) => e.ownerId).toInt64List(), assets.map((e) => e.ownerId).toInt64List(),
assets.map((e) => e.checksum).toList(growable: false), assets.map((e) => e.checksum).toList(growable: false),
); );
@@ -783,7 +779,7 @@ class SyncService {
final Asset a = assets[i]; final Asset a = assets[i];
final Asset? b = inDb[i]; final Asset? b = inDb[i];
if (b == null) { if (b == null) {
if (a.id != Isar.autoIncrement) { if (!a.isInDb) {
_log.warning( _log.warning(
"Trying to update an asset that does not exist in DB:\n$a", "Trying to update an asset that does not exist in DB:\n$a",
); );
@@ -827,19 +823,19 @@ class SyncService {
return deviceAlbum.name != dbAlbum.name || return deviceAlbum.name != dbAlbum.name ||
!deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || !deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) != await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) !=
(await _db.eTags.getById(deviceAlbum.eTagKeyAssetCount)) (await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
?.assetCount; ?.assetCount;
} }
Future<bool> _removeAllLocalAlbumsAndAssets() async { Future<bool> _removeAllLocalAlbumsAndAssets() async {
try { try {
final assets = await _db.assets.where().localIdIsNotNull().findAll(); final assets = await _assetRepository.getAllLocal();
final (toDelete, toUpdate) = final (toDelete, toUpdate) =
_handleAssetRemoval(assets, [], remote: false); _handleAssetRemoval(assets, [], remote: false);
await _db.writeTxn(() async { await _assetRepository.transaction(() async {
await _db.assets.deleteAll(toDelete); await _assetRepository.deleteById(toDelete);
await _db.assets.putAll(toUpdate); await _assetRepository.updateAll(toUpdate);
await _db.albums.where().localIdIsNotNull().deleteAll(); await _albumRepository.deleteAllLocal();
}); });
return true; return true;
} catch (e) { } catch (e) {

View File

@@ -0,0 +1,3 @@
const downloadGroupImage = 'group_image';
const downloadGroupVideo = 'group_video';
const downloadGroupLivePhoto = 'group_livephoto';

View File

@@ -131,11 +131,7 @@ class MultiselectGrid extends HookConsumerWidget {
processing.value = true; processing.value = true;
if (shareLocal) { if (shareLocal) {
// Share = Download + Send to OS specific share sheet // Share = Download + Send to OS specific share sheet
// Filter offline assets since we cannot fetch their original file handleShareAssets(ref, context, selection.value);
final liveAssets = selection.value.nonOfflineOnly(
errorCallback: errorBuilder('asset_action_share_err_offline'.tr()),
);
handleShareAssets(ref, context, liveAssets);
} else { } else {
final ids = final ids =
remoteSelection(errorMessage: "home_page_share_err_local".tr()) remoteSelection(errorMessage: "home_page_share_err_local".tr())

View File

@@ -9,7 +9,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
import 'package:immich_mobile/services/stack.service.dart'; import 'package:immich_mobile/services/stack.service.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
@@ -172,7 +172,16 @@ class BottomGalleryBar extends ConsumerWidget {
} }
shareAsset() { shareAsset() {
ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context); if (asset.isOffline) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'asset_action_share_err_offline'.tr(),
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.read(downloadStateProvider.notifier).shareAsset(asset, context);
} }
void handleEdit() async { void handleEdit() async {
@@ -202,7 +211,17 @@ class BottomGalleryBar extends ConsumerWidget {
if (asset.isLocal) { if (asset.isLocal) {
return; return;
} }
ref.read(imageViewerStateProvider.notifier).downloadAsset( if (asset.isOffline) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'asset_action_share_err_offline'.tr(),
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.read(downloadStateProvider.notifier).downloadAsset(
asset, asset,
context, context,
); );

View File

@@ -5,7 +5,7 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart';
import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart'; import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
@@ -94,7 +94,7 @@ class GalleryAppBar extends ConsumerWidget {
} }
handleDownloadAsset() { handleDownloadAsset() {
ref.read(imageViewerStateProvider.notifier).downloadAsset(asset, context); ref.read(downloadStateProvider.notifier).downloadAsset(asset, context);
} }
return IgnorePointer( return IgnorePointer(

View File

@@ -176,7 +176,7 @@ class LoginForm extends HookConsumerWidget {
populateTestLoginInfo1() { populateTestLoginInfo1() {
usernameController.text = 'testuser@email.com'; usernameController.text = 'testuser@email.com';
passwordController.text = 'password'; passwordController.text = 'password';
serverEndpointController.text = 'http://10.1.15.216:2283/api'; serverEndpointController.text = 'http://192.168.1.16:2283/api';
} }
login() async { login() async {

View File

@@ -1,5 +1,3 @@
library photo_view;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller.dart'; import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller.dart';

View File

@@ -1,5 +1,3 @@
library photo_view_gallery;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart' import 'package:immich_mobile/widgets/photo_view/photo_view.dart'

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.116.0 - API version: 1.116.2
- Generator version: 7.8.0 - Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen - Build package: org.openapitools.codegen.languages.DartClientCodegen
@@ -183,6 +183,7 @@ Class | Method | HTTP request | Description
*ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage | *ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage |
*ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types | *ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types |
*ServerApi* | [**getTheme**](doc//ServerApi.md#gettheme) | **GET** /server/theme | *ServerApi* | [**getTheme**](doc//ServerApi.md#gettheme) | **GET** /server/theme |
*ServerApi* | [**getVersionHistory**](doc//ServerApi.md#getversionhistory) | **GET** /server/version-history |
*ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping | *ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping |
*ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license | *ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license |
*SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions | *SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
@@ -400,6 +401,7 @@ Class | Method | HTTP request | Description
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md) - [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
- [ServerStorageResponseDto](doc//ServerStorageResponseDto.md) - [ServerStorageResponseDto](doc//ServerStorageResponseDto.md)
- [ServerThemeDto](doc//ServerThemeDto.md) - [ServerThemeDto](doc//ServerThemeDto.md)
- [ServerVersionHistoryResponseDto](doc//ServerVersionHistoryResponseDto.md)
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md) - [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
- [SessionResponseDto](doc//SessionResponseDto.md) - [SessionResponseDto](doc//SessionResponseDto.md)
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
@@ -416,6 +418,7 @@ Class | Method | HTTP request | Description
- [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigDto](doc//SystemConfigDto.md)
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
- [SystemConfigFacesDto](doc//SystemConfigFacesDto.md) - [SystemConfigFacesDto](doc//SystemConfigFacesDto.md)
- [SystemConfigGeneratedImageDto](doc//SystemConfigGeneratedImageDto.md)
- [SystemConfigImageDto](doc//SystemConfigImageDto.md) - [SystemConfigImageDto](doc//SystemConfigImageDto.md)
- [SystemConfigJobDto](doc//SystemConfigJobDto.md) - [SystemConfigJobDto](doc//SystemConfigJobDto.md)
- [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md) - [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md)

View File

@@ -213,6 +213,7 @@ part 'model/server_ping_response.dart';
part 'model/server_stats_response_dto.dart'; part 'model/server_stats_response_dto.dart';
part 'model/server_storage_response_dto.dart'; part 'model/server_storage_response_dto.dart';
part 'model/server_theme_dto.dart'; part 'model/server_theme_dto.dart';
part 'model/server_version_history_response_dto.dart';
part 'model/server_version_response_dto.dart'; part 'model/server_version_response_dto.dart';
part 'model/session_response_dto.dart'; part 'model/session_response_dto.dart';
part 'model/shared_link_create_dto.dart'; part 'model/shared_link_create_dto.dart';
@@ -229,6 +230,7 @@ part 'model/stack_update_dto.dart';
part 'model/system_config_dto.dart'; part 'model/system_config_dto.dart';
part 'model/system_config_f_fmpeg_dto.dart'; part 'model/system_config_f_fmpeg_dto.dart';
part 'model/system_config_faces_dto.dart'; part 'model/system_config_faces_dto.dart';
part 'model/system_config_generated_image_dto.dart';
part 'model/system_config_image_dto.dart'; part 'model/system_config_image_dto.dart';
part 'model/system_config_job_dto.dart'; part 'model/system_config_job_dto.dart';
part 'model/system_config_library_dto.dart'; part 'model/system_config_library_dto.dart';

View File

@@ -383,7 +383,7 @@ class SearchApi {
/// Parameters: /// Parameters:
/// ///
/// * [RandomSearchDto] randomSearchDto (required): /// * [RandomSearchDto] randomSearchDto (required):
Future<SearchResponseDto?> searchRandom(RandomSearchDto randomSearchDto,) async { Future<List<AssetResponseDto>?> searchRandom(RandomSearchDto randomSearchDto,) async {
final response = await searchRandomWithHttpInfo(randomSearchDto,); final response = await searchRandomWithHttpInfo(randomSearchDto,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
@@ -392,8 +392,11 @@ class SearchApi {
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string. // FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchResponseDto',) as SearchResponseDto; final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
.cast<AssetResponseDto>()
.toList(growable: false);
} }
return null; return null;
} }

View File

@@ -418,6 +418,50 @@ class ServerApi {
return null; return null;
} }
/// Performs an HTTP 'GET /server/version-history' operation and returns the [Response].
Future<Response> getVersionHistoryWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server/version-history';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<List<ServerVersionHistoryResponseDto>?> getVersionHistory() async {
final response = await getVersionHistoryWithHttpInfo();
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<ServerVersionHistoryResponseDto>') as List)
.cast<ServerVersionHistoryResponseDto>()
.toList(growable: false);
}
return null;
}
/// Performs an HTTP 'GET /server/ping' operation and returns the [Response]. /// Performs an HTTP 'GET /server/ping' operation and returns the [Response].
Future<Response> pingServerWithHttpInfo() async { Future<Response> pingServerWithHttpInfo() async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations

View File

@@ -480,6 +480,8 @@ class ApiClient {
return ServerStorageResponseDto.fromJson(value); return ServerStorageResponseDto.fromJson(value);
case 'ServerThemeDto': case 'ServerThemeDto':
return ServerThemeDto.fromJson(value); return ServerThemeDto.fromJson(value);
case 'ServerVersionHistoryResponseDto':
return ServerVersionHistoryResponseDto.fromJson(value);
case 'ServerVersionResponseDto': case 'ServerVersionResponseDto':
return ServerVersionResponseDto.fromJson(value); return ServerVersionResponseDto.fromJson(value);
case 'SessionResponseDto': case 'SessionResponseDto':
@@ -512,6 +514,8 @@ class ApiClient {
return SystemConfigFFmpegDto.fromJson(value); return SystemConfigFFmpegDto.fromJson(value);
case 'SystemConfigFacesDto': case 'SystemConfigFacesDto':
return SystemConfigFacesDto.fromJson(value); return SystemConfigFacesDto.fromJson(value);
case 'SystemConfigGeneratedImageDto':
return SystemConfigGeneratedImageDto.fromJson(value);
case 'SystemConfigImageDto': case 'SystemConfigImageDto':
return SystemConfigImageDto.fromJson(value); return SystemConfigImageDto.fromJson(value);
case 'SystemConfigJobDto': case 'SystemConfigJobDto':

View File

@@ -29,7 +29,6 @@ class RandomSearchDto {
this.libraryId, this.libraryId,
this.make, this.make,
this.model, this.model,
this.page,
this.personIds = const [], this.personIds = const [],
this.size, this.size,
this.state, this.state,
@@ -145,15 +144,6 @@ class RandomSearchDto {
String? model; String? model;
/// Minimum value: 1
///
/// 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.
///
num? page;
List<String> personIds; List<String> personIds;
/// Minimum value: 1 /// Minimum value: 1
@@ -276,7 +266,6 @@ class RandomSearchDto {
other.libraryId == libraryId && other.libraryId == libraryId &&
other.make == make && other.make == make &&
other.model == model && other.model == model &&
other.page == page &&
_deepEquality.equals(other.personIds, personIds) && _deepEquality.equals(other.personIds, personIds) &&
other.size == size && other.size == size &&
other.state == state && other.state == state &&
@@ -312,7 +301,6 @@ class RandomSearchDto {
(libraryId == null ? 0 : libraryId!.hashCode) + (libraryId == null ? 0 : libraryId!.hashCode) +
(make == null ? 0 : make!.hashCode) + (make == null ? 0 : make!.hashCode) +
(model == null ? 0 : model!.hashCode) + (model == null ? 0 : model!.hashCode) +
(page == null ? 0 : page!.hashCode) +
(personIds.hashCode) + (personIds.hashCode) +
(size == null ? 0 : size!.hashCode) + (size == null ? 0 : size!.hashCode) +
(state == null ? 0 : state!.hashCode) + (state == null ? 0 : state!.hashCode) +
@@ -330,7 +318,7 @@ class RandomSearchDto {
(withStacked == null ? 0 : withStacked!.hashCode); (withStacked == null ? 0 : withStacked!.hashCode);
@override @override
String toString() => 'RandomSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; String toString() => 'RandomSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -413,11 +401,6 @@ class RandomSearchDto {
json[r'model'] = this.model; json[r'model'] = this.model;
} else { } else {
// json[r'model'] = null; // json[r'model'] = null;
}
if (this.page != null) {
json[r'page'] = this.page;
} else {
// json[r'page'] = null;
} }
json[r'personIds'] = this.personIds; json[r'personIds'] = this.personIds;
if (this.size != null) { if (this.size != null) {
@@ -514,7 +497,6 @@ class RandomSearchDto {
libraryId: mapValueOfType<String>(json, r'libraryId'), libraryId: mapValueOfType<String>(json, r'libraryId'),
make: mapValueOfType<String>(json, r'make'), make: mapValueOfType<String>(json, r'make'),
model: mapValueOfType<String>(json, r'model'), model: mapValueOfType<String>(json, r'model'),
page: num.parse('${json[r'page']}'),
personIds: json[r'personIds'] is Iterable personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],

View File

@@ -28,6 +28,10 @@ class ServerAboutResponseDto {
this.sourceCommit, this.sourceCommit,
this.sourceRef, this.sourceRef,
this.sourceUrl, this.sourceUrl,
this.thirdPartyBugFeatureUrl,
this.thirdPartyDocumentationUrl,
this.thirdPartySourceUrl,
this.thirdPartySupportUrl,
required this.version, required this.version,
required this.versionUrl, required this.versionUrl,
}); });
@@ -146,6 +150,38 @@ class ServerAboutResponseDto {
/// ///
String? sourceUrl; String? sourceUrl;
///
/// 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? thirdPartyBugFeatureUrl;
///
/// 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? thirdPartyDocumentationUrl;
///
/// 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? thirdPartySourceUrl;
///
/// 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? thirdPartySupportUrl;
String version; String version;
String versionUrl; String versionUrl;
@@ -167,6 +203,10 @@ class ServerAboutResponseDto {
other.sourceCommit == sourceCommit && other.sourceCommit == sourceCommit &&
other.sourceRef == sourceRef && other.sourceRef == sourceRef &&
other.sourceUrl == sourceUrl && other.sourceUrl == sourceUrl &&
other.thirdPartyBugFeatureUrl == thirdPartyBugFeatureUrl &&
other.thirdPartyDocumentationUrl == thirdPartyDocumentationUrl &&
other.thirdPartySourceUrl == thirdPartySourceUrl &&
other.thirdPartySupportUrl == thirdPartySupportUrl &&
other.version == version && other.version == version &&
other.versionUrl == versionUrl; other.versionUrl == versionUrl;
@@ -188,11 +228,15 @@ class ServerAboutResponseDto {
(sourceCommit == null ? 0 : sourceCommit!.hashCode) + (sourceCommit == null ? 0 : sourceCommit!.hashCode) +
(sourceRef == null ? 0 : sourceRef!.hashCode) + (sourceRef == null ? 0 : sourceRef!.hashCode) +
(sourceUrl == null ? 0 : sourceUrl!.hashCode) + (sourceUrl == null ? 0 : sourceUrl!.hashCode) +
(thirdPartyBugFeatureUrl == null ? 0 : thirdPartyBugFeatureUrl!.hashCode) +
(thirdPartyDocumentationUrl == null ? 0 : thirdPartyDocumentationUrl!.hashCode) +
(thirdPartySourceUrl == null ? 0 : thirdPartySourceUrl!.hashCode) +
(thirdPartySupportUrl == null ? 0 : thirdPartySupportUrl!.hashCode) +
(version.hashCode) + (version.hashCode) +
(versionUrl.hashCode); (versionUrl.hashCode);
@override @override
String toString() => 'ServerAboutResponseDto[build=$build, buildImage=$buildImage, buildImageUrl=$buildImageUrl, buildUrl=$buildUrl, exiftool=$exiftool, ffmpeg=$ffmpeg, imagemagick=$imagemagick, libvips=$libvips, licensed=$licensed, nodejs=$nodejs, repository=$repository, repositoryUrl=$repositoryUrl, sourceCommit=$sourceCommit, sourceRef=$sourceRef, sourceUrl=$sourceUrl, version=$version, versionUrl=$versionUrl]'; String toString() => 'ServerAboutResponseDto[build=$build, buildImage=$buildImage, buildImageUrl=$buildImageUrl, buildUrl=$buildUrl, exiftool=$exiftool, ffmpeg=$ffmpeg, imagemagick=$imagemagick, libvips=$libvips, licensed=$licensed, nodejs=$nodejs, repository=$repository, repositoryUrl=$repositoryUrl, sourceCommit=$sourceCommit, sourceRef=$sourceRef, sourceUrl=$sourceUrl, thirdPartyBugFeatureUrl=$thirdPartyBugFeatureUrl, thirdPartyDocumentationUrl=$thirdPartyDocumentationUrl, thirdPartySourceUrl=$thirdPartySourceUrl, thirdPartySupportUrl=$thirdPartySupportUrl, version=$version, versionUrl=$versionUrl]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -266,6 +310,26 @@ class ServerAboutResponseDto {
json[r'sourceUrl'] = this.sourceUrl; json[r'sourceUrl'] = this.sourceUrl;
} else { } else {
// json[r'sourceUrl'] = null; // json[r'sourceUrl'] = null;
}
if (this.thirdPartyBugFeatureUrl != null) {
json[r'thirdPartyBugFeatureUrl'] = this.thirdPartyBugFeatureUrl;
} else {
// json[r'thirdPartyBugFeatureUrl'] = null;
}
if (this.thirdPartyDocumentationUrl != null) {
json[r'thirdPartyDocumentationUrl'] = this.thirdPartyDocumentationUrl;
} else {
// json[r'thirdPartyDocumentationUrl'] = null;
}
if (this.thirdPartySourceUrl != null) {
json[r'thirdPartySourceUrl'] = this.thirdPartySourceUrl;
} else {
// json[r'thirdPartySourceUrl'] = null;
}
if (this.thirdPartySupportUrl != null) {
json[r'thirdPartySupportUrl'] = this.thirdPartySupportUrl;
} else {
// json[r'thirdPartySupportUrl'] = null;
} }
json[r'version'] = this.version; json[r'version'] = this.version;
json[r'versionUrl'] = this.versionUrl; json[r'versionUrl'] = this.versionUrl;
@@ -296,6 +360,10 @@ class ServerAboutResponseDto {
sourceCommit: mapValueOfType<String>(json, r'sourceCommit'), sourceCommit: mapValueOfType<String>(json, r'sourceCommit'),
sourceRef: mapValueOfType<String>(json, r'sourceRef'), sourceRef: mapValueOfType<String>(json, r'sourceRef'),
sourceUrl: mapValueOfType<String>(json, r'sourceUrl'), sourceUrl: mapValueOfType<String>(json, r'sourceUrl'),
thirdPartyBugFeatureUrl: mapValueOfType<String>(json, r'thirdPartyBugFeatureUrl'),
thirdPartyDocumentationUrl: mapValueOfType<String>(json, r'thirdPartyDocumentationUrl'),
thirdPartySourceUrl: mapValueOfType<String>(json, r'thirdPartySourceUrl'),
thirdPartySupportUrl: mapValueOfType<String>(json, r'thirdPartySupportUrl'),
version: mapValueOfType<String>(json, r'version')!, version: mapValueOfType<String>(json, r'version')!,
versionUrl: mapValueOfType<String>(json, r'versionUrl')!, versionUrl: mapValueOfType<String>(json, r'versionUrl')!,
); );

View File

@@ -0,0 +1,115 @@
//
// 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 ServerVersionHistoryResponseDto {
/// Returns a new [ServerVersionHistoryResponseDto] instance.
ServerVersionHistoryResponseDto({
required this.createdAt,
required this.id,
required this.version,
});
DateTime createdAt;
String id;
String version;
@override
bool operator ==(Object other) => identical(this, other) || other is ServerVersionHistoryResponseDto &&
other.createdAt == createdAt &&
other.id == id &&
other.version == version;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(createdAt.hashCode) +
(id.hashCode) +
(version.hashCode);
@override
String toString() => 'ServerVersionHistoryResponseDto[createdAt=$createdAt, id=$id, version=$version]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
json[r'id'] = this.id;
json[r'version'] = this.version;
return json;
}
/// Returns a new [ServerVersionHistoryResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ServerVersionHistoryResponseDto? fromJson(dynamic value) {
upgradeDto(value, "ServerVersionHistoryResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return ServerVersionHistoryResponseDto(
createdAt: mapDateTime(json, r'createdAt', r'')!,
id: mapValueOfType<String>(json, r'id')!,
version: mapValueOfType<String>(json, r'version')!,
);
}
return null;
}
static List<ServerVersionHistoryResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <ServerVersionHistoryResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = ServerVersionHistoryResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, ServerVersionHistoryResponseDto> mapFromJson(dynamic json) {
final map = <String, ServerVersionHistoryResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = ServerVersionHistoryResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of ServerVersionHistoryResponseDto-objects as value to a dart map
static Map<String, List<ServerVersionHistoryResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<ServerVersionHistoryResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = ServerVersionHistoryResponseDto.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',
'version',
};
}

View File

@@ -0,0 +1,118 @@
//
// 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 SystemConfigGeneratedImageDto {
/// Returns a new [SystemConfigGeneratedImageDto] instance.
SystemConfigGeneratedImageDto({
required this.format,
required this.quality,
required this.size,
});
ImageFormat format;
/// Minimum value: 1
/// Maximum value: 100
int quality;
/// Minimum value: 1
int size;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigGeneratedImageDto &&
other.format == format &&
other.quality == quality &&
other.size == size;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(format.hashCode) +
(quality.hashCode) +
(size.hashCode);
@override
String toString() => 'SystemConfigGeneratedImageDto[format=$format, quality=$quality, size=$size]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'format'] = this.format;
json[r'quality'] = this.quality;
json[r'size'] = this.size;
return json;
}
/// Returns a new [SystemConfigGeneratedImageDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SystemConfigGeneratedImageDto? fromJson(dynamic value) {
upgradeDto(value, "SystemConfigGeneratedImageDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SystemConfigGeneratedImageDto(
format: ImageFormat.fromJson(json[r'format'])!,
quality: mapValueOfType<int>(json, r'quality')!,
size: mapValueOfType<int>(json, r'size')!,
);
}
return null;
}
static List<SystemConfigGeneratedImageDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SystemConfigGeneratedImageDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SystemConfigGeneratedImageDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SystemConfigGeneratedImageDto> mapFromJson(dynamic json) {
final map = <String, SystemConfigGeneratedImageDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SystemConfigGeneratedImageDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SystemConfigGeneratedImageDto-objects as value to a dart map
static Map<String, List<SystemConfigGeneratedImageDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SystemConfigGeneratedImageDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SystemConfigGeneratedImageDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'format',
'quality',
'size',
};
}

View File

@@ -15,64 +15,42 @@ class SystemConfigImageDto {
SystemConfigImageDto({ SystemConfigImageDto({
required this.colorspace, required this.colorspace,
required this.extractEmbedded, required this.extractEmbedded,
required this.previewFormat, required this.preview,
required this.previewSize, required this.thumbnail,
required this.quality,
required this.thumbnailFormat,
required this.thumbnailSize,
}); });
Colorspace colorspace; Colorspace colorspace;
bool extractEmbedded; bool extractEmbedded;
ImageFormat previewFormat; SystemConfigGeneratedImageDto preview;
/// Minimum value: 1 SystemConfigGeneratedImageDto thumbnail;
int previewSize;
/// Minimum value: 1
/// Maximum value: 100
int quality;
ImageFormat thumbnailFormat;
/// Minimum value: 1
int thumbnailSize;
@override @override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto && bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto &&
other.colorspace == colorspace && other.colorspace == colorspace &&
other.extractEmbedded == extractEmbedded && other.extractEmbedded == extractEmbedded &&
other.previewFormat == previewFormat && other.preview == preview &&
other.previewSize == previewSize && other.thumbnail == thumbnail;
other.quality == quality &&
other.thumbnailFormat == thumbnailFormat &&
other.thumbnailSize == thumbnailSize;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(colorspace.hashCode) + (colorspace.hashCode) +
(extractEmbedded.hashCode) + (extractEmbedded.hashCode) +
(previewFormat.hashCode) + (preview.hashCode) +
(previewSize.hashCode) + (thumbnail.hashCode);
(quality.hashCode) +
(thumbnailFormat.hashCode) +
(thumbnailSize.hashCode);
@override @override
String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]'; String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, preview=$preview, thumbnail=$thumbnail]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'colorspace'] = this.colorspace; json[r'colorspace'] = this.colorspace;
json[r'extractEmbedded'] = this.extractEmbedded; json[r'extractEmbedded'] = this.extractEmbedded;
json[r'previewFormat'] = this.previewFormat; json[r'preview'] = this.preview;
json[r'previewSize'] = this.previewSize; json[r'thumbnail'] = this.thumbnail;
json[r'quality'] = this.quality;
json[r'thumbnailFormat'] = this.thumbnailFormat;
json[r'thumbnailSize'] = this.thumbnailSize;
return json; return json;
} }
@@ -87,11 +65,8 @@ class SystemConfigImageDto {
return SystemConfigImageDto( return SystemConfigImageDto(
colorspace: Colorspace.fromJson(json[r'colorspace'])!, colorspace: Colorspace.fromJson(json[r'colorspace'])!,
extractEmbedded: mapValueOfType<bool>(json, r'extractEmbedded')!, extractEmbedded: mapValueOfType<bool>(json, r'extractEmbedded')!,
previewFormat: ImageFormat.fromJson(json[r'previewFormat'])!, preview: SystemConfigGeneratedImageDto.fromJson(json[r'preview'])!,
previewSize: mapValueOfType<int>(json, r'previewSize')!, thumbnail: SystemConfigGeneratedImageDto.fromJson(json[r'thumbnail'])!,
quality: mapValueOfType<int>(json, r'quality')!,
thumbnailFormat: ImageFormat.fromJson(json[r'thumbnailFormat'])!,
thumbnailSize: mapValueOfType<int>(json, r'thumbnailSize')!,
); );
} }
return null; return null;
@@ -141,11 +116,8 @@ class SystemConfigImageDto {
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'colorspace', 'colorspace',
'extractEmbedded', 'extractEmbedded',
'previewFormat', 'preview',
'previewSize', 'thumbnail',
'quality',
'thumbnailFormat',
'thumbnailSize',
}; };
} }

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