Compare commits
1 Commits
fix/docs-c
...
fix/scroll
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e628e6a807 |
2
.github/workflows/cli.yml
vendored
2
.github/workflows/cli.yml
vendored
@@ -88,7 +88,7 @@ jobs:
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v6.7.0
|
||||
with:
|
||||
file: cli/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
4
.github/workflows/docker-cleanup.yml
vendored
4
.github/workflows/docker-cleanup.yml
vendored
@@ -22,7 +22,7 @@ concurrency:
|
||||
jobs:
|
||||
cleanup-images:
|
||||
name: Cleanup Stale Images Tags for ${{ matrix.primary-name }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
|
||||
cleanup-untagged-images:
|
||||
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- cleanup-images
|
||||
strategy:
|
||||
|
||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -173,7 +173,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v6.7.0
|
||||
with:
|
||||
context: ${{ env.context }}
|
||||
file: ${{ env.file }}
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.9.0
|
||||
uses: docker/build-push-action@v6.7.0
|
||||
with:
|
||||
context: ${{ env.context }}
|
||||
file: ${{ env.file }}
|
||||
|
||||
2
.github/workflows/docs-build.yml
vendored
2
.github/workflows/docs-build.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
pre-job:
|
||||
|
||||
180
cli/package-lock.json
generated
180
cli/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.22",
|
||||
"version": "2.2.20",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.22",
|
||||
"version": "2.2.20",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"fast-glob": "^3.3.2",
|
||||
@@ -24,7 +24,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^20.16.9",
|
||||
"@types/node": "^20.16.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
@@ -52,14 +52,14 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.116.2",
|
||||
"version": "1.116.0",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.16.9",
|
||||
"@types/node": "^20.16.5",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -765,16 +765,6 @@
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
|
||||
@@ -835,9 +825,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz",
|
||||
"integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==",
|
||||
"version": "9.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
|
||||
"integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -855,9 +845,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
|
||||
"integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz",
|
||||
"integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -1322,13 +1312,6 @@
|
||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||
"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": {
|
||||
"version": "4.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
|
||||
@@ -1354,9 +1337,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||
"version": "20.16.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
|
||||
"integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1370,17 +1353,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
|
||||
"integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
|
||||
"integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.7.0",
|
||||
"@typescript-eslint/type-utils": "8.7.0",
|
||||
"@typescript-eslint/utils": "8.7.0",
|
||||
"@typescript-eslint/visitor-keys": "8.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.6.0",
|
||||
"@typescript-eslint/type-utils": "8.6.0",
|
||||
"@typescript-eslint/utils": "8.6.0",
|
||||
"@typescript-eslint/visitor-keys": "8.6.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1404,16 +1387,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz",
|
||||
"integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
|
||||
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.7.0",
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/typescript-estree": "8.7.0",
|
||||
"@typescript-eslint/visitor-keys": "8.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.6.0",
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"@typescript-eslint/typescript-estree": "8.6.0",
|
||||
"@typescript-eslint/visitor-keys": "8.6.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1433,14 +1416,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz",
|
||||
"integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz",
|
||||
"integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/visitor-keys": "8.7.0"
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"@typescript-eslint/visitor-keys": "8.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1451,14 +1434,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz",
|
||||
"integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz",
|
||||
"integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.7.0",
|
||||
"@typescript-eslint/utils": "8.7.0",
|
||||
"@typescript-eslint/typescript-estree": "8.6.0",
|
||||
"@typescript-eslint/utils": "8.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
@@ -1476,9 +1459,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz",
|
||||
"integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
|
||||
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1490,14 +1473,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz",
|
||||
"integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz",
|
||||
"integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/visitor-keys": "8.7.0",
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"@typescript-eslint/visitor-keys": "8.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1519,16 +1502,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz",
|
||||
"integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz",
|
||||
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.7.0",
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/typescript-estree": "8.7.0"
|
||||
"@typescript-eslint/scope-manager": "8.6.0",
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"@typescript-eslint/typescript-estree": "8.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1542,13 +1525,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz",
|
||||
"integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz",
|
||||
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2187,24 +2170,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.11.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz",
|
||||
"integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==",
|
||||
"version": "9.10.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
|
||||
"integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.11.0",
|
||||
"@eslint/config-array": "^0.18.0",
|
||||
"@eslint/core": "^0.6.0",
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "9.11.1",
|
||||
"@eslint/plugin-kit": "^0.2.0",
|
||||
"@eslint/js": "9.10.0",
|
||||
"@eslint/plugin-kit": "^0.1.0",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.3.0",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
@@ -2355,13 +2335,6 @@
|
||||
"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": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
@@ -2374,9 +2347,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
|
||||
"integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
|
||||
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -3148,11 +3121,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mock-fs": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.3.0.tgz",
|
||||
"integrity": "sha512-IMvz1X+RF7vf+ur7qUenXMR7/FSKSIqS3HqFHXcyNI7G0FbpFO8L5lfsUJhl+bhK1AiulVHWKUSxebWauPA+xQ==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz",
|
||||
"integrity": "sha512-2dF2R6YMSZbpip1V1WHKGLNjr/k48uQClqMVb5H3MOvwc9qhYis3/IWbj02qIg/Y8MDXKFF4c5v0rxx2o6xTZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
@@ -3477,17 +3449,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-organize-imports": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
|
||||
"integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
|
||||
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@vue/language-plugin-pug": "^2.0.24",
|
||||
"prettier": ">=2.0",
|
||||
"typescript": ">=2.9",
|
||||
"vue-tsc": "^2.1.0"
|
||||
"vue-tsc": "^2.0.24"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/language-plugin-pug": {
|
||||
"optional": true
|
||||
},
|
||||
"vue-tsc": {
|
||||
"optional": true
|
||||
}
|
||||
@@ -4179,9 +4155,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
|
||||
"integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
|
||||
"version": "5.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
|
||||
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.22",
|
||||
"version": "2.2.20",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
@@ -20,7 +20,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^20.16.9",
|
||||
"@types/node": "^20.16.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
|
||||
@@ -2,37 +2,37 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "4.43.0"
|
||||
constraints = "4.43.0"
|
||||
version = "4.41.0"
|
||||
constraints = "4.41.0"
|
||||
hashes = [
|
||||
"h1:2kDVLD36BOVgBzI9p0WIQ+xjFfMmjaItA0l8SyZWEPo=",
|
||||
"h1:2sGJDAwFEgO8+3y+2suYO+yrjNOzSsihad0hbM3+jPg=",
|
||||
"h1:A1WPQFcdD+7FrFBFrKcx4CiSr75xSmsO93C0e5NBAeQ=",
|
||||
"h1:BuXs/1ohmF4fWyOErY6vNbm7DaEIfbLSepSiZ2ol9I8=",
|
||||
"h1:QPh+X19oyo808sqdeJaVqahZcQgcG1jCi3DA5zpjz6U=",
|
||||
"h1:RI7c7dhSJoIkfou5b8ITRpM5MqsQD3FULj1h/rI4rJk=",
|
||||
"h1:gdI5JTCPjewdGq1bhGAs+V5qCcmJ73N2gtMfuFybJp4=",
|
||||
"h1:h4lnJpCIYZ7dsN9IO2mmwNdWNiQYEPoAEUjLF2sZ5kc=",
|
||||
"h1:jTaExrX/eR7vGT5wayGqH8ZtXS2zyk0WmD3zbAKFIQU=",
|
||||
"h1:l5NKJUOQJ1mHl1eekeXaxUZ+g+8Yv4aGcIN9vuK6GL4=",
|
||||
"h1:sNbvm66/2vc8B/khyioOO8eNaU8nb89x693AN7fQheU=",
|
||||
"h1:tXS4g1yE420AU4mvZ7RrYI+yYTutkRID3l+W0gBH4BM=",
|
||||
"h1:vA+kES7uqmKA9K0U45IXR94jaTQZCHZLCHqMUeGxKMI=",
|
||||
"h1:zV131k79+ob9p4jrLDgztDNvZvt8fvrrzpn0nPikBw8=",
|
||||
"zh:006d111d6eafe6eeb5df2f91bd0ca320f979bd71f8cd8c475f10b2bd94acba55",
|
||||
"zh:031fbb5cac23a841dc18e270cbfcd3ce9f4ba504edbd3c78931f7ed9827220a8",
|
||||
"zh:07a72fe8b55afee99529bf4169ab6abfac5eabcd10968c29101925bcd358b09f",
|
||||
"zh:0d14727d011c2d9df4c3058f527d2409223449ab48b46cbc86922eb553ef77c1",
|
||||
"zh:155ce1333672d26cd18a5866b0761489d91682beffee58e45c3a1b68e8491d3d",
|
||||
"zh:35a2a1939a965335b29ebdbfd759d93a97c0f589d9cd218f537dee6f600e3fb9",
|
||||
"zh:52912fe421e7d911431f77788db2ea13836efd65a2e82385adb52c6a84d4ee90",
|
||||
"zh:57374318d9194ea1db08884b0541a9055823d5970ad48f9a57547ac231163007",
|
||||
"zh:5fb942b9e2553c058fe09fe12fb39dd175cd6715bb41c059c1a70df2bfc64dc1",
|
||||
"zh:63cabd2bda201b09b35a3279d1f813ab71394b9b90fc5cf8962a5eba207803bc",
|
||||
"h1:0mc+YrjQrcctGrGYDmzlcqcgSv9MYB74rvMaZylIKC8=",
|
||||
"h1:0zUx4vk4jOORQqn6xHBF7dO6N6bielFHdJ0mgF4Obn8=",
|
||||
"h1:AsIZW3uLFNOZO7kL/K7/Y/S0IYxUV9Hz85NNk/3TTsA=",
|
||||
"h1:FSgYM4+LHMbX/a4Y1kx7FPPWmXqS3/MQYzvjMJHHHWM=",
|
||||
"h1:Tx6Nh3BWP1x9L3KK/Eyi+ET0T26g3+jf1jyiuqpNIis=",
|
||||
"h1:VRI9wu8P43xxfpeTndRwsisLnqncfnmEYMOEH5zH4pQ=",
|
||||
"h1:YxQqmiES/Yanq/VfGqBEqg+VIO7FGhO88aKoWFHyGIg=",
|
||||
"h1:ZWHiaesjgDLKWlfdNj0oKyj/DWdxcfsO6NINu39zfpY=",
|
||||
"h1:a2aCgDDBz3ccrr8YstIMl7VFnKo1xZAp+rOv59PPJ7U=",
|
||||
"h1:aRyv8tB6wBAF9lKsLEdiHyCqnK5LfZq0FqMXCcUB4UU=",
|
||||
"h1:lXpuO7zv2uD2GzPE1ARxznreRAh+QHTc2lAJ7iOoFgY=",
|
||||
"h1:sA1xq0QNQ4fH8SHXouYNq50xirVD18SamKQwPsBQrrY=",
|
||||
"h1:v7sHvKq7oqMYPn47ULHFyIQsKD9o+6Xg/uHbxQUixEw=",
|
||||
"h1:wo/x4atWyXuWGlfR6h5nH0YwBAmBwTRY27HtWP8ycLo=",
|
||||
"zh:339d26e06dc6fb299ea8aad9476a60fd65bb1d40631ae8eeb81cddf2dd2bebc8",
|
||||
"zh:3dec2ad96ac2c283fd34ce65781b55c4edbb4d5c5cb53da8e31537176c0ed562",
|
||||
"zh:5f63a5f8080319a2fff09d4d49944829fa708723436520787cfb60725ced80cf",
|
||||
"zh:67162c28ccea71cb8141ed15c0637e35621354ebe14878e0b75a8f160fc5505d",
|
||||
"zh:6ac1e07f5347b6395aca690ed22101bb25e957d25f986f760ff673a7adfd5ef6",
|
||||
"zh:70282a723c7b52fcabde2baad41c864ed3a8d69f0c4d27a6b6933cac434cffc6",
|
||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||
"zh:978ee67d3d53970a5c474ab40b00adee97f4153b16804a2b6b7ee205ae69d18a",
|
||||
"zh:bbafdbef631b5c80570087817b42b16b1a76d556d692853a71c47fb48663cf00",
|
||||
"zh:be91b3f2a697cbbb41f65aad2600972d0ede1e962a7d8a00bb3177cb77d86666",
|
||||
"zh:efe168ad4aaa6156ce5a31d4e50e9d54d38ee5a5888412f9e690c0de5d619683",
|
||||
"zh:924cd23abc326c6b3914e2cd9c94c7832c2552e1e9ae258fb9fd9aedaa5f7ce7",
|
||||
"zh:a4b75e4c239879296259e7d54f1befbc7fdc16da2d62d1294e9f73add4cae61e",
|
||||
"zh:a6ceb08feb63b00c7141783b31e45a154c76fd8cdebbdf371074805f0053572d",
|
||||
"zh:afae1843f9ba85f2f6d94108c65cf43a457e83531a632d44d863e935160cb2ba",
|
||||
"zh:bd6628ce60c778960a5755f7010b7e2cc5c6ff0341a21c175341b28058ec843d",
|
||||
"zh:cd30866a1ff99d72b5fa1699db582fa4f25562e6ab21dcc6870324f3056108e0",
|
||||
"zh:df5924cca691a8220aaaebb5cb55c3d6c32ff0a881f198695eff28155eb12b54",
|
||||
"zh:e78d0696c941aba58df1cb36b8a0d25cd5f3963f01d9338fdbda74db58afdd49",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "4.43.0"
|
||||
version = "4.41.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,37 +2,37 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "4.43.0"
|
||||
constraints = "4.43.0"
|
||||
version = "4.41.0"
|
||||
constraints = "4.41.0"
|
||||
hashes = [
|
||||
"h1:2kDVLD36BOVgBzI9p0WIQ+xjFfMmjaItA0l8SyZWEPo=",
|
||||
"h1:2sGJDAwFEgO8+3y+2suYO+yrjNOzSsihad0hbM3+jPg=",
|
||||
"h1:A1WPQFcdD+7FrFBFrKcx4CiSr75xSmsO93C0e5NBAeQ=",
|
||||
"h1:BuXs/1ohmF4fWyOErY6vNbm7DaEIfbLSepSiZ2ol9I8=",
|
||||
"h1:QPh+X19oyo808sqdeJaVqahZcQgcG1jCi3DA5zpjz6U=",
|
||||
"h1:RI7c7dhSJoIkfou5b8ITRpM5MqsQD3FULj1h/rI4rJk=",
|
||||
"h1:gdI5JTCPjewdGq1bhGAs+V5qCcmJ73N2gtMfuFybJp4=",
|
||||
"h1:h4lnJpCIYZ7dsN9IO2mmwNdWNiQYEPoAEUjLF2sZ5kc=",
|
||||
"h1:jTaExrX/eR7vGT5wayGqH8ZtXS2zyk0WmD3zbAKFIQU=",
|
||||
"h1:l5NKJUOQJ1mHl1eekeXaxUZ+g+8Yv4aGcIN9vuK6GL4=",
|
||||
"h1:sNbvm66/2vc8B/khyioOO8eNaU8nb89x693AN7fQheU=",
|
||||
"h1:tXS4g1yE420AU4mvZ7RrYI+yYTutkRID3l+W0gBH4BM=",
|
||||
"h1:vA+kES7uqmKA9K0U45IXR94jaTQZCHZLCHqMUeGxKMI=",
|
||||
"h1:zV131k79+ob9p4jrLDgztDNvZvt8fvrrzpn0nPikBw8=",
|
||||
"zh:006d111d6eafe6eeb5df2f91bd0ca320f979bd71f8cd8c475f10b2bd94acba55",
|
||||
"zh:031fbb5cac23a841dc18e270cbfcd3ce9f4ba504edbd3c78931f7ed9827220a8",
|
||||
"zh:07a72fe8b55afee99529bf4169ab6abfac5eabcd10968c29101925bcd358b09f",
|
||||
"zh:0d14727d011c2d9df4c3058f527d2409223449ab48b46cbc86922eb553ef77c1",
|
||||
"zh:155ce1333672d26cd18a5866b0761489d91682beffee58e45c3a1b68e8491d3d",
|
||||
"zh:35a2a1939a965335b29ebdbfd759d93a97c0f589d9cd218f537dee6f600e3fb9",
|
||||
"zh:52912fe421e7d911431f77788db2ea13836efd65a2e82385adb52c6a84d4ee90",
|
||||
"zh:57374318d9194ea1db08884b0541a9055823d5970ad48f9a57547ac231163007",
|
||||
"zh:5fb942b9e2553c058fe09fe12fb39dd175cd6715bb41c059c1a70df2bfc64dc1",
|
||||
"zh:63cabd2bda201b09b35a3279d1f813ab71394b9b90fc5cf8962a5eba207803bc",
|
||||
"h1:0mc+YrjQrcctGrGYDmzlcqcgSv9MYB74rvMaZylIKC8=",
|
||||
"h1:0zUx4vk4jOORQqn6xHBF7dO6N6bielFHdJ0mgF4Obn8=",
|
||||
"h1:AsIZW3uLFNOZO7kL/K7/Y/S0IYxUV9Hz85NNk/3TTsA=",
|
||||
"h1:FSgYM4+LHMbX/a4Y1kx7FPPWmXqS3/MQYzvjMJHHHWM=",
|
||||
"h1:Tx6Nh3BWP1x9L3KK/Eyi+ET0T26g3+jf1jyiuqpNIis=",
|
||||
"h1:VRI9wu8P43xxfpeTndRwsisLnqncfnmEYMOEH5zH4pQ=",
|
||||
"h1:YxQqmiES/Yanq/VfGqBEqg+VIO7FGhO88aKoWFHyGIg=",
|
||||
"h1:ZWHiaesjgDLKWlfdNj0oKyj/DWdxcfsO6NINu39zfpY=",
|
||||
"h1:a2aCgDDBz3ccrr8YstIMl7VFnKo1xZAp+rOv59PPJ7U=",
|
||||
"h1:aRyv8tB6wBAF9lKsLEdiHyCqnK5LfZq0FqMXCcUB4UU=",
|
||||
"h1:lXpuO7zv2uD2GzPE1ARxznreRAh+QHTc2lAJ7iOoFgY=",
|
||||
"h1:sA1xq0QNQ4fH8SHXouYNq50xirVD18SamKQwPsBQrrY=",
|
||||
"h1:v7sHvKq7oqMYPn47ULHFyIQsKD9o+6Xg/uHbxQUixEw=",
|
||||
"h1:wo/x4atWyXuWGlfR6h5nH0YwBAmBwTRY27HtWP8ycLo=",
|
||||
"zh:339d26e06dc6fb299ea8aad9476a60fd65bb1d40631ae8eeb81cddf2dd2bebc8",
|
||||
"zh:3dec2ad96ac2c283fd34ce65781b55c4edbb4d5c5cb53da8e31537176c0ed562",
|
||||
"zh:5f63a5f8080319a2fff09d4d49944829fa708723436520787cfb60725ced80cf",
|
||||
"zh:67162c28ccea71cb8141ed15c0637e35621354ebe14878e0b75a8f160fc5505d",
|
||||
"zh:6ac1e07f5347b6395aca690ed22101bb25e957d25f986f760ff673a7adfd5ef6",
|
||||
"zh:70282a723c7b52fcabde2baad41c864ed3a8d69f0c4d27a6b6933cac434cffc6",
|
||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||
"zh:978ee67d3d53970a5c474ab40b00adee97f4153b16804a2b6b7ee205ae69d18a",
|
||||
"zh:bbafdbef631b5c80570087817b42b16b1a76d556d692853a71c47fb48663cf00",
|
||||
"zh:be91b3f2a697cbbb41f65aad2600972d0ede1e962a7d8a00bb3177cb77d86666",
|
||||
"zh:efe168ad4aaa6156ce5a31d4e50e9d54d38ee5a5888412f9e690c0de5d619683",
|
||||
"zh:924cd23abc326c6b3914e2cd9c94c7832c2552e1e9ae258fb9fd9aedaa5f7ce7",
|
||||
"zh:a4b75e4c239879296259e7d54f1befbc7fdc16da2d62d1294e9f73add4cae61e",
|
||||
"zh:a6ceb08feb63b00c7141783b31e45a154c76fd8cdebbdf371074805f0053572d",
|
||||
"zh:afae1843f9ba85f2f6d94108c65cf43a457e83531a632d44d863e935160cb2ba",
|
||||
"zh:bd6628ce60c778960a5755f7010b7e2cc5c6ff0341a21c175341b28058ec843d",
|
||||
"zh:cd30866a1ff99d72b5fa1699db582fa4f25562e6ab21dcc6870324f3056108e0",
|
||||
"zh:df5924cca691a8220aaaebb5cb55c3d6c32ff0a881f198695eff28155eb12b54",
|
||||
"zh:e78d0696c941aba58df1cb36b8a0d25cd5f3963f01d9338fdbda74db58afdd49",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "4.43.0"
|
||||
version = "4.41.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,6 @@ services:
|
||||
IMMICH_BUILD_URL: https://github.com/immich-app/immich/actions/runs/9654404849
|
||||
IMMICH_BUILD_IMAGE: development
|
||||
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:
|
||||
nofile:
|
||||
soft: 1048576
|
||||
|
||||
@@ -91,7 +91,7 @@ services:
|
||||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:11.2.1-ubuntu@sha256:b90c0fdc482913de7a55fe96539bf9e3c4fbcee835d0c2dffc59152bc3964ff7
|
||||
image: grafana/grafana:11.2.0-ubuntu@sha256:8e2c13739563c3da9d45de96c6bcb63ba617cac8c571c060112c7fc8ad6914e9
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ However, when the trash is emptied, the files will re-appear in the main timelin
|
||||
|
||||
### How does smart search work?
|
||||
|
||||
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).
|
||||
Immich uses CLIP models. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
|
||||
|
||||
### How does facial recognition work?
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -20,7 +20,6 @@ The default configuration looks like this:
|
||||
"acceptedVideoCodecs": ["h264"],
|
||||
"targetAudioCodec": "aac",
|
||||
"acceptedAudioCodecs": ["aac", "mp3", "libopus"],
|
||||
"acceptedContainers": ["mov", "ogg", "webm"],
|
||||
"targetResolution": "720",
|
||||
"maxBitrate": "0",
|
||||
"bframes": -1,
|
||||
@@ -33,8 +32,7 @@ The default configuration looks like this:
|
||||
"preferredHwDevice": "auto",
|
||||
"transcode": "required",
|
||||
"tonemap": "hable",
|
||||
"accel": "disabled",
|
||||
"accelDecode": false
|
||||
"accel": "disabled"
|
||||
},
|
||||
"job": {
|
||||
"backgroundTask": {
|
||||
@@ -62,13 +60,10 @@ The default configuration looks like this:
|
||||
"concurrency": 5
|
||||
},
|
||||
"thumbnailGeneration": {
|
||||
"concurrency": 3
|
||||
"concurrency": 5
|
||||
},
|
||||
"videoConversion": {
|
||||
"concurrency": 1
|
||||
},
|
||||
"notifications": {
|
||||
"concurrency": 5
|
||||
}
|
||||
},
|
||||
"logging": {
|
||||
@@ -83,46 +78,40 @@ The default configuration looks like this:
|
||||
"modelName": "ViT-B-32__openai"
|
||||
},
|
||||
"duplicateDetection": {
|
||||
"enabled": true,
|
||||
"maxDistance": 0.01
|
||||
"enabled": false,
|
||||
"maxDistance": 0.03
|
||||
},
|
||||
"facialRecognition": {
|
||||
"enabled": true,
|
||||
"modelName": "buffalo_l",
|
||||
"minScore": 0.7,
|
||||
"maxDistance": 0.5,
|
||||
"maxDistance": 0.6,
|
||||
"minFaces": 3
|
||||
}
|
||||
},
|
||||
"map": {
|
||||
"enabled": true,
|
||||
"lightStyle": "https://tiles.immich.cloud/v1/style/light.json",
|
||||
"darkStyle": "https://tiles.immich.cloud/v1/style/dark.json"
|
||||
"lightStyle": "",
|
||||
"darkStyle": ""
|
||||
},
|
||||
"reverseGeocoding": {
|
||||
"enabled": true
|
||||
},
|
||||
"metadata": {
|
||||
"faces": {
|
||||
"import": false
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
"autoLaunch": false,
|
||||
"autoRegister": true,
|
||||
"buttonText": "Login with OAuth",
|
||||
"clientId": "",
|
||||
"clientSecret": "",
|
||||
"defaultStorageQuota": 0,
|
||||
"enabled": false,
|
||||
"issuerUrl": "",
|
||||
"mobileOverrideEnabled": false,
|
||||
"mobileRedirectUri": "",
|
||||
"clientId": "",
|
||||
"clientSecret": "",
|
||||
"scope": "openid email profile",
|
||||
"signingAlgorithm": "RS256",
|
||||
"profileSigningAlgorithm": "none",
|
||||
"storageLabelClaim": "preferred_username",
|
||||
"storageQuotaClaim": "immich_quota"
|
||||
"storageQuotaClaim": "immich_quota",
|
||||
"defaultStorageQuota": 0,
|
||||
"buttonText": "Login with OAuth",
|
||||
"autoRegister": true,
|
||||
"autoLaunch": false,
|
||||
"mobileOverrideEnabled": false,
|
||||
"mobileRedirectUri": ""
|
||||
},
|
||||
"passwordLogin": {
|
||||
"enabled": true
|
||||
@@ -133,16 +122,11 @@ The default configuration looks like this:
|
||||
"template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}"
|
||||
},
|
||||
"image": {
|
||||
"thumbnail": {
|
||||
"format": "webp",
|
||||
"size": 250,
|
||||
"quality": 80
|
||||
},
|
||||
"preview": {
|
||||
"format": "jpeg",
|
||||
"size": 1440,
|
||||
"quality": 80
|
||||
},
|
||||
"thumbnailFormat": "webp",
|
||||
"thumbnailSize": 250,
|
||||
"previewFormat": "jpeg",
|
||||
"previewSize": 1440,
|
||||
"quality": 80,
|
||||
"colorspace": "p3",
|
||||
"extractEmbedded": false
|
||||
},
|
||||
@@ -156,35 +140,23 @@ The default configuration looks like this:
|
||||
"theme": {
|
||||
"customCss": ""
|
||||
},
|
||||
"user": {
|
||||
"deleteDelay": 7
|
||||
},
|
||||
"library": {
|
||||
"scan": {
|
||||
"enabled": true,
|
||||
"cronExpression": "0 0 * * *"
|
||||
},
|
||||
"watch": {
|
||||
"enabled": false
|
||||
"enabled": false,
|
||||
"usePolling": false,
|
||||
"interval": 10000
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"externalDomain": "",
|
||||
"loginPageMessage": ""
|
||||
},
|
||||
"notifications": {
|
||||
"smtp": {
|
||||
"enabled": false,
|
||||
"from": "",
|
||||
"replyTo": "",
|
||||
"transport": {
|
||||
"ignoreCert": false,
|
||||
"host": "",
|
||||
"port": 587,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"deleteDelay": 7
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -42,7 +42,6 @@ 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_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_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"`.
|
||||
`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
29
docs/package-lock.json
generated
@@ -12715,9 +12715,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
@@ -12829,9 +12829,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||
"version": "8.4.40",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
|
||||
"integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -12849,8 +12849,8 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.1.0",
|
||||
"source-map-js": "^1.2.1"
|
||||
"picocolors": "^1.0.1",
|
||||
"source-map-js": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
@@ -15670,10 +15670,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -16092,9 +16091,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.13",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
|
||||
"integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
|
||||
"version": "3.4.12",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
|
||||
"integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
|
||||
8
docs/static/archived-versions.json
vendored
8
docs/static/archived-versions.json
vendored
@@ -1,12 +1,4 @@
|
||||
[
|
||||
{
|
||||
"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",
|
||||
"url": "https://v1.116.0.archive.immich.app"
|
||||
|
||||
249
e2e/package-lock.json
generated
249
e2e/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.116.2",
|
||||
"version": "1.116.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-e2e",
|
||||
"version": "1.116.2",
|
||||
"version": "1.116.0",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
@@ -15,7 +15,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.16.9",
|
||||
"@types/node": "^20.16.5",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"../cli": {
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.22",
|
||||
"version": "2.2.20",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
@@ -64,7 +64,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^20.16.9",
|
||||
"@types/node": "^20.16.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
@@ -92,14 +92,14 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.116.2",
|
||||
"version": "1.116.0",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.16.9",
|
||||
"@types/node": "^20.16.5",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -784,16 +784,6 @@
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
|
||||
@@ -832,9 +822,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz",
|
||||
"integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==",
|
||||
"version": "9.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
|
||||
"integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -852,9 +842,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz",
|
||||
"integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==",
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz",
|
||||
"integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -1131,11 +1121,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@photostructure/tz-lookup": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-11.0.0.tgz",
|
||||
"integrity": "sha512-QMV5/dWtY/MdVPXZs/EApqzyhnqDq1keYEqpS+Xj2uidyaqw2Nk/fWcsszdruIXjdqp1VoWNzsgrO6bUHU1mFw==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-10.0.0.tgz",
|
||||
"integrity": "sha512-8ZAjoj/irCuvUlyEinQ/HB6A8hP3bD1dgTOZvfl1b9nAwqniutFDHOQRcGM6Crea68bOwPj010f0Z4KkmuLHEA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
@@ -1160,13 +1149,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.47.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz",
|
||||
"integrity": "sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==",
|
||||
"version": "1.47.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.1.tgz",
|
||||
"integrity": "sha512-dbWpcNQZ5nj16m+A5UNScYx7HX5trIy7g4phrcitn+Nk83S32EBX/CLU4hiF4RGKX/yRc93AAqtfaXB7JWBd4Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.47.2"
|
||||
"playwright": "1.47.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -1530,13 +1519,6 @@
|
||||
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
|
||||
"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": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz",
|
||||
@@ -1587,9 +1569,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.16.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz",
|
||||
"integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==",
|
||||
"version": "20.16.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
|
||||
"integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1751,17 +1733,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
|
||||
"integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
|
||||
"integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.7.0",
|
||||
"@typescript-eslint/type-utils": "8.7.0",
|
||||
"@typescript-eslint/utils": "8.7.0",
|
||||
"@typescript-eslint/visitor-keys": "8.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.6.0",
|
||||
"@typescript-eslint/type-utils": "8.6.0",
|
||||
"@typescript-eslint/utils": "8.6.0",
|
||||
"@typescript-eslint/visitor-keys": "8.6.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1785,16 +1767,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz",
|
||||
"integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
|
||||
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.7.0",
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/typescript-estree": "8.7.0",
|
||||
"@typescript-eslint/visitor-keys": "8.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.6.0",
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"@typescript-eslint/typescript-estree": "8.6.0",
|
||||
"@typescript-eslint/visitor-keys": "8.6.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1814,14 +1796,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz",
|
||||
"integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz",
|
||||
"integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/visitor-keys": "8.7.0"
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"@typescript-eslint/visitor-keys": "8.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1832,14 +1814,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz",
|
||||
"integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz",
|
||||
"integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.7.0",
|
||||
"@typescript-eslint/utils": "8.7.0",
|
||||
"@typescript-eslint/typescript-estree": "8.6.0",
|
||||
"@typescript-eslint/utils": "8.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
@@ -1857,9 +1839,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz",
|
||||
"integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
|
||||
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1871,14 +1853,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz",
|
||||
"integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz",
|
||||
"integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/visitor-keys": "8.7.0",
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"@typescript-eslint/visitor-keys": "8.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1926,16 +1908,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz",
|
||||
"integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz",
|
||||
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.7.0",
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/typescript-estree": "8.7.0"
|
||||
"@typescript-eslint/scope-manager": "8.6.0",
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"@typescript-eslint/typescript-estree": "8.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1949,13 +1931,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz",
|
||||
"integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz",
|
||||
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2872,17 +2854,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz",
|
||||
"integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==",
|
||||
"version": "6.5.4",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
|
||||
"integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
@@ -2991,24 +2972,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.11.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz",
|
||||
"integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==",
|
||||
"version": "9.10.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
|
||||
"integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.11.0",
|
||||
"@eslint/config-array": "^0.18.0",
|
||||
"@eslint/core": "^0.6.0",
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "9.11.1",
|
||||
"@eslint/plugin-kit": "^0.2.0",
|
||||
"@eslint/js": "9.10.0",
|
||||
"@eslint/plugin-kit": "^0.1.0",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.3.0",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.2",
|
||||
@@ -3160,9 +3138,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/eslint-visitor-keys": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz",
|
||||
"integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
|
||||
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -3268,27 +3246,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/exiftool-vendored": {
|
||||
"version": "28.3.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.3.0.tgz",
|
||||
"integrity": "sha512-2DOSOvj5c1gkbKtubAnlGglxdYp9h55n0GxjK2nypVivoaCdgP/le3MOZRKgEUNObfJHmYHj4u/NnYVneu/gUw==",
|
||||
"version": "28.2.1",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz",
|
||||
"integrity": "sha512-D3YsKErr3BbjKeJzUVsv6CVZ+SQNgAJKPFWVLXu0CBtr24FNuE3CZBXWKWysGu0MjzeDCNwQrQI5+bXUFeiYVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@photostructure/tz-lookup": "^11.0.0",
|
||||
"@photostructure/tz-lookup": "^10.0.0",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"batch-cluster": "^13.0.0",
|
||||
"he": "^1.2.0",
|
||||
"luxon": "^3.5.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"exiftool-vendored.exe": "12.96.0",
|
||||
"exiftool-vendored.pl": "12.96.0"
|
||||
"exiftool-vendored.exe": "12.91.0",
|
||||
"exiftool-vendored.pl": "12.91.0"
|
||||
}
|
||||
},
|
||||
"node_modules/exiftool-vendored.exe": {
|
||||
"version": "12.96.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.96.0.tgz",
|
||||
"integrity": "sha512-pKPN9F/Evw2yyO5/+ml3spbXIqejzOxyF7jEnj8tLU2JPSmIlziPUZ75XIhcPbilX86jVKmuiso7FUDicOg8pQ==",
|
||||
"version": "12.91.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.91.0.tgz",
|
||||
"integrity": "sha512-nxcoGBaJL/D+Wb0jVe8qwyV8QZpRcCzU0aCKhG0S1XNGWGjJJJ4QV851aobcfDwI4NluFOdqkjTSf32pVijvHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -3297,9 +3275,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/exiftool-vendored.pl": {
|
||||
"version": "12.96.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.96.0.tgz",
|
||||
"integrity": "sha512-v4nGnovAMBsTfOWhwAcOiRiq/8kuJOo3GUMHNpug7Mr4jLz3tmWEo7DdNyOYmpcvWbA6smOTG0SmwsrY8fsW+A==",
|
||||
"version": "12.91.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.91.0.tgz",
|
||||
"integrity": "sha512-GZMy9+Jiv8/C7R4uYe1kWtXsAaJdgVezTwYa+wDeoqvReHiX2t5uzkCrzWdjo4LGl5mPQkyKhN7/uPLYk5Ak6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -4164,9 +4142,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.9.3.tgz",
|
||||
"integrity": "sha512-egLIoYSpcd+QUF+UHgobt5YzI2Pkw/H39ou9suW687MY6PmCwPmkNV/4TNjn1p2tX5xO3j0d0sq5hiYE24bSlg==",
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.9.2.tgz",
|
||||
"integrity": "sha512-ILI2xx/I57b20sd7rHZvgiiQrmp2mcotwsAH+5ajbpFQbrYVQdNHYlQhoA5cFb78CgtBOxtC05TeA+mcgkuCqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -5157,13 +5135,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.47.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz",
|
||||
"integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==",
|
||||
"version": "1.47.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.1.tgz",
|
||||
"integrity": "sha512-SUEKi6947IqYbKxRiqnbUobVZY4bF1uu+ZnZNJX9DfU1tlf2UhWfvVjLf01pQx9URsOr18bFVUKXmanYWhbfkw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.47.2"
|
||||
"playwright-core": "1.47.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -5176,9 +5154,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.47.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz",
|
||||
"integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==",
|
||||
"version": "1.47.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.1.tgz",
|
||||
"integrity": "sha512-i1iyJdLftqtt51mEk6AhYFaAJCDx0xQ/O5NU8EKaWFgMjItPVma542Nh/Aq8aLCjIJSzjaiEQGW/nyqLkGF1OQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -5318,17 +5296,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-organize-imports": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.1.0.tgz",
|
||||
"integrity": "sha512-5aWRdCgv645xaa58X8lOxzZoiHAldAPChljr/MT0crXVOWTZ+Svl4hIWlz+niYSlO6ikE5UXkN1JrRvIP2ut0A==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
|
||||
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@vue/language-plugin-pug": "^2.0.24",
|
||||
"prettier": ">=2.0",
|
||||
"typescript": ">=2.9",
|
||||
"vue-tsc": "^2.1.0"
|
||||
"vue-tsc": "^2.0.24"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/language-plugin-pug": {
|
||||
"optional": true
|
||||
},
|
||||
"vue-tsc": {
|
||||
"optional": true
|
||||
}
|
||||
@@ -5814,15 +5796,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.0",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz",
|
||||
"integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==",
|
||||
"version": "4.7.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
|
||||
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.6.1",
|
||||
"engine.io-client": "~6.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6751,9 +6732,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz",
|
||||
"integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.116.2",
|
||||
"version": "1.116.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -25,7 +25,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.16.9",
|
||||
"@types/node": "^20.16.5",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
|
||||
@@ -76,6 +76,7 @@ describe('/asset', () => {
|
||||
let user2Assets: AssetMediaResponseDto[];
|
||||
let locationAsset: AssetMediaResponseDto;
|
||||
let ratingAsset: AssetMediaResponseDto;
|
||||
let facesAsset: AssetMediaResponseDto;
|
||||
|
||||
const setupTests = async () => {
|
||||
await utils.resetDatabase();
|
||||
@@ -235,7 +236,7 @@ describe('/asset', () => {
|
||||
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
// asset faces
|
||||
const facesAsset = await utils.createAsset(admin.accessToken, {
|
||||
facesAsset = await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
filename: 'portrait.jpg',
|
||||
bytes: await readFile(facesAssetFilepath),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:3cdce69fd5663ca47c420ec4d4df8e3545519a4030372f7d2064fb1be2279844 AS builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:157a371e60389919fe4a72dff71ce86eaa5234f59114c23b0b346d0d02c74d39 AS builder-cpu
|
||||
|
||||
FROM builder-cpu AS builder-openvino
|
||||
|
||||
@@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:5501a4fe605abe24de87c2f3d6cf9fd760354416a0cad0296cf284fddcdca9e2 AS prod-cpu
|
||||
FROM python:3.11-slim-bookworm@sha256:669bbd08353610485a94d5d0c976b4b6498c55280fe42c00f7581f85ee9f3121 AS prod-cpu
|
||||
|
||||
FROM prod-cpu AS prod-openvino
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:e3797091302382ea841498bc93a7b0a50f7c1448333d5e946d2d1608d0c5f43d AS builder
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:5f32c5742e2248f2ca07ccae6861371321aba37372bf8e1a80d6f728f1ab4418 AS builder
|
||||
|
||||
ENV TRANSFORMERS_CACHE=/cache \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
|
||||
85
machine-learning/poetry.lock
generated
85
machine-learning/poetry.lock
generated
@@ -2,13 +2,13 @@
|
||||
|
||||
[[package]]
|
||||
name = "aiocache"
|
||||
version = "0.12.3"
|
||||
version = "0.12.2"
|
||||
description = "multi backend asyncio cache"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "aiocache-0.12.3-py2.py3-none-any.whl", hash = "sha256:889086fc24710f431937b87ad3720a289f7fc31c4fd8b68e9f918b9bacd8270d"},
|
||||
{file = "aiocache-0.12.3.tar.gz", hash = "sha256:f528b27bf4d436b497a1d0d1a8f59a542c153ab1e37c3621713cb376d44c4713"},
|
||||
{file = "aiocache-0.12.2-py2.py3-none-any.whl", hash = "sha256:9b6fa30634ab0bfc3ecc44928a91ff07c6ea16d27d55469636b296ebc6eb5918"},
|
||||
{file = "aiocache-0.12.2.tar.gz", hash = "sha256:b41c9a145b050a5dcbae1599f847db6dd445193b1f3bd172d8e0fe0cb9e96684"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -680,13 +680,13 @@ test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi-slim"
|
||||
version = "0.115.0"
|
||||
version = "0.114.2"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "fastapi_slim-0.115.0-py3-none-any.whl", hash = "sha256:27ab44da95b622e68be7a19f06df1960a320b9d94e689b0adfc055bb26ee9be7"},
|
||||
{file = "fastapi_slim-0.115.0.tar.gz", hash = "sha256:b4b962ca2aa0a31010dafdad3d4da99d368a5591223304c6fb385712fad7feb6"},
|
||||
{file = "fastapi_slim-0.114.2-py3-none-any.whl", hash = "sha256:52ae76c53a30ad0fa96beb84c1bf4bef9c72e88c2f7c0473e836f01d7ac3ca6b"},
|
||||
{file = "fastapi_slim-0.114.2.tar.gz", hash = "sha256:76d0a450826fb0fa740268be55ef04c44807da87a94fbbf5f16338b5a4a2d321"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1237,13 +1237,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "huggingface-hub"
|
||||
version = "0.25.1"
|
||||
version = "0.25.0"
|
||||
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "huggingface_hub-0.25.1-py3-none-any.whl", hash = "sha256:a5158ded931b3188f54ea9028097312cb0acd50bffaaa2612014c3c526b44972"},
|
||||
{file = "huggingface_hub-0.25.1.tar.gz", hash = "sha256:9ff7cb327343211fbd06e2b149b8f362fd1e389454f3f14c6db75a4999ee20ff"},
|
||||
{file = "huggingface_hub-0.25.0-py3-none-any.whl", hash = "sha256:e2f357b35d72d5012cfd127108c4e14abcd61ba4ebc90a5a374dc2456cb34e12"},
|
||||
{file = "huggingface_hub-0.25.0.tar.gz", hash = "sha256:fb5fbe6c12fcd99d187ec7db95db9110fb1a20505f23040a5449a717c1a0db4d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1531,13 +1531,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "locust"
|
||||
version = "2.31.8"
|
||||
version = "2.31.6"
|
||||
description = "Developer-friendly load testing framework"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "locust-2.31.8-py3-none-any.whl", hash = "sha256:4194e3d4a0472f1206c51532ed527017f3da1a7d1037ca4b2f0735d5dcd2f78f"},
|
||||
{file = "locust-2.31.8.tar.gz", hash = "sha256:b240c0d3e1724317d9211e81e99fbe42a3469071ef4d34d2ae6a727776d56377"},
|
||||
{file = "locust-2.31.6-py3-none-any.whl", hash = "sha256:004c963c7a588dc15d57d710cdc6a262d85b57936d7fad3c38ac0657aa98fc3b"},
|
||||
{file = "locust-2.31.6.tar.gz", hash = "sha256:03b6da0491d6a0b905692d9ac128d9deec403f40dc605c481a90dbab5126318c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2037,22 +2037,22 @@ reference = "cuda12"
|
||||
|
||||
[[package]]
|
||||
name = "onnxruntime-openvino"
|
||||
version = "1.19.0"
|
||||
version = "1.18.0"
|
||||
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "onnxruntime_openvino-1.19.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8c5658da819b26d9f35f95204e1bdfb74a100a7533e74edab3af6316c1e316e8"},
|
||||
{file = "onnxruntime_openvino-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb8de2a60cf78db6e201b0a489479995d166938e9c53b01ff342dc7f5f8251ff"},
|
||||
{file = "onnxruntime_openvino-1.19.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f3a0b954026286421b3a769c746c403e8f141f3887d1dd601beb7c4dbf77488a"},
|
||||
{file = "onnxruntime_openvino-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:12330922ecdb694ea28dbdcf08c172e47a5a84fee603040691341336ee3e42bc"},
|
||||
{file = "onnxruntime_openvino-1.19.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:be00502b1a46ba1891cbe49049033745f71c0b99df6d24b979f5b4084b9567d0"},
|
||||
{file = "onnxruntime_openvino-1.18.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:565b874d21bcd48126da7d62f57db019f5ec0e1f82ae9b0740afa2ad91f8d331"},
|
||||
{file = "onnxruntime_openvino-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:7f1931060f710a6c8e32121bb73044c4772ef5925802fc8776d3fe1e87ab3f75"},
|
||||
{file = "onnxruntime_openvino-1.18.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb1723d386f70a8e26398d983ebe35d2c25ba56e9cdb382670ebbf1f5139f8ba"},
|
||||
{file = "onnxruntime_openvino-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:874a1e263dd86674593e5a879257650b06a8609c4d5768c3d8ed8dc4ae874b9c"},
|
||||
{file = "onnxruntime_openvino-1.18.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:597eb18f3de7ead69b08a242d74c4573b28bbfba40ca2a1a40f75bf7a834808e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
coloredlogs = "*"
|
||||
flatbuffers = "*"
|
||||
numpy = ">=1.21.6"
|
||||
numpy = ">=1.26.4"
|
||||
packaging = "*"
|
||||
protobuf = "*"
|
||||
sympy = "*"
|
||||
@@ -2576,15 +2576,18 @@ cli = ["click (>=5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.10"
|
||||
version = "0.0.9"
|
||||
description = "A streaming multipart parser for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "python_multipart-0.0.10-py3-none-any.whl", hash = "sha256:2b06ad9e8d50c7a8db80e3b56dab590137b323410605af2be20d62a5f1ba1dc8"},
|
||||
{file = "python_multipart-0.0.10.tar.gz", hash = "sha256:46eb3c6ce6fdda5fb1a03c7e11d490e407c6930a2703fe7aef4da71c374688fa"},
|
||||
{file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"},
|
||||
{file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"},
|
||||
]
|
||||
|
||||
[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]]
|
||||
name = "pywin32"
|
||||
version = "306"
|
||||
@@ -2831,29 +2834,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.8"
|
||||
version = "0.6.6"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2"},
|
||||
{file = "ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c"},
|
||||
{file = "ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5"},
|
||||
{file = "ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f"},
|
||||
{file = "ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb"},
|
||||
{file = "ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f"},
|
||||
{file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0"},
|
||||
{file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87"},
|
||||
{file = "ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098"},
|
||||
{file = "ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0"},
|
||||
{file = "ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750"},
|
||||
{file = "ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce"},
|
||||
{file = "ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa"},
|
||||
{file = "ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44"},
|
||||
{file = "ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a"},
|
||||
{file = "ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263"},
|
||||
{file = "ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc"},
|
||||
{file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"},
|
||||
{file = "ruff-0.6.6-py3-none-linux_armv6l.whl", hash = "sha256:f5bc5398457484fc0374425b43b030e4668ed4d2da8ee7fdda0e926c9f11ccfb"},
|
||||
{file = "ruff-0.6.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:515a698254c9c47bb84335281a170213b3ee5eb47feebe903e1be10087a167ce"},
|
||||
{file = "ruff-0.6.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6bb1b4995775f1837ab70f26698dd73852bbb82e8f70b175d2713c0354fe9182"},
|
||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69c546f412dfae8bb9cc4f27f0e45cdd554e42fecbb34f03312b93368e1cd0a6"},
|
||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59627e97364329e4eae7d86fa7980c10e2b129e2293d25c478ebcb861b3e3fd6"},
|
||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94c3f78c3d32190aafbb6bc5410c96cfed0a88aadb49c3f852bbc2aa9783a7d8"},
|
||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:704da526c1e137f38c8a067a4a975fe6834b9f8ba7dbc5fd7503d58148851b8f"},
|
||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efeede5815a24104579a0f6320660536c5ffc1c91ae94f8c65659af915fb9de9"},
|
||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e368aef0cc02ca3593eae2fb8186b81c9c2b3f39acaaa1108eb6b4d04617e61f"},
|
||||
{file = "ruff-0.6.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2653fc3b2a9315bd809725c88dd2446550099728d077a04191febb5ea79a4f79"},
|
||||
{file = "ruff-0.6.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bb858cd9ce2d062503337c5b9784d7b583bcf9d1a43c4df6ccb5eab774fbafcb"},
|
||||
{file = "ruff-0.6.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:488f8e15c01ea9afb8c0ba35d55bd951f484d0c1b7c5fd746ce3c47ccdedce68"},
|
||||
{file = "ruff-0.6.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:aefb0bd15f1cfa4c9c227b6120573bb3d6c4ee3b29fb54a5ad58f03859bc43c6"},
|
||||
{file = "ruff-0.6.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a4c0698cc780bcb2c61496cbd56b6a3ac0ad858c966652f7dbf4ceb029252fbe"},
|
||||
{file = "ruff-0.6.6-py3-none-win32.whl", hash = "sha256:aadf81ddc8ab5b62da7aae78a91ec933cbae9f8f1663ec0325dae2c364e4ad84"},
|
||||
{file = "ruff-0.6.6-py3-none-win_amd64.whl", hash = "sha256:0adb801771bc1f1b8cf4e0a6fdc30776e7c1894810ff3b344e50da82ef50eeb1"},
|
||||
{file = "ruff-0.6.6-py3-none-win_arm64.whl", hash = "sha256:4b4d32c137bc781c298964dd4e52f07d6f7d57c03eae97a72d97856844aa510a"},
|
||||
{file = "ruff-0.6.6.tar.gz", hash = "sha256:0fc030b6fd14814d69ac0196396f6761921bd20831725c7361e1b8100b818034"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.116.2"
|
||||
version = "1.116.0"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -64,19 +64,19 @@ custom_lint:
|
||||
allowed:
|
||||
# required / wanted
|
||||
- lib/entities/*.entity.dart
|
||||
- lib/repositories/{album,asset,backup,database,etag,exif_info,user}.repository.dart
|
||||
# acceptable exceptions for the time being (until Isar is fully replaced)
|
||||
- lib/repositories/{album,asset,backup,exif_info,user}.repository.dart
|
||||
# acceptable exceptions for the time being
|
||||
- integration_test/test_utils/general_helper.dart
|
||||
- lib/main.dart
|
||||
- lib/pages/common/album_asset_selection.page.dart
|
||||
- lib/routing/router.dart
|
||||
- lib/services/immich_logger.service.dart # not really a service... more a util
|
||||
- lib/utils/{db,migration,renderlist_generator}.dart
|
||||
- lib/widgets/asset_grid/asset_grid_data_structure.dart
|
||||
- test/**.dart
|
||||
# refactor the remaining providers
|
||||
# refactor to make the providers and services testable
|
||||
- lib/pages/common/album_asset_selection.page.dart
|
||||
- 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
|
||||
- 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/{asset,background,backup,immich_logger,sync}.service.dart
|
||||
- lib/widgets/asset_grid/asset_grid_data_structure.dart
|
||||
|
||||
- import_rule_openapi:
|
||||
message: openapi must only be used through ApiRepositories
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 161,
|
||||
"android.injected.version.name" => "1.116.2",
|
||||
"android.injected.version.code" => 160,
|
||||
"android.injected.version.name" => "1.116.0",
|
||||
}
|
||||
)
|
||||
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')
|
||||
|
||||
@@ -588,16 +588,5 @@
|
||||
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
|
||||
"viewer_remove_from_stack": "Remove from Stack",
|
||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||
"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"
|
||||
}
|
||||
"viewer_unstack": "Un-Stack"
|
||||
}
|
||||
@@ -186,10 +186,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: lints
|
||||
sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
|
||||
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
version: "4.0.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -367,4 +367,4 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
dart: ">=3.4.0 <4.0.0"
|
||||
|
||||
@@ -11,4 +11,4 @@ dependencies:
|
||||
glob: ^2.1.2
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^5.0.0
|
||||
lints: ^4.0.0
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
PODS:
|
||||
- background_downloader (0.0.1):
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift
|
||||
@@ -101,7 +99,6 @@ PODS:
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
@@ -140,8 +137,6 @@ SPEC REPOS:
|
||||
- Toast
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
background_downloader:
|
||||
:path: ".symlinks/plugins/background_downloader/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
device_info_plus:
|
||||
@@ -194,7 +189,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
background_downloader: 9f788ffc5de45acf87d6380e91ca0841066c18cf
|
||||
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
|
||||
@@ -401,7 +401,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 177;
|
||||
CURRENT_PROJECT_VERSION = 176;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -543,7 +543,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 177;
|
||||
CURRENT_PROJECT_VERSION = 176;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -571,7 +571,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 177;
|
||||
CURRENT_PROJECT_VERSION = 176;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.116.1</string>
|
||||
<string>1.116.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>177</string>
|
||||
<string>176</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"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>"]}}}
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Release"
|
||||
lane :release do
|
||||
increment_version_number(
|
||||
version_number: "1.116.2"
|
||||
version_number: "1.116.0"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -70,6 +70,19 @@ extension AssetListExtension on Iterable<Asset> {
|
||||
}
|
||||
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> {
|
||||
|
||||
@@ -1,43 +1,21 @@
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
|
||||
abstract interface class IAlbumRepository implements IDatabaseRepository {
|
||||
abstract interface class IAlbumRepository {
|
||||
Future<int> count({bool? local});
|
||||
Future<Album> create(Album album);
|
||||
|
||||
Future<Album?> get(int id);
|
||||
|
||||
Future<Album?> getById(int id);
|
||||
Future<Album?> getByName(
|
||||
String name, {
|
||||
bool? shared,
|
||||
bool? remote,
|
||||
});
|
||||
|
||||
Future<List<Album>> getAll({
|
||||
bool? shared,
|
||||
bool? remote,
|
||||
int? ownerId,
|
||||
AlbumSort? sortBy,
|
||||
});
|
||||
|
||||
Future<Album> update(Album album);
|
||||
|
||||
Future<void> delete(int albumId);
|
||||
|
||||
Future<void> deleteAllLocal();
|
||||
|
||||
Future<int> count({bool? local});
|
||||
|
||||
Future<void> addUsers(Album album, List<User> users);
|
||||
|
||||
Future<List<Album>> getAll({bool? shared});
|
||||
Future<void> removeUsers(Album album, List<User> users);
|
||||
|
||||
Future<void> addAssets(Album album, List<Asset> assets);
|
||||
|
||||
Future<void> removeAssets(Album album, List<Asset> assets);
|
||||
|
||||
Future<Album> recalculateMetadata(Album album);
|
||||
}
|
||||
|
||||
enum AlbumSort { remoteId, localId }
|
||||
|
||||
@@ -1,62 +1,27 @@
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/device_asset.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
|
||||
abstract interface class IAssetRepository implements IDatabaseRepository {
|
||||
abstract interface class IAssetRepository {
|
||||
Future<Asset?> getByRemoteId(String id);
|
||||
|
||||
Future<Asset?> getByOwnerIdChecksum(int ownerId, String checksum);
|
||||
|
||||
Future<List<Asset>> getAllByRemoteId(
|
||||
Iterable<String> ids, {
|
||||
AssetState? state,
|
||||
});
|
||||
|
||||
Future<List<Asset?>> getAllByOwnerIdChecksum(
|
||||
List<int> ids,
|
||||
List<String> checksums,
|
||||
);
|
||||
|
||||
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids);
|
||||
Future<List<Asset>> getByAlbum(Album album, {User? notOwnedBy});
|
||||
Future<void> deleteById(List<int> ids);
|
||||
Future<List<Asset>> getAll({
|
||||
required int ownerId,
|
||||
AssetState? state,
|
||||
AssetSort? sortBy,
|
||||
int? limit,
|
||||
bool? remote,
|
||||
int limit = 100,
|
||||
});
|
||||
|
||||
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<void> deleteAllByRemoteId(List<String> ids, {AssetState? state});
|
||||
|
||||
Future<void> deleteById(List<int> ids);
|
||||
|
||||
Future<List<Asset>> getMatches({
|
||||
required List<Asset> assets,
|
||||
required int ownerId,
|
||||
AssetState? state,
|
||||
bool? remote,
|
||||
int limit = 100,
|
||||
});
|
||||
|
||||
Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids);
|
||||
|
||||
Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets);
|
||||
|
||||
Future<void> upsertDuplicatedAssets(Iterable<String> duplicatedAssets);
|
||||
|
||||
Future<List<String>> getAllDuplicatedAssetIds();
|
||||
}
|
||||
|
||||
enum AssetSort { checksum, ownerIdChecksum }
|
||||
|
||||
@@ -4,7 +4,4 @@ abstract interface class IAssetMediaRepository {
|
||||
Future<List<String>> deleteAll(List<String> ids);
|
||||
|
||||
Future<Asset?> get(String id);
|
||||
|
||||
/// Obtaining the correct original filename of the asset
|
||||
Future<String?> getOriginalFilename(String id);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
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<BackupAlbum>> getAllBySelection(BackupSelection backup);
|
||||
|
||||
Future<void> updateAll(List<BackupAlbum> backupAlbums);
|
||||
|
||||
Future<void> deleteAll(List<int> ids);
|
||||
}
|
||||
|
||||
enum BackupAlbumSort { id }
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
abstract interface class IDatabaseRepository {
|
||||
Future<T> transaction<T>(Future<T> Function() callback);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
|
||||
abstract interface class IExifInfoRepository implements IDatabaseRepository {
|
||||
abstract interface class IExifInfoRepository {
|
||||
Future<ExifInfo?> get(int id);
|
||||
|
||||
Future<ExifInfo> update(ExifInfo exifInfo);
|
||||
|
||||
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos);
|
||||
|
||||
Future<void> delete(int id);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,8 @@
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||
|
||||
abstract interface class IUserRepository implements IDatabaseRepository {
|
||||
Future<User?> get(String id);
|
||||
|
||||
abstract interface class IUserRepository {
|
||||
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?> get(String id);
|
||||
Future<List<User>> getAll({bool self = true});
|
||||
Future<User> update(User user);
|
||||
|
||||
Future<void> deleteById(List<int> ids);
|
||||
|
||||
Future<User> me();
|
||||
}
|
||||
|
||||
enum UserSort { id }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -10,7 +9,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
@@ -74,6 +72,7 @@ Future<void> initApp() async {
|
||||
var log = Logger("ImmichErrorLogger");
|
||||
|
||||
FlutterError.onError = (details) {
|
||||
debugPrint("FlutterError - Catch all: $details");
|
||||
FlutterError.presentError(details);
|
||||
log.severe(
|
||||
'FlutterError - Catch all',
|
||||
@@ -83,29 +82,11 @@ Future<void> initApp() async {
|
||||
};
|
||||
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
debugPrint("FlutterError - Catch all: $error");
|
||||
log.severe('PlatformDispatcher - Catch all', error, stack);
|
||||
return true;
|
||||
};
|
||||
|
||||
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 {
|
||||
@@ -207,8 +188,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final router = ref.watch(appRouterProvider);
|
||||
final immichTheme = ref.watch(immichThemeProvider);
|
||||
var router = ref.watch(appRouterProvider);
|
||||
var immichTheme = ref.watch(immichThemeProvider);
|
||||
|
||||
return MaterialApp(
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.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/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
||||
@@ -422,7 +421,6 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const DownloadPanel(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -275,14 +275,28 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
return isSuccess ? remote.toList() : [];
|
||||
}
|
||||
|
||||
Future<void> toggleFavorite(List<Asset> assets, [bool? status]) {
|
||||
Future<void> toggleFavorite(List<Asset> assets, [bool? status]) async {
|
||||
status ??= !assets.every((a) => a.isFavorite);
|
||||
return _assetService.changeFavoriteStatus(assets, status);
|
||||
final newAssets = await _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]) {
|
||||
Future<void> toggleArchive(List<Asset> assets, [bool? status]) async {
|
||||
status ??= !assets.every((a) => a.isArchived);
|
||||
return _assetService.changeArchiveStatus(assets, status);
|
||||
final newAssets = await _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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
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),
|
||||
)),
|
||||
);
|
||||
@@ -0,0 +1,99 @@
|
||||
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),
|
||||
)),
|
||||
);
|
||||
@@ -7,7 +7,6 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.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/models/backup/available_album.model.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
@@ -18,7 +17,6 @@ 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/providers/backup/error_backup_list.provider.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/services/background.service.dart';
|
||||
import 'package:immich_mobile/services/backup.service.dart';
|
||||
@@ -47,7 +45,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
this._db,
|
||||
this._albumMediaRepository,
|
||||
this._fileMediaRepository,
|
||||
this._backupRepository,
|
||||
this.ref,
|
||||
) : super(
|
||||
BackUpState(
|
||||
@@ -98,7 +95,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
final Isar _db;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final IFileMediaRepository _fileMediaRepository;
|
||||
final IBackupRepository _backupRepository;
|
||||
final Ref ref;
|
||||
|
||||
///
|
||||
@@ -259,9 +255,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
state = state.copyWith(availableAlbums: availableAlbums);
|
||||
|
||||
final List<BackupAlbum> excludedBackupAlbums =
|
||||
await _backupRepository.getAllBySelection(BackupSelection.exclude);
|
||||
await _backupService.excludedAlbumsQuery().findAll();
|
||||
final List<BackupAlbum> selectedBackupAlbums =
|
||||
await _backupRepository.getAllBySelection(BackupSelection.select);
|
||||
await _backupService.selectedAlbumsQuery().findAll();
|
||||
|
||||
final Set<AvailableAlbum> selectedAlbums = {};
|
||||
for (final BackupAlbum ba in selectedBackupAlbums) {
|
||||
@@ -771,7 +767,6 @@ final backupProvider =
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
ref.watch(fileMediaRepositoryProvider),
|
||||
ref.watch(backupRepositoryProvider),
|
||||
ref,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -6,10 +6,8 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:fluttertoast/fluttertoast.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/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/services/background.service.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
@@ -27,6 +25,7 @@ import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||
@@ -37,7 +36,6 @@ final manualUploadProvider =
|
||||
ref.watch(localNotificationService),
|
||||
ref.watch(backupProvider.notifier),
|
||||
ref.watch(backupServiceProvider),
|
||||
ref.watch(backupRepositoryProvider),
|
||||
ref,
|
||||
);
|
||||
});
|
||||
@@ -47,14 +45,12 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
||||
final LocalNotificationService _localNotificationService;
|
||||
final BackupNotifier _backupProvider;
|
||||
final BackupService _backupService;
|
||||
final BackupRepository _backupRepository;
|
||||
final Ref ref;
|
||||
|
||||
ManualUploadNotifier(
|
||||
this._localNotificationService,
|
||||
this._backupProvider,
|
||||
this._backupService,
|
||||
this._backupRepository,
|
||||
this.ref,
|
||||
) : super(
|
||||
ManualUploadState(
|
||||
@@ -210,9 +206,9 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
||||
}
|
||||
|
||||
final selectedBackupAlbums =
|
||||
await _backupRepository.getAllBySelection(BackupSelection.select);
|
||||
_backupService.selectedAlbumsQuery().findAllSync();
|
||||
final excludedBackupAlbums =
|
||||
await _backupRepository.getAllBySelection(BackupSelection.exclude);
|
||||
_backupService.excludedAlbumsQuery().findAllSync();
|
||||
|
||||
// Get candidates from selected albums and excluded albums
|
||||
Set<BackupCandidate> candidates =
|
||||
|
||||
@@ -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/models/activities/activity.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/base_api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final activityApiRepositoryProvider = Provider(
|
||||
(ref) => ActivityApiRepository(ref.watch(apiServiceProvider).activitiesApi),
|
||||
);
|
||||
|
||||
class ActivityApiRepository extends ApiRepository
|
||||
class ActivityApiRepository extends BaseApiRepository
|
||||
implements IActivityApiRepository {
|
||||
final ActivitiesApi _api;
|
||||
|
||||
|
||||
@@ -4,36 +4,32 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/album.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
final albumRepositoryProvider =
|
||||
Provider((ref) => AlbumRepository(ref.watch(dbProvider)));
|
||||
|
||||
class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
|
||||
AlbumRepository(super.db);
|
||||
class AlbumRepository implements IAlbumRepository {
|
||||
final Isar _db;
|
||||
|
||||
AlbumRepository(
|
||||
this._db,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<int> count({bool? local}) {
|
||||
final baseQuery = db.albums.where();
|
||||
final QueryBuilder<Album, Album, QAfterWhereClause> query;
|
||||
switch (local) {
|
||||
case null:
|
||||
query = baseQuery.noOp();
|
||||
case true:
|
||||
query = baseQuery.localIdIsNotNull();
|
||||
case false:
|
||||
query = baseQuery.remoteIdIsNotNull();
|
||||
}
|
||||
return query.count();
|
||||
if (local == true) return _db.albums.where().localIdIsNotNull().count();
|
||||
if (local == false) return _db.albums.where().remoteIdIsNotNull().count();
|
||||
return _db.albums.count();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album> create(Album album) => txn(() => db.albums.store(album));
|
||||
Future<Album> create(Album album) =>
|
||||
_db.writeTxn(() => _db.albums.store(album));
|
||||
|
||||
@override
|
||||
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) {
|
||||
query = query.sharedEqualTo(shared);
|
||||
}
|
||||
@@ -46,61 +42,37 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album> update(Album album) => txn(() => db.albums.store(album));
|
||||
Future<Album> update(Album album) =>
|
||||
_db.writeTxn(() => _db.albums.store(album));
|
||||
|
||||
@override
|
||||
Future<void> delete(int albumId) => txn(() => db.albums.delete(albumId));
|
||||
Future<void> delete(int albumId) =>
|
||||
_db.writeTxn(() => _db.albums.delete(albumId));
|
||||
|
||||
@override
|
||||
Future<List<Album>> getAll({
|
||||
bool? shared,
|
||||
bool? remote,
|
||||
int? ownerId,
|
||||
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();
|
||||
}
|
||||
QueryBuilder<Album, Album, QAfterFilterCondition> filterQuery =
|
||||
afterWhere.filter().noOp();
|
||||
Future<List<Album>> getAll({bool? shared}) {
|
||||
final baseQuery = _db.albums.filter();
|
||||
QueryBuilder<Album, Album, QAfterFilterCondition>? query;
|
||||
if (shared != null) {
|
||||
filterQuery = filterQuery.sharedEqualTo(true);
|
||||
query = baseQuery.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();
|
||||
return query?.findAll() ?? _db.albums.where().findAll();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Album?> get(int id) => db.albums.get(id);
|
||||
Future<Album?> getById(int id) => _db.albums.get(id);
|
||||
|
||||
@override
|
||||
Future<void> removeUsers(Album album, List<User> users) =>
|
||||
txn(() => album.sharedUsers.update(unlink: users));
|
||||
_db.writeTxn(() => album.sharedUsers.update(unlink: users));
|
||||
|
||||
@override
|
||||
Future<void> addAssets(Album album, List<Asset> assets) =>
|
||||
txn(() => album.assets.update(link: assets));
|
||||
_db.writeTxn(() => album.assets.update(link: assets));
|
||||
|
||||
@override
|
||||
Future<void> removeAssets(Album album, List<Asset> assets) =>
|
||||
txn(() => album.assets.update(unlink: assets));
|
||||
_db.writeTxn(() => album.assets.update(unlink: assets));
|
||||
|
||||
@override
|
||||
Future<Album> recalculateMetadata(Album album) async {
|
||||
@@ -110,12 +82,4 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
|
||||
await album.assets.filter().updatedAtProperty().max();
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@ import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/base_api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final albumApiRepositoryProvider = Provider(
|
||||
(ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
|
||||
);
|
||||
|
||||
class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
|
||||
class AlbumApiRepository extends BaseApiRepository
|
||||
implements IAlbumApiRepository {
|
||||
final AlbumsApi _api;
|
||||
|
||||
AlbumApiRepository(this._api);
|
||||
@@ -25,7 +26,7 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
|
||||
@override
|
||||
Future<List<Album>> getAll({bool? shared}) async {
|
||||
final dtos = await checkNull(_api.getAllAlbums(shared: shared));
|
||||
return dtos.map(_toAlbum).toList();
|
||||
return dtos.map(_toAlbum).toList().cast();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -5,145 +5,78 @@ import 'package:immich_mobile/entities/album.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/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/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
final assetRepositoryProvider =
|
||||
Provider((ref) => AssetRepository(ref.watch(dbProvider)));
|
||||
|
||||
class AssetRepository extends DatabaseRepository implements IAssetRepository {
|
||||
AssetRepository(super.db);
|
||||
class AssetRepository implements IAssetRepository {
|
||||
final Isar _db;
|
||||
|
||||
AssetRepository(
|
||||
this._db,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<Asset>> getByAlbum(
|
||||
Album album, {
|
||||
Iterable<int> notOwnedBy = const [],
|
||||
int? ownerId,
|
||||
AssetState? state,
|
||||
AssetSort? sortBy,
|
||||
}) {
|
||||
Future<List<Asset>> getByAlbum(Album album, {User? notOwnedBy}) {
|
||||
var query = album.assets.filter();
|
||||
if (notOwnedBy.length == 1) {
|
||||
query = query.not().ownerIdEqualTo(notOwnedBy.first);
|
||||
} else if (notOwnedBy.isNotEmpty) {
|
||||
query =
|
||||
query.not().anyOf(notOwnedBy, (q, int id) => q.ownerIdEqualTo(id));
|
||||
if (notOwnedBy != null) {
|
||||
query = query.not().ownerIdEqualTo(notOwnedBy.isarId);
|
||||
}
|
||||
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();
|
||||
return query.findAll();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteById(List<int> ids) => txn(() async {
|
||||
await db.assets.deleteAll(ids);
|
||||
await db.exifInfos.deleteAll(ids);
|
||||
});
|
||||
Future<void> deleteById(List<int> ids) =>
|
||||
_db.writeTxn(() => _db.assets.deleteAll(ids));
|
||||
|
||||
@override
|
||||
Future<Asset?> getByRemoteId(String id) => db.assets.getByRemoteId(id);
|
||||
Future<Asset?> getByRemoteId(String id) => _db.assets.getByRemoteId(id);
|
||||
|
||||
@override
|
||||
Future<List<Asset>> getAllByRemoteId(
|
||||
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();
|
||||
}
|
||||
}
|
||||
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) =>
|
||||
_db.assets.getAllByRemoteId(ids);
|
||||
|
||||
@override
|
||||
Future<List<Asset>> getAll({
|
||||
required int ownerId,
|
||||
AssetState? state,
|
||||
AssetSort? sortBy,
|
||||
int? limit,
|
||||
bool? remote,
|
||||
int limit = 100,
|
||||
}) {
|
||||
final baseQuery = db.assets.where();
|
||||
final QueryBuilder<Asset, Asset, QAfterFilterCondition> filteredQuery;
|
||||
switch (state) {
|
||||
case null:
|
||||
filteredQuery = baseQuery.ownerIdEqualToAnyChecksum(ownerId).noOp();
|
||||
case AssetState.local:
|
||||
filteredQuery = baseQuery
|
||||
.remoteIdIsNull()
|
||||
.filter()
|
||||
.localIdIsNotNull()
|
||||
.ownerIdEqualTo(ownerId);
|
||||
case AssetState.remote:
|
||||
filteredQuery = baseQuery
|
||||
.localIdIsNull()
|
||||
.filter()
|
||||
.remoteIdIsNotNull()
|
||||
.ownerIdEqualTo(ownerId);
|
||||
case AssetState.merged:
|
||||
filteredQuery = baseQuery
|
||||
.ownerIdEqualToAnyChecksum(ownerId)
|
||||
.filter()
|
||||
.remoteIdIsNotNull()
|
||||
.localIdIsNotNull();
|
||||
if (remote == null) {
|
||||
return _db.assets
|
||||
.where()
|
||||
.ownerIdEqualToAnyChecksum(ownerId)
|
||||
.limit(limit)
|
||||
.findAll();
|
||||
}
|
||||
final QueryBuilder<Asset, Asset, QAfterFilterCondition> query;
|
||||
if (remote) {
|
||||
query = _db.assets
|
||||
.where()
|
||||
.localIdIsNull()
|
||||
.filter()
|
||||
.remoteIdIsNotNull()
|
||||
.ownerIdEqualTo(ownerId);
|
||||
} else {
|
||||
query = _db.assets
|
||||
.where()
|
||||
.remoteIdIsNull()
|
||||
.filter()
|
||||
.localIdIsNotNull()
|
||||
.ownerIdEqualTo(ownerId);
|
||||
}
|
||||
|
||||
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();
|
||||
return query.limit(limit).findAll();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Asset>> updateAll(List<Asset> assets) async {
|
||||
await txn(() => db.assets.putAll(assets));
|
||||
await _db.writeTxn(() => _db.assets.putAll(assets));
|
||||
return assets;
|
||||
}
|
||||
|
||||
@@ -151,20 +84,16 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
|
||||
Future<List<Asset>> getMatches({
|
||||
required List<Asset> assets,
|
||||
required int ownerId,
|
||||
AssetState? state,
|
||||
bool? remote,
|
||||
int limit = 100,
|
||||
}) {
|
||||
final baseQuery = db.assets.where();
|
||||
final QueryBuilder<Asset, Asset, QAfterFilterCondition> query;
|
||||
switch (state) {
|
||||
case null:
|
||||
query = baseQuery.noOp();
|
||||
case AssetState.local:
|
||||
query = baseQuery.remoteIdIsNull().filter().localIdIsNotNull();
|
||||
case AssetState.remote:
|
||||
query = baseQuery.localIdIsNull().filter().remoteIdIsNotNull();
|
||||
case AssetState.merged:
|
||||
query = baseQuery.localIdIsNotNull().filter().remoteIdIsNotNull();
|
||||
if (remote == null) {
|
||||
query = _db.assets.filter().remoteIdIsNotNull().or().localIdIsNotNull();
|
||||
} else if (remote) {
|
||||
query = _db.assets.where().localIdIsNull().filter().remoteIdIsNotNull();
|
||||
} else {
|
||||
query = _db.assets.where().remoteIdIsNull().filter().localIdIsNotNull();
|
||||
}
|
||||
return _getMatchesImpl(query, ownerId, assets, limit);
|
||||
}
|
||||
@@ -172,50 +101,16 @@ class AssetRepository extends DatabaseRepository implements IAssetRepository {
|
||||
@override
|
||||
Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids) =>
|
||||
Platform.isAndroid
|
||||
? db.androidDeviceAssets.getAll(ids.cast())
|
||||
: db.iOSDeviceAssets.getAllById(ids.cast());
|
||||
? _db.androidDeviceAssets.getAll(ids.cast())
|
||||
: _db.iOSDeviceAssets.getAllById(ids.cast());
|
||||
|
||||
@override
|
||||
Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets) => txn(
|
||||
Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets) =>
|
||||
_db.writeTxn(
|
||||
() => Platform.isAndroid
|
||||
? db.androidDeviceAssets.putAll(deviceAssets.cast())
|
||||
: db.iOSDeviceAssets.putAll(deviceAssets.cast()),
|
||||
? _db.androidDeviceAssets.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(
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/base_api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final assetApiRepositoryProvider = Provider(
|
||||
@@ -12,7 +12,8 @@ final assetApiRepositoryProvider = Provider(
|
||||
),
|
||||
);
|
||||
|
||||
class AssetApiRepository extends ApiRepository implements IAssetApiRepository {
|
||||
class AssetApiRepository extends BaseApiRepository
|
||||
implements IAssetApiRepository {
|
||||
final AssetsApi _api;
|
||||
final SearchApi _searchApi;
|
||||
|
||||
|
||||
@@ -43,17 +43,4 @@ class AssetMediaRepository implements IAssetMediaRepository {
|
||||
asset.local = local;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,41 +2,19 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
final backupRepositoryProvider =
|
||||
Provider((ref) => BackupRepository(ref.watch(dbProvider)));
|
||||
|
||||
class BackupRepository extends DatabaseRepository implements IBackupRepository {
|
||||
BackupRepository(super.db);
|
||||
class BackupRepository implements IBackupRepository {
|
||||
final Isar _db;
|
||||
|
||||
@override
|
||||
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();
|
||||
}
|
||||
BackupRepository(
|
||||
this._db,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<String>> getIdsBySelection(BackupSelection backup) =>
|
||||
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));
|
||||
_db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/constants/errors.dart';
|
||||
|
||||
abstract class ApiRepository {
|
||||
abstract class BaseApiRepository {
|
||||
@protected
|
||||
Future<T> checkNull<T>(Future<T?> future) async {
|
||||
final response = await future;
|
||||
if (response == null) throw NoResponseDtoError();
|
||||
@@ -1,28 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -2,30 +2,27 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
final exifInfoRepositoryProvider =
|
||||
Provider((ref) => ExifInfoRepository(ref.watch(dbProvider)));
|
||||
|
||||
class ExifInfoRepository extends DatabaseRepository
|
||||
implements IExifInfoRepository {
|
||||
ExifInfoRepository(super.db);
|
||||
class ExifInfoRepository implements IExifInfoRepository {
|
||||
final Isar _db;
|
||||
|
||||
ExifInfoRepository(
|
||||
this._db,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> delete(int id) => txn(() => db.exifInfos.delete(id));
|
||||
Future<void> delete(int id) => _db.exifInfos.delete(id);
|
||||
|
||||
@override
|
||||
Future<ExifInfo?> get(int id) => db.exifInfos.get(id);
|
||||
Future<ExifInfo?> get(int id) => _db.exifInfos.get(id);
|
||||
|
||||
@override
|
||||
Future<ExifInfo> update(ExifInfo exifInfo) async {
|
||||
await txn(() => db.exifInfos.put(exifInfo));
|
||||
await _db.writeTxn(() => _db.exifInfos.put(exifInfo));
|
||||
return exifInfo;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<ExifInfo>> updateAll(List<ExifInfo> exifInfos) async {
|
||||
await txn(() => db.exifInfos.putAll(exifInfos));
|
||||
return exifInfos;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,8 @@ class FileMediaRepository implements IFileMediaRepository {
|
||||
required String title,
|
||||
String? relativePath,
|
||||
}) async {
|
||||
final entity = await PhotoManager.editor.saveImage(
|
||||
data,
|
||||
filename: title,
|
||||
title: title,
|
||||
relativePath: relativePath,
|
||||
);
|
||||
final entity = await PhotoManager.editor
|
||||
.saveImage(data, title: title, relativePath: relativePath);
|
||||
return AssetMediaRepository.toAsset(entity);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/base_api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final partnerApiRepositoryProvider = Provider(
|
||||
@@ -11,7 +11,7 @@ final partnerApiRepositoryProvider = Provider(
|
||||
),
|
||||
);
|
||||
|
||||
class PartnerApiRepository extends ApiRepository
|
||||
class PartnerApiRepository extends BaseApiRepository
|
||||
implements IPartnerApiRepository {
|
||||
final PartnersApi _api;
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/interfaces/person_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/base_api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final personApiRepositoryProvider = Provider(
|
||||
(ref) => PersonApiRepository(ref.watch(apiServiceProvider).peopleApi),
|
||||
);
|
||||
|
||||
class PersonApiRepository extends ApiRepository
|
||||
class PersonApiRepository extends BaseApiRepository
|
||||
implements IPersonApiRepository {
|
||||
final PeopleApi _api;
|
||||
|
||||
|
||||
@@ -3,61 +3,37 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
final userRepositoryProvider =
|
||||
Provider((ref) => UserRepository(ref.watch(dbProvider)));
|
||||
|
||||
class UserRepository extends DatabaseRepository implements IUserRepository {
|
||||
UserRepository(super.db);
|
||||
class UserRepository implements IUserRepository {
|
||||
final Isar _db;
|
||||
|
||||
UserRepository(
|
||||
this._db,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<List<User>> getByIds(List<String> ids) async =>
|
||||
(await db.users.getAllById(ids)).nonNulls.toList();
|
||||
(await _db.users.getAllById(ids)).cast();
|
||||
|
||||
@override
|
||||
Future<User?> get(String id) => db.users.getById(id);
|
||||
Future<User?> get(String id) => _db.users.getById(id);
|
||||
|
||||
@override
|
||||
Future<List<User>> getAll({bool self = true, UserSort? sortBy}) {
|
||||
final baseQuery = db.users.where();
|
||||
final int userId = Store.get(StoreKey.currentUser).isarId;
|
||||
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();
|
||||
Future<List<User>> getAll({bool self = true}) {
|
||||
if (self) {
|
||||
return _db.users.where().findAll();
|
||||
}
|
||||
return query.findAll();
|
||||
final int userId = Store.get(StoreKey.currentUser).isarId;
|
||||
return _db.users.where().isarIdNotEqualTo(userId).findAll();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<User> update(User user) async {
|
||||
await txn(() => db.users.put(user));
|
||||
await _db.writeTxn(() => _db.users.put(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();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:http/http.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/interfaces/user_api.interface.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/base_api.repository.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final userApiRepositoryProvider = Provider(
|
||||
@@ -14,7 +14,8 @@ final userApiRepositoryProvider = Provider(
|
||||
),
|
||||
);
|
||||
|
||||
class UserApiRepository extends ApiRepository implements IUserApiRepository {
|
||||
class UserApiRepository extends BaseApiRepository
|
||||
implements IUserApiRepository {
|
||||
final UsersApi _api;
|
||||
|
||||
UserApiRepository(this._api);
|
||||
|
||||
@@ -243,15 +243,14 @@ class AlbumService {
|
||||
int albumId, {
|
||||
List<Asset> add = const [],
|
||||
List<Asset> remove = const [],
|
||||
}) =>
|
||||
_albumRepository.transaction(() async {
|
||||
final album = await _albumRepository.get(albumId);
|
||||
if (album == null) return;
|
||||
await _albumRepository.addAssets(album, add);
|
||||
await _albumRepository.removeAssets(album, remove);
|
||||
await _albumRepository.recalculateMetadata(album);
|
||||
await _albumRepository.update(album);
|
||||
});
|
||||
}) async {
|
||||
final album = await _albumRepository.getById(albumId);
|
||||
if (album == null) return;
|
||||
await _albumRepository.addAssets(album, add);
|
||||
await _albumRepository.removeAssets(album, remove);
|
||||
await _albumRepository.recalculateMetadata(album);
|
||||
await _albumRepository.update(album);
|
||||
}
|
||||
|
||||
Future<bool> addAdditionalUserToAlbum(
|
||||
List<String> sharedUserIds,
|
||||
@@ -286,20 +285,20 @@ class AlbumService {
|
||||
|
||||
Future<bool> deleteAlbum(Album album) async {
|
||||
try {
|
||||
final userId = Store.get(StoreKey.currentUser).isarId;
|
||||
if (album.owner.value?.isarId == userId) {
|
||||
final user = Store.get(StoreKey.currentUser);
|
||||
if (album.owner.value?.isarId == user.isarId) {
|
||||
await _albumApiRepository.delete(album.remoteId!);
|
||||
}
|
||||
if (album.shared) {
|
||||
final foreignAssets =
|
||||
await _assetRepository.getByAlbum(album, notOwnedBy: [userId]);
|
||||
await _assetRepository.getByAlbum(album, notOwnedBy: user);
|
||||
await _albumRepository.delete(album.id);
|
||||
|
||||
final List<Album> albums = await _albumRepository.getAll(shared: true);
|
||||
final List<Asset> existing = [];
|
||||
for (Album album in albums) {
|
||||
existing.addAll(
|
||||
await _assetRepository.getByAlbum(album, notOwnedBy: [userId]),
|
||||
await _assetRepository.getByAlbum(album, notOwnedBy: user),
|
||||
);
|
||||
}
|
||||
final List<int> idsToRemove =
|
||||
@@ -358,7 +357,7 @@ class AlbumService {
|
||||
|
||||
album.sharedUsers.remove(user);
|
||||
await _albumRepository.removeUsers(album, [user]);
|
||||
final a = await _albumRepository.get(album.id);
|
||||
final a = await _albumRepository.getById(album.id);
|
||||
// trigger watcher
|
||||
await _albumRepository.update(a!);
|
||||
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
// ignore_for_file: null_argument_to_non_null_type
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.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/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/user.interface.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.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/user.repository.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/backup.service.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:immich_mobile/services/user.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
@@ -32,54 +29,48 @@ import 'package:openapi/api.dart';
|
||||
final assetServiceProvider = Provider(
|
||||
(ref) => AssetService(
|
||||
ref.watch(assetApiRepositoryProvider),
|
||||
ref.watch(assetRepositoryProvider),
|
||||
ref.watch(exifInfoRepositoryProvider),
|
||||
ref.watch(userRepositoryProvider),
|
||||
ref.watch(etagRepositoryProvider),
|
||||
ref.watch(backupRepositoryProvider),
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(syncServiceProvider),
|
||||
ref.watch(userServiceProvider),
|
||||
ref.watch(backupServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class AssetService {
|
||||
final IAssetApiRepository _assetApiRepository;
|
||||
final IAssetRepository _assetRepository;
|
||||
final IExifInfoRepository _exifInfoRepository;
|
||||
final IUserRepository _userRepository;
|
||||
final IETagRepository _etagRepository;
|
||||
final IBackupRepository _backupRepository;
|
||||
final ApiService _apiService;
|
||||
final SyncService _syncService;
|
||||
final UserService _userService;
|
||||
final BackupService _backupService;
|
||||
final AlbumService _albumService;
|
||||
final log = Logger('AssetService');
|
||||
final Isar _db;
|
||||
|
||||
AssetService(
|
||||
this._assetApiRepository,
|
||||
this._assetRepository,
|
||||
this._exifInfoRepository,
|
||||
this._userRepository,
|
||||
this._etagRepository,
|
||||
this._backupRepository,
|
||||
this._apiService,
|
||||
this._syncService,
|
||||
this._userService,
|
||||
this._backupService,
|
||||
this._albumService,
|
||||
this._db,
|
||||
);
|
||||
|
||||
/// Checks the server for updated assets and updates the local database if
|
||||
/// required. Returns `true` if there were any changes.
|
||||
Future<bool> refreshRemoteAssets() async {
|
||||
final syncedUserIds = await _etagRepository.getAllIds();
|
||||
final syncedUserIds = await _db.eTags.where().idProperty().findAll();
|
||||
final List<User> syncedUsers = syncedUserIds.isEmpty
|
||||
? []
|
||||
: await _userRepository.getByIds(syncedUserIds);
|
||||
: await _db.users
|
||||
.where()
|
||||
.anyOf(syncedUserIds, (q, id) => q.idEqualTo(id))
|
||||
.findAll();
|
||||
final Stopwatch sw = Stopwatch()..start();
|
||||
final bool changes = await _syncService.syncRemoteAssetsToDb(
|
||||
users: syncedUsers,
|
||||
@@ -184,7 +175,7 @@ class AssetService {
|
||||
/// Loads the exif information from the database. If there is none, loads
|
||||
/// the exif info from the server (remote assets only)
|
||||
Future<Asset> loadExif(Asset a) async {
|
||||
a.exifInfo ??= await _exifInfoRepository.get(a.id);
|
||||
a.exifInfo ??= await _db.exifInfos.get(a.id);
|
||||
// fileSize is always filled on the server but not set on client
|
||||
if (a.exifInfo?.fileSize == null) {
|
||||
if (a.isRemote) {
|
||||
@@ -194,7 +185,7 @@ class AssetService {
|
||||
a.exifInfo = newExif;
|
||||
if (newExif != a.exifInfo) {
|
||||
if (a.isInDb) {
|
||||
_assetRepository.transaction(() => _assetRepository.update(a));
|
||||
_db.writeTxn(() => a.put(_db));
|
||||
} else {
|
||||
debugPrint("[loadExif] parameter Asset is not from DB!");
|
||||
}
|
||||
@@ -223,7 +214,7 @@ class AssetService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Asset>> changeFavoriteStatus(
|
||||
Future<List<Asset?>> changeFavoriteStatus(
|
||||
List<Asset> assets,
|
||||
bool isFavorite,
|
||||
) async {
|
||||
@@ -239,11 +230,11 @@ class AssetService {
|
||||
return assets;
|
||||
} catch (error, stack) {
|
||||
log.severe("Error while changing favorite status", error, stack);
|
||||
return [];
|
||||
return Future.value(null);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset>> changeArchiveStatus(
|
||||
Future<List<Asset?>> changeArchiveStatus(
|
||||
List<Asset> assets,
|
||||
bool isArchived,
|
||||
) async {
|
||||
@@ -259,11 +250,11 @@ class AssetService {
|
||||
return assets;
|
||||
} catch (error, stack) {
|
||||
log.severe("Error while changing archive status", error, stack);
|
||||
return [];
|
||||
return Future.value(null);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset>?> changeDateTime(
|
||||
Future<List<Asset?>> changeDateTime(
|
||||
List<Asset> assets,
|
||||
String updatedDt,
|
||||
) async {
|
||||
@@ -287,7 +278,7 @@ class AssetService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Asset>?> changeLocation(
|
||||
Future<List<Asset?>> changeLocation(
|
||||
List<Asset> assets,
|
||||
LatLng location,
|
||||
) async {
|
||||
@@ -316,10 +307,10 @@ class AssetService {
|
||||
|
||||
Future<void> syncUploadedAssetToAlbums() async {
|
||||
try {
|
||||
final selectedAlbums =
|
||||
await _backupRepository.getAllBySelection(BackupSelection.select);
|
||||
final excludedAlbums =
|
||||
await _backupRepository.getAllBySelection(BackupSelection.exclude);
|
||||
final [selectedAlbums, excludedAlbums] = await Future.wait([
|
||||
_backupService.selectedAlbumsQuery().findAll(),
|
||||
_backupService.excludedAlbumsQuery().findAll(),
|
||||
]);
|
||||
|
||||
final candidates = await _backupService.buildUploadCandidates(
|
||||
selectedAlbums,
|
||||
@@ -328,11 +319,12 @@ class AssetService {
|
||||
);
|
||||
|
||||
await refreshRemoteAssets();
|
||||
final owner = await _userRepository.me();
|
||||
final remoteAssets = await _assetRepository.getAll(
|
||||
ownerId: owner.isarId,
|
||||
state: AssetState.merged,
|
||||
);
|
||||
final remoteAssets = await _db.assets
|
||||
.where()
|
||||
.localIdIsNotNull()
|
||||
.filter()
|
||||
.remoteIdIsNotNull()
|
||||
.findAll();
|
||||
|
||||
/// Map<AlbumName, [AssetId]>
|
||||
Map<String, List<String>> assetToAlbums = {};
|
||||
|
||||
@@ -9,18 +9,14 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.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/models/backup/backup_candidate.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_api.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/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/partner_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/user.repository.dart';
|
||||
@@ -41,6 +37,7 @@ import 'package:immich_mobile/services/user.service.dart';
|
||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||
import 'package:immich_mobile/utils/diff.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:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||
|
||||
@@ -359,7 +356,7 @@ class BackgroundService {
|
||||
}
|
||||
|
||||
Future<bool> _onAssetsChanged() async {
|
||||
final db = await loadDb();
|
||||
final Isar db = await loadDb();
|
||||
|
||||
HttpOverrides.global = HttpSSLCertOverride();
|
||||
ApiService apiService = ApiService();
|
||||
@@ -368,12 +365,9 @@ class BackgroundService {
|
||||
AppSettingsService settingsService = AppSettingsService();
|
||||
AlbumRepository albumRepository = AlbumRepository(db);
|
||||
AssetRepository assetRepository = AssetRepository(db);
|
||||
BackupRepository backupRepository = BackupRepository(db);
|
||||
ExifInfoRepository exifInfoRepository = ExifInfoRepository(db);
|
||||
ETagRepository eTagRepository = ETagRepository(db);
|
||||
BackupRepository backupAlbumRepository = BackupRepository(db);
|
||||
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
|
||||
FileMediaRepository fileMediaRepository = FileMediaRepository();
|
||||
AssetMediaRepository assetMediaRepository = AssetMediaRepository();
|
||||
UserRepository userRepository = UserRepository(db);
|
||||
UserApiRepository userApiRepository =
|
||||
UserApiRepository(apiService.usersApi);
|
||||
@@ -386,15 +380,11 @@ class BackgroundService {
|
||||
EntityService entityService =
|
||||
EntityService(assetRepository, userRepository);
|
||||
SyncService syncSerive = SyncService(
|
||||
db,
|
||||
hashService,
|
||||
entityService,
|
||||
albumMediaRepository,
|
||||
albumApiRepository,
|
||||
albumRepository,
|
||||
assetRepository,
|
||||
exifInfoRepository,
|
||||
userRepository,
|
||||
eTagRepository,
|
||||
);
|
||||
UserService userService = UserService(
|
||||
partnerApiRepository,
|
||||
@@ -408,24 +398,21 @@ class BackgroundService {
|
||||
entityService,
|
||||
albumRepository,
|
||||
assetRepository,
|
||||
backupRepository,
|
||||
backupAlbumRepository,
|
||||
albumMediaRepository,
|
||||
albumApiRepository,
|
||||
);
|
||||
BackupService backupService = BackupService(
|
||||
apiService,
|
||||
db,
|
||||
settingService,
|
||||
albumService,
|
||||
albumMediaRepository,
|
||||
fileMediaRepository,
|
||||
assetRepository,
|
||||
assetMediaRepository,
|
||||
);
|
||||
|
||||
final selectedAlbums =
|
||||
await backupRepository.getAllBySelection(BackupSelection.select);
|
||||
final excludedAlbums =
|
||||
await backupRepository.getAllBySelection(BackupSelection.exclude);
|
||||
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
|
||||
final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync();
|
||||
if (selectedAlbums.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
@@ -443,28 +430,28 @@ class BackgroundService {
|
||||
await Store.delete(StoreKey.backupFailedSince);
|
||||
final backupAlbums = [...selectedAlbums, ...excludedAlbums];
|
||||
backupAlbums.sortBy((e) => e.id);
|
||||
|
||||
final dbAlbums =
|
||||
await backupRepository.getAll(sort: BackupAlbumSort.id);
|
||||
final List<int> toDelete = [];
|
||||
final List<BackupAlbum> toUpsert = [];
|
||||
// stores the most recent `lastBackup` per album but always keeps the `selection` from the most recent DB state
|
||||
diffSortedListsSync(
|
||||
dbAlbums,
|
||||
backupAlbums,
|
||||
compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id),
|
||||
both: (BackupAlbum a, BackupAlbum b) {
|
||||
a.lastBackup = a.lastBackup.isAfter(b.lastBackup)
|
||||
? a.lastBackup
|
||||
: b.lastBackup;
|
||||
toUpsert.add(a);
|
||||
return true;
|
||||
},
|
||||
onlyFirst: (BackupAlbum a) => toUpsert.add(a),
|
||||
onlySecond: (BackupAlbum b) => toDelete.add(b.isarId),
|
||||
);
|
||||
await backupRepository.deleteAll(toDelete);
|
||||
await backupRepository.updateAll(toUpsert);
|
||||
db.writeTxnSync(() {
|
||||
final dbAlbums = db.backupAlbums.where().sortById().findAllSync();
|
||||
final List<int> toDelete = [];
|
||||
final List<BackupAlbum> toUpsert = [];
|
||||
// stores the most recent `lastBackup` per album but always keeps the `selection` from the most recent DB state
|
||||
diffSortedListsSync(
|
||||
dbAlbums,
|
||||
backupAlbums,
|
||||
compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id),
|
||||
both: (BackupAlbum a, BackupAlbum b) {
|
||||
a.lastBackup = a.lastBackup.isAfter(b.lastBackup)
|
||||
? a.lastBackup
|
||||
: b.lastBackup;
|
||||
toUpsert.add(a);
|
||||
return true;
|
||||
},
|
||||
onlyFirst: (BackupAlbum a) => toUpsert.add(a),
|
||||
onlySecond: (BackupAlbum b) => toDelete.add(b.isarId),
|
||||
);
|
||||
db.backupAlbums.deleteAllSync(toDelete);
|
||||
db.backupAlbums.putAllSync(toUpsert);
|
||||
});
|
||||
} else if (Store.tryGet(StoreKey.backupFailedSince) == null) {
|
||||
Store.put(StoreKey.backupFailedSince, DateTime.now());
|
||||
return false;
|
||||
|
||||
@@ -9,10 +9,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.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/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/models/backup/backup_candidate.model.dart';
|
||||
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
||||
@@ -20,13 +19,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/providers/api.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/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/services/album.service.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@@ -36,34 +35,31 @@ import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||
final backupServiceProvider = Provider(
|
||||
(ref) => BackupService(
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(appSettingsServiceProvider),
|
||||
ref.watch(albumServiceProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
ref.watch(fileMediaRepositoryProvider),
|
||||
ref.watch(assetRepositoryProvider),
|
||||
ref.watch(assetMediaRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class BackupService {
|
||||
final httpClient = http.Client();
|
||||
final ApiService _apiService;
|
||||
final Isar _db;
|
||||
final Logger _log = Logger("BackupService");
|
||||
final AppSettingsService _appSetting;
|
||||
final AlbumService _albumService;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final IFileMediaRepository _fileMediaRepository;
|
||||
final IAssetRepository _assetRepository;
|
||||
final IAssetMediaRepository _assetMediaRepository;
|
||||
|
||||
BackupService(
|
||||
this._apiService,
|
||||
this._db,
|
||||
this._appSetting,
|
||||
this._albumService,
|
||||
this._albumMediaRepository,
|
||||
this._fileMediaRepository,
|
||||
this._assetRepository,
|
||||
this._assetMediaRepository,
|
||||
);
|
||||
|
||||
Future<List<String>?> getDeviceBackupAsset() async {
|
||||
@@ -77,17 +73,24 @@ class BackupService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveDuplicatedAssetIds(List<String> deviceAssetIds) =>
|
||||
_assetRepository.transaction(
|
||||
() => _assetRepository.upsertDuplicatedAssets(deviceAssetIds),
|
||||
);
|
||||
Future<void> _saveDuplicatedAssetIds(List<String> deviceAssetIds) {
|
||||
final duplicates = deviceAssetIds.map((id) => DuplicatedAsset(id)).toList();
|
||||
return _db.writeTxn(() => _db.duplicatedAssets.putAll(duplicates));
|
||||
}
|
||||
|
||||
/// Get duplicated asset id from database
|
||||
Future<Set<String>> getDuplicatedAssetIds() async {
|
||||
final duplicates = await _assetRepository.getAllDuplicatedAssetIds();
|
||||
return duplicates.toSet();
|
||||
final duplicates = await _db.duplicatedAssets.where().findAll();
|
||||
return duplicates.map((e) => e.id).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
|
||||
/// if `useTimeFilter` is set to true, all assets will be returned
|
||||
Future<Set<BackupCandidate>> buildUploadCandidates(
|
||||
@@ -326,9 +329,7 @@ class BackupService {
|
||||
}
|
||||
|
||||
if (file != null) {
|
||||
String? originalFileName =
|
||||
await _assetMediaRepository.getOriginalFilename(asset.localId!);
|
||||
originalFileName ??= asset.fileName;
|
||||
String originalFileName = asset.fileName;
|
||||
|
||||
if (asset.local!.isLivePhoto) {
|
||||
if (livePhotoFile == null) {
|
||||
|
||||
@@ -34,19 +34,19 @@ class BackupVerificationService {
|
||||
final owner = Store.get(StoreKey.currentUser).isarId;
|
||||
final List<Asset> onlyLocal = await _assetRepository.getAll(
|
||||
ownerId: owner,
|
||||
state: AssetState.local,
|
||||
remote: false,
|
||||
limit: limit,
|
||||
);
|
||||
final List<Asset> remoteMatches = await _assetRepository.getMatches(
|
||||
assets: onlyLocal,
|
||||
ownerId: owner,
|
||||
state: AssetState.remote,
|
||||
remote: true,
|
||||
limit: limit,
|
||||
);
|
||||
final List<Asset> localMatches = await _assetRepository.getMatches(
|
||||
assets: remoteMatches,
|
||||
ownerId: owner,
|
||||
state: AssetState.local,
|
||||
remote: false,
|
||||
limit: limit,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
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 ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -130,9 +130,7 @@ class HashService {
|
||||
final validHashes = anyNull
|
||||
? toAdd.where((e) => e.hash.length == 20).toList(growable: false)
|
||||
: toAdd;
|
||||
|
||||
await _assetRepository
|
||||
.transaction(() => _assetRepository.upsertDeviceAssets(validHashes));
|
||||
await _assetRepository.upsertDeviceAssets(validHashes);
|
||||
_log.fine("Hashed ${validHashes.length}/${toHash.length} assets");
|
||||
}
|
||||
|
||||
|
||||
117
mobile/lib/services/image_viewer.service.dart
Normal file
117
mobile/lib/services/image_viewer.service.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,8 +61,7 @@ class StackService {
|
||||
|
||||
removeAssets.add(asset);
|
||||
}
|
||||
await _assetRepository
|
||||
.transaction(() => _assetRepository.updateAll(removeAssets));
|
||||
await _assetRepository.updateAll(removeAssets);
|
||||
} catch (error) {
|
||||
debugPrint("Error while deleting stack: $error");
|
||||
}
|
||||
|
||||
@@ -5,66 +5,48 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.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/interfaces/album.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/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/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/album_api.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/hash.service.dart';
|
||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/utils/datetime_comparison.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final syncServiceProvider = Provider(
|
||||
(ref) => SyncService(
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(hashServiceProvider),
|
||||
ref.watch(entityServiceProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
ref.watch(albumApiRepositoryProvider),
|
||||
ref.watch(albumRepositoryProvider),
|
||||
ref.watch(assetRepositoryProvider),
|
||||
ref.watch(exifInfoRepositoryProvider),
|
||||
ref.watch(userRepositoryProvider),
|
||||
ref.watch(etagRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class SyncService {
|
||||
final Isar _db;
|
||||
final HashService _hashService;
|
||||
final EntityService _entityService;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final IAlbumApiRepository _albumApiRepository;
|
||||
final IAlbumRepository _albumRepository;
|
||||
final IAssetRepository _assetRepository;
|
||||
final IExifInfoRepository _exifInfoRepository;
|
||||
final IUserRepository _userRepository;
|
||||
final IETagRepository _eTagRepository;
|
||||
final AsyncMutex _lock = AsyncMutex();
|
||||
final Logger _log = Logger('SyncService');
|
||||
|
||||
SyncService(
|
||||
this._db,
|
||||
this._hashService,
|
||||
this._entityService,
|
||||
this._albumMediaRepository,
|
||||
this._albumApiRepository,
|
||||
this._albumRepository,
|
||||
this._assetRepository,
|
||||
this._exifInfoRepository,
|
||||
this._userRepository,
|
||||
this._eTagRepository,
|
||||
);
|
||||
|
||||
// public methods:
|
||||
@@ -137,7 +119,7 @@ class SyncService {
|
||||
/// Returns `true`if there were any changes
|
||||
Future<bool> _syncUsersFromServer(List<User> users) async {
|
||||
users.sortBy((u) => u.id);
|
||||
final dbUsers = await _userRepository.getAll(sortBy: UserSort.id);
|
||||
final dbUsers = await _db.users.where().sortById().findAll();
|
||||
assert(dbUsers.isSortedBy((u) => u.id), "dbUsers not sorted!");
|
||||
final List<int> toDelete = [];
|
||||
final List<User> toUpsert = [];
|
||||
@@ -159,9 +141,9 @@ class SyncService {
|
||||
onlySecond: (User b) => toDelete.add(b.isarId),
|
||||
);
|
||||
if (changes) {
|
||||
await _userRepository.transaction(() async {
|
||||
await _userRepository.deleteById(toDelete);
|
||||
await _userRepository.upsertAll(toUpsert);
|
||||
await _db.writeTxn(() async {
|
||||
await _db.users.deleteAll(toDelete);
|
||||
await _db.users.putAll(toUpsert);
|
||||
});
|
||||
}
|
||||
return changes;
|
||||
@@ -170,15 +152,15 @@ class SyncService {
|
||||
/// Syncs a new asset to the db. Returns `true` if successful
|
||||
Future<bool> _syncNewAssetToDb(Asset a) async {
|
||||
final Asset? inDb =
|
||||
await _assetRepository.getByOwnerIdChecksum(a.ownerId, a.checksum);
|
||||
await _db.assets.getByOwnerIdChecksum(a.ownerId, a.checksum);
|
||||
if (inDb != null) {
|
||||
// unify local/remote assets by replacing the
|
||||
// local-only asset in the DB with a local&remote asset
|
||||
a = inDb.updatedCopy(a);
|
||||
}
|
||||
try {
|
||||
await _assetRepository.update(a);
|
||||
} catch (e) {
|
||||
await _db.writeTxn(() => a.put(_db));
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to put new asset into db", e);
|
||||
return false;
|
||||
}
|
||||
@@ -193,9 +175,9 @@ class SyncService {
|
||||
DateTime since,
|
||||
) getChangedAssets,
|
||||
) async {
|
||||
final currentUser = await _userRepository.me();
|
||||
final currentUser = Store.get(StoreKey.currentUser);
|
||||
final DateTime? since =
|
||||
(await _eTagRepository.get(currentUser.isarId))?.time?.toUtc();
|
||||
_db.eTags.getSync(currentUser.isarId)?.time?.toUtc();
|
||||
if (since == null) return null;
|
||||
final DateTime now = DateTime.now();
|
||||
final (toUpsert, toDelete) = await getChangedAssets(users, since);
|
||||
@@ -216,7 +198,7 @@ class SyncService {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to sync remote assets to db", e);
|
||||
}
|
||||
return null;
|
||||
@@ -224,21 +206,23 @@ class SyncService {
|
||||
|
||||
/// Deletes remote-only assets, updates merged assets to be local-only
|
||||
Future<void> handleRemoteAssetRemoval(List<String> idsToDelete) {
|
||||
return _assetRepository.transaction(() async {
|
||||
await _assetRepository.deleteAllByRemoteId(
|
||||
idsToDelete,
|
||||
state: AssetState.remote,
|
||||
);
|
||||
final merged = await _assetRepository.getAllByRemoteId(
|
||||
idsToDelete,
|
||||
state: AssetState.merged,
|
||||
);
|
||||
if (merged.isEmpty) return;
|
||||
for (final Asset asset in merged) {
|
||||
asset.remoteId = null;
|
||||
asset.isTrashed = false;
|
||||
return _db.writeTxn(() async {
|
||||
final idsToRemove = await _db.assets
|
||||
.remote(idsToDelete)
|
||||
.filter()
|
||||
.localIdIsNull()
|
||||
.idProperty()
|
||||
.findAll();
|
||||
await _db.assets.deleteAll(idsToRemove);
|
||||
await _db.exifInfos.deleteAll(idsToRemove);
|
||||
final onlyLocal = await _db.assets.remote(idsToDelete).findAll();
|
||||
if (onlyLocal.isNotEmpty) {
|
||||
for (final Asset a in onlyLocal) {
|
||||
a.remoteId = null;
|
||||
a.isTrashed = false;
|
||||
}
|
||||
await _db.assets.putAll(onlyLocal);
|
||||
}
|
||||
await _assetRepository.updateAll(merged);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -253,7 +237,12 @@ class SyncService {
|
||||
return false;
|
||||
}
|
||||
await _syncUsersFromServer(serverUsers);
|
||||
final List<User> users = await _userRepository.getAllAccessible();
|
||||
final List<User> users = await _db.users
|
||||
.filter()
|
||||
.isPartnerSharedWithEqualTo(true)
|
||||
.or()
|
||||
.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.findAll();
|
||||
bool changes = false;
|
||||
for (User u in users) {
|
||||
changes |= await _syncRemoteAssetsForUser(u, loadAssets);
|
||||
@@ -270,10 +259,11 @@ class SyncService {
|
||||
if (remote == null) {
|
||||
return false;
|
||||
}
|
||||
final List<Asset> inDb = await _assetRepository.getAll(
|
||||
ownerId: user.isarId,
|
||||
sortBy: AssetSort.checksum,
|
||||
);
|
||||
final List<Asset> inDb = await _db.assets
|
||||
.where()
|
||||
.ownerIdEqualToAnyChecksum(user.isarId)
|
||||
.sortByChecksum()
|
||||
.findAll();
|
||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||
|
||||
remote.sort(Asset.compareByChecksum);
|
||||
@@ -288,9 +278,9 @@ class SyncService {
|
||||
}
|
||||
final idsToDelete = toRemove.map((e) => e.id).toList();
|
||||
try {
|
||||
await _assetRepository.deleteById(idsToDelete);
|
||||
await _db.writeTxn(() => _db.assets.deleteAll(idsToDelete));
|
||||
await upsertAssetsWithExif(toAdd + toUpdate);
|
||||
} catch (e) {
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to sync remote assets to db", e);
|
||||
}
|
||||
await _updateUserAssetsETag([user], now);
|
||||
@@ -299,12 +289,12 @@ class SyncService {
|
||||
|
||||
Future<void> _updateUserAssetsETag(List<User> users, DateTime time) {
|
||||
final etags = users.map((u) => ETag(id: u.id, time: time)).toList();
|
||||
return _eTagRepository.upsertAll(etags);
|
||||
return _db.writeTxn(() => _db.eTags.putAll(etags));
|
||||
}
|
||||
|
||||
Future<void> _clearUserAssetsETag(List<User> users) {
|
||||
final ids = users.map((u) => u.id).toList();
|
||||
return _eTagRepository.deleteByIds(ids);
|
||||
return _db.writeTxn(() => _db.eTags.deleteAllById(ids));
|
||||
}
|
||||
|
||||
/// Syncs remote albums to the database
|
||||
@@ -315,13 +305,15 @@ class SyncService {
|
||||
) async {
|
||||
remoteAlbums.sortBy((e) => e.remoteId!);
|
||||
|
||||
final User me = await _userRepository.me();
|
||||
final List<Album> dbAlbums = await _albumRepository.getAll(
|
||||
remote: true,
|
||||
shared: isShared ? true : null,
|
||||
ownerId: isShared ? null : me.isarId,
|
||||
sortBy: AlbumSort.remoteId,
|
||||
);
|
||||
final baseQuery = _db.albums.where().remoteIdIsNotNull().filter();
|
||||
final QueryBuilder<Album, Album, QAfterFilterCondition> query;
|
||||
if (isShared) {
|
||||
query = baseQuery.sharedEqualTo(true);
|
||||
} else {
|
||||
final User me = Store.get(StoreKey.currentUser);
|
||||
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!");
|
||||
|
||||
final List<Asset> toDelete = [];
|
||||
@@ -341,7 +333,10 @@ class SyncService {
|
||||
if (isShared && toDelete.isNotEmpty) {
|
||||
final List<int> idsToRemove = sharedAssetsToRemove(toDelete, existing);
|
||||
if (idsToRemove.isNotEmpty) {
|
||||
await _assetRepository.deleteById(idsToRemove);
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.deleteAll(idsToRemove);
|
||||
await _db.exifInfos.deleteAll(idsToRemove);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
assert(toDelete.isEmpty);
|
||||
@@ -365,11 +360,8 @@ class SyncService {
|
||||
// i.e. it will always be null. Save it here.
|
||||
final originalDto = dto;
|
||||
dto = await _albumApiRepository.get(dto.remoteId!);
|
||||
|
||||
final assetsInDb = await _assetRepository.getByAlbum(
|
||||
album,
|
||||
sortBy: AssetSort.ownerIdChecksum,
|
||||
);
|
||||
final assetsInDb =
|
||||
await album.assets.filter().sortByOwnerId().thenByChecksum().findAll();
|
||||
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
|
||||
final List<Asset> assetsOnRemote = dto.remoteAssets.toList();
|
||||
assetsOnRemote.sort(Asset.compareByOwnerChecksum);
|
||||
@@ -399,7 +391,7 @@ class SyncService {
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
|
||||
await upsertAssetsWithExif(updated);
|
||||
final assetsToLink = existingInDb + updated;
|
||||
final usersToLink = await _userRepository.getByIds(userIdsToAdd);
|
||||
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
|
||||
|
||||
album.name = dto.name;
|
||||
album.shared = dto.shared;
|
||||
@@ -410,33 +402,32 @@ class SyncService {
|
||||
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
|
||||
album.shared = dto.shared;
|
||||
album.activityEnabled = dto.activityEnabled;
|
||||
final remoteThumbnailAssetId = dto.remoteThumbnailAssetId;
|
||||
if (remoteThumbnailAssetId != null &&
|
||||
album.thumbnail.value?.remoteId != remoteThumbnailAssetId) {
|
||||
album.thumbnail.value =
|
||||
await _assetRepository.getByRemoteId(remoteThumbnailAssetId);
|
||||
if (album.thumbnail.value?.remoteId != dto.remoteThumbnailAssetId) {
|
||||
album.thumbnail.value = await _db.assets
|
||||
.where()
|
||||
.remoteIdEqualTo(dto.remoteThumbnailAssetId)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
// write & commit all changes to DB
|
||||
try {
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.updateAll(toUpdate);
|
||||
await _albumRepository.addUsers(album, usersToLink);
|
||||
await _albumRepository.removeUsers(album, usersToUnlink);
|
||||
await _albumRepository.addAssets(album, assetsToLink);
|
||||
await _albumRepository.removeAssets(album, toUnlink);
|
||||
await _albumRepository.recalculateMetadata(album);
|
||||
await _albumRepository.update(album);
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(toUpdate);
|
||||
await album.thumbnail.save();
|
||||
await album.sharedUsers
|
||||
.update(link: usersToLink, unlink: usersToUnlink);
|
||||
await album.assets.update(link: assetsToLink, unlink: toUnlink.cast());
|
||||
await _db.albums.put(album);
|
||||
});
|
||||
_log.info("Synced changes of remote album ${album.name} to DB");
|
||||
} catch (e) {
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to sync remote album to database", e);
|
||||
}
|
||||
|
||||
if (album.shared || dto.shared) {
|
||||
final userId = (await _userRepository.me()).isarId;
|
||||
final userId = Store.get(StoreKey.currentUser).isarId;
|
||||
final foreign =
|
||||
await _assetRepository.getByAlbum(album, notOwnedBy: [userId]);
|
||||
await album.assets.filter().not().ownerIdEqualTo(userId).findAll();
|
||||
existing.addAll(foreign);
|
||||
|
||||
// delete assets in DB unless they belong to this user or part of some other shared album
|
||||
@@ -465,7 +456,7 @@ class SyncService {
|
||||
await upsertAssetsWithExif(updated);
|
||||
|
||||
await _entityService.fillAlbumWithDatabaseEntities(album);
|
||||
await _albumRepository.create(album);
|
||||
await _db.writeTxn(() => _db.albums.store(album));
|
||||
} else {
|
||||
_log.warning(
|
||||
"Failed to add album from server: assetCount ${album.remoteAssetCount} != "
|
||||
@@ -483,18 +474,27 @@ class SyncService {
|
||||
_log.info("Removing local album $album from DB");
|
||||
// delete assets in DB unless they are remote or part of some other album
|
||||
deleteCandidates.addAll(
|
||||
await _assetRepository.getByAlbum(album, state: AssetState.local),
|
||||
await album.assets.filter().remoteIdIsNull().findAll(),
|
||||
);
|
||||
} 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
|
||||
final userIds =
|
||||
(await _userRepository.getAllAccessible()).map((user) => user.isarId);
|
||||
final orphanedAssets =
|
||||
await _assetRepository.getByAlbum(album, notOwnedBy: userIds);
|
||||
final userIds = await _db.users
|
||||
.filter()
|
||||
.isPartnerSharedWithEqualTo(true)
|
||||
.isarIdProperty()
|
||||
.findAll();
|
||||
userIds.add(user.isarId);
|
||||
final orphanedAssets = await album.assets
|
||||
.filter()
|
||||
.not()
|
||||
.anyOf(userIds, (q, int id) => q.ownerIdEqualTo(id))
|
||||
.findAll();
|
||||
deleteCandidates.addAll(orphanedAssets);
|
||||
}
|
||||
try {
|
||||
await _albumRepository.delete(album.id);
|
||||
final bool ok = await _db.writeTxn(() => _db.albums.delete(album.id));
|
||||
assert(ok);
|
||||
_log.info("Removed local album $album from DB");
|
||||
} catch (e) {
|
||||
_log.severe("Failed to remove local album $album from DB", e);
|
||||
@@ -509,7 +509,7 @@ class SyncService {
|
||||
]) async {
|
||||
onDevice.sort((a, b) => a.id.compareTo(b.id));
|
||||
final inDb =
|
||||
await _albumRepository.getAll(remote: false, sortBy: AlbumSort.localId);
|
||||
await _db.albums.where().localIdIsNotNull().sortByLocalId().findAll();
|
||||
final List<Asset> deleteCandidates = [];
|
||||
final List<Asset> existing = [];
|
||||
assert(inDb.isSorted((a, b) => a.localId!.compareTo(b.localId!)), "sort!");
|
||||
@@ -536,9 +536,10 @@ class SyncService {
|
||||
"${toDelete.length} assets to delete, ${toUpdate.length} to update",
|
||||
);
|
||||
if (toDelete.isNotEmpty || toUpdate.isNotEmpty) {
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.deleteById(toDelete);
|
||||
await _assetRepository.updateAll(toUpdate);
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.deleteAll(toDelete);
|
||||
await _db.exifInfos.deleteAll(toDelete);
|
||||
await _db.assets.putAll(toUpdate);
|
||||
});
|
||||
_log.info(
|
||||
"Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB",
|
||||
@@ -569,13 +570,13 @@ class SyncService {
|
||||
await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) {
|
||||
return true;
|
||||
}
|
||||
// general case, e.g. some assets have been deleted or there are excluded albums on iOS
|
||||
final inDb = await _assetRepository.getByAlbum(
|
||||
dbAlbum,
|
||||
ownerId: (await _userRepository.me()).isarId,
|
||||
sortBy: AssetSort.checksum,
|
||||
);
|
||||
|
||||
// general case, e.g. some assets have been deleted or there are excluded albums on iOS
|
||||
final inDb = await dbAlbum.assets
|
||||
.filter()
|
||||
.ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.sortByChecksum()
|
||||
.findAll();
|
||||
assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!");
|
||||
final int assetCountOnDevice =
|
||||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
|
||||
@@ -596,14 +597,15 @@ class SyncService {
|
||||
"Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync.",
|
||||
);
|
||||
if (assetCountOnDevice !=
|
||||
(await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
|
||||
?.assetCount) {
|
||||
await _eTagRepository.upsertAll([
|
||||
ETag(
|
||||
id: deviceAlbum.eTagKeyAssetCount,
|
||||
assetCount: assetCountOnDevice,
|
||||
_db.eTags.getByIdSync(deviceAlbum.eTagKeyAssetCount)?.assetCount) {
|
||||
await _db.writeTxn(
|
||||
() => _db.eTags.put(
|
||||
ETag(
|
||||
id: deviceAlbum.eTagKeyAssetCount,
|
||||
assetCount: assetCountOnDevice,
|
||||
),
|
||||
),
|
||||
]);
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -623,21 +625,23 @@ class SyncService {
|
||||
dbAlbum.thumbnail.value = null;
|
||||
}
|
||||
try {
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.updateAll(updated + toUpdate);
|
||||
await _albumRepository.addAssets(dbAlbum, existingInDb + updated);
|
||||
await _albumRepository.removeAssets(dbAlbum, toDelete);
|
||||
await _albumRepository.recalculateMetadata(dbAlbum);
|
||||
await _albumRepository.update(dbAlbum);
|
||||
await _eTagRepository.upsertAll([
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(updated);
|
||||
await _db.assets.putAll(toUpdate);
|
||||
await dbAlbum.assets
|
||||
.update(link: existingInDb + updated, unlink: toDelete);
|
||||
await _db.albums.put(dbAlbum);
|
||||
dbAlbum.thumbnail.value ??= await dbAlbum.assets.filter().findFirst();
|
||||
await dbAlbum.thumbnail.save();
|
||||
await _db.eTags.put(
|
||||
ETag(
|
||||
id: deviceAlbum.eTagKeyAssetCount,
|
||||
assetCount: assetCountOnDevice,
|
||||
),
|
||||
]);
|
||||
);
|
||||
});
|
||||
_log.info("Synced changes of local album ${deviceAlbum.name} to DB");
|
||||
} catch (e) {
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to update synced album ${deviceAlbum.name} in DB", e);
|
||||
}
|
||||
|
||||
@@ -653,8 +657,7 @@ class SyncService {
|
||||
final int totalOnDevice =
|
||||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!);
|
||||
final int lastKnownTotal =
|
||||
(await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
|
||||
?.assetCount ??
|
||||
(await _db.eTags.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount ??
|
||||
0;
|
||||
if (totalOnDevice <= lastKnownTotal) {
|
||||
return false;
|
||||
@@ -672,17 +675,16 @@ class SyncService {
|
||||
_removeDuplicates(newAssets);
|
||||
final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets);
|
||||
try {
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.updateAll(updated);
|
||||
await _albumRepository.addAssets(dbAlbum, existingInDb + updated);
|
||||
await _albumRepository.recalculateMetadata(dbAlbum);
|
||||
await _albumRepository.update(dbAlbum);
|
||||
await _eTagRepository.upsertAll(
|
||||
[ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice)],
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(updated);
|
||||
await dbAlbum.assets.update(link: existingInDb + updated);
|
||||
await _db.albums.put(dbAlbum);
|
||||
await _db.eTags.put(
|
||||
ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice),
|
||||
);
|
||||
});
|
||||
_log.info("Fast synced local album ${deviceAlbum.name} to DB");
|
||||
} catch (e) {
|
||||
} on IsarError catch (e) {
|
||||
_log.severe(
|
||||
"Failed to fast sync local album ${deviceAlbum.name} to DB",
|
||||
e,
|
||||
@@ -717,9 +719,9 @@ class SyncService {
|
||||
final thumb = existingInDb.firstOrNull ?? updated.firstOrNull;
|
||||
album.thumbnail.value = thumb;
|
||||
try {
|
||||
await _albumRepository.create(album);
|
||||
await _db.writeTxn(() => _db.albums.store(album));
|
||||
_log.info("Added a new local album to DB: ${album.name}");
|
||||
} catch (e) {
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to add new local album ${album.name} to DB", e);
|
||||
}
|
||||
}
|
||||
@@ -730,7 +732,7 @@ class SyncService {
|
||||
) async {
|
||||
if (assets.isEmpty) return ([].cast<Asset>(), [].cast<Asset>());
|
||||
|
||||
final List<Asset?> inDb = await _assetRepository.getAllByOwnerIdChecksum(
|
||||
final List<Asset?> inDb = await _db.assets.getAllByOwnerIdChecksum(
|
||||
assets.map((a) => a.ownerId).toInt64List(),
|
||||
assets.map((a) => a.checksum).toList(growable: false),
|
||||
);
|
||||
@@ -744,7 +746,7 @@ class SyncService {
|
||||
}
|
||||
if (b.canUpdate(assets[i])) {
|
||||
final updated = b.updatedCopy(assets[i]);
|
||||
assert(updated.isInDb);
|
||||
assert(updated.id != Isar.autoIncrement);
|
||||
toUpsert.add(updated);
|
||||
} else {
|
||||
existing.add(b);
|
||||
@@ -756,22 +758,24 @@ class SyncService {
|
||||
|
||||
/// Inserts or updates the assets in the database with their ExifInfo (if any)
|
||||
Future<void> upsertAssetsWithExif(List<Asset> assets) async {
|
||||
if (assets.isEmpty) return;
|
||||
final exifInfos = assets.map((e) => e.exifInfo).nonNulls.toList();
|
||||
if (assets.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final exifInfos = assets.map((e) => e.exifInfo).whereNotNull().toList();
|
||||
try {
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.updateAll(assets);
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.putAll(assets);
|
||||
for (final Asset added in assets) {
|
||||
added.exifInfo?.id = added.id;
|
||||
}
|
||||
await _exifInfoRepository.updateAll(exifInfos);
|
||||
await _db.exifInfos.putAll(exifInfos);
|
||||
});
|
||||
_log.info("Upserted ${assets.length} assets into the DB");
|
||||
} catch (e) {
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to upsert ${assets.length} assets into the DB", e);
|
||||
// give details on the errors
|
||||
assets.sort(Asset.compareByOwnerChecksum);
|
||||
final inDb = await _assetRepository.getAllByOwnerIdChecksum(
|
||||
final inDb = await _db.assets.getAllByOwnerIdChecksum(
|
||||
assets.map((e) => e.ownerId).toInt64List(),
|
||||
assets.map((e) => e.checksum).toList(growable: false),
|
||||
);
|
||||
@@ -779,7 +783,7 @@ class SyncService {
|
||||
final Asset a = assets[i];
|
||||
final Asset? b = inDb[i];
|
||||
if (b == null) {
|
||||
if (!a.isInDb) {
|
||||
if (a.id != Isar.autoIncrement) {
|
||||
_log.warning(
|
||||
"Trying to update an asset that does not exist in DB:\n$a",
|
||||
);
|
||||
@@ -823,19 +827,19 @@ class SyncService {
|
||||
return deviceAlbum.name != dbAlbum.name ||
|
||||
!deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
|
||||
await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) !=
|
||||
(await _eTagRepository.getById(deviceAlbum.eTagKeyAssetCount))
|
||||
(await _db.eTags.getById(deviceAlbum.eTagKeyAssetCount))
|
||||
?.assetCount;
|
||||
}
|
||||
|
||||
Future<bool> _removeAllLocalAlbumsAndAssets() async {
|
||||
try {
|
||||
final assets = await _assetRepository.getAllLocal();
|
||||
final assets = await _db.assets.where().localIdIsNotNull().findAll();
|
||||
final (toDelete, toUpdate) =
|
||||
_handleAssetRemoval(assets, [], remote: false);
|
||||
await _assetRepository.transaction(() async {
|
||||
await _assetRepository.deleteById(toDelete);
|
||||
await _assetRepository.updateAll(toUpdate);
|
||||
await _albumRepository.deleteAllLocal();
|
||||
await _db.writeTxn(() async {
|
||||
await _db.assets.deleteAll(toDelete);
|
||||
await _db.assets.putAll(toUpdate);
|
||||
await _db.albums.where().localIdIsNotNull().deleteAll();
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
const downloadGroupImage = 'group_image';
|
||||
const downloadGroupVideo = 'group_video';
|
||||
const downloadGroupLivePhoto = 'group_livephoto';
|
||||
@@ -131,7 +131,11 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
processing.value = true;
|
||||
if (shareLocal) {
|
||||
// Share = Download + Send to OS specific share sheet
|
||||
handleShareAssets(ref, context, selection.value);
|
||||
// Filter offline assets since we cannot fetch their original file
|
||||
final liveAssets = selection.value.nonOfflineOnly(
|
||||
errorCallback: errorBuilder('asset_action_share_err_offline'.tr()),
|
||||
);
|
||||
handleShareAssets(ref, context, liveAssets);
|
||||
} else {
|
||||
final ids =
|
||||
remoteSelection(errorMessage: "home_page_share_err_local".tr())
|
||||
|
||||
@@ -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/shared_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.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/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
@@ -172,16 +172,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
}
|
||||
|
||||
shareAsset() {
|
||||
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);
|
||||
ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context);
|
||||
}
|
||||
|
||||
void handleEdit() async {
|
||||
@@ -211,17 +202,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
if (asset.isLocal) {
|
||||
return;
|
||||
}
|
||||
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(
|
||||
ref.read(imageViewerStateProvider.notifier).downloadAsset(
|
||||
asset,
|
||||
context,
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/providers/asset_viewer/download.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.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/providers/backup/manual_upload.provider.dart';
|
||||
@@ -94,7 +94,7 @@ class GalleryAppBar extends ConsumerWidget {
|
||||
}
|
||||
|
||||
handleDownloadAsset() {
|
||||
ref.read(downloadStateProvider.notifier).downloadAsset(asset, context);
|
||||
ref.read(imageViewerStateProvider.notifier).downloadAsset(asset, context);
|
||||
}
|
||||
|
||||
return IgnorePointer(
|
||||
|
||||
@@ -176,7 +176,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
populateTestLoginInfo1() {
|
||||
usernameController.text = 'testuser@email.com';
|
||||
passwordController.text = 'password';
|
||||
serverEndpointController.text = 'http://192.168.1.16:2283/api';
|
||||
serverEndpointController.text = 'http://10.1.15.216:2283/api';
|
||||
}
|
||||
|
||||
login() async {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
library photo_view;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller.dart';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
library photo_view_gallery;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/photo_view.dart'
|
||||
|
||||
5
mobile/openapi/README.md
generated
5
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.116.2
|
||||
- API version: 1.116.0
|
||||
- Generator version: 7.8.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
@@ -183,7 +183,6 @@ Class | Method | HTTP request | Description
|
||||
*ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage |
|
||||
*ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types |
|
||||
*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* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license |
|
||||
*SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
|
||||
@@ -401,7 +400,6 @@ Class | Method | HTTP request | Description
|
||||
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
|
||||
- [ServerStorageResponseDto](doc//ServerStorageResponseDto.md)
|
||||
- [ServerThemeDto](doc//ServerThemeDto.md)
|
||||
- [ServerVersionHistoryResponseDto](doc//ServerVersionHistoryResponseDto.md)
|
||||
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
|
||||
- [SessionResponseDto](doc//SessionResponseDto.md)
|
||||
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
|
||||
@@ -418,7 +416,6 @@ Class | Method | HTTP request | Description
|
||||
- [SystemConfigDto](doc//SystemConfigDto.md)
|
||||
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
|
||||
- [SystemConfigFacesDto](doc//SystemConfigFacesDto.md)
|
||||
- [SystemConfigGeneratedImageDto](doc//SystemConfigGeneratedImageDto.md)
|
||||
- [SystemConfigImageDto](doc//SystemConfigImageDto.md)
|
||||
- [SystemConfigJobDto](doc//SystemConfigJobDto.md)
|
||||
- [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md)
|
||||
|
||||
2
mobile/openapi/lib/api.dart
generated
2
mobile/openapi/lib/api.dart
generated
@@ -213,7 +213,6 @@ part 'model/server_ping_response.dart';
|
||||
part 'model/server_stats_response_dto.dart';
|
||||
part 'model/server_storage_response_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/session_response_dto.dart';
|
||||
part 'model/shared_link_create_dto.dart';
|
||||
@@ -230,7 +229,6 @@ part 'model/stack_update_dto.dart';
|
||||
part 'model/system_config_dto.dart';
|
||||
part 'model/system_config_f_fmpeg_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_job_dto.dart';
|
||||
part 'model/system_config_library_dto.dart';
|
||||
|
||||
9
mobile/openapi/lib/api/search_api.dart
generated
9
mobile/openapi/lib/api/search_api.dart
generated
@@ -383,7 +383,7 @@ class SearchApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [RandomSearchDto] randomSearchDto (required):
|
||||
Future<List<AssetResponseDto>?> searchRandom(RandomSearchDto randomSearchDto,) async {
|
||||
Future<SearchResponseDto?> searchRandom(RandomSearchDto randomSearchDto,) async {
|
||||
final response = await searchRandomWithHttpInfo(randomSearchDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
@@ -392,11 +392,8 @@ class SearchApi {
|
||||
// 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<AssetResponseDto>') as List)
|
||||
.cast<AssetResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchResponseDto',) as SearchResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
44
mobile/openapi/lib/api/server_api.dart
generated
44
mobile/openapi/lib/api/server_api.dart
generated
@@ -418,50 +418,6 @@ class ServerApi {
|
||||
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].
|
||||
Future<Response> pingServerWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
|
||||
4
mobile/openapi/lib/api_client.dart
generated
4
mobile/openapi/lib/api_client.dart
generated
@@ -480,8 +480,6 @@ class ApiClient {
|
||||
return ServerStorageResponseDto.fromJson(value);
|
||||
case 'ServerThemeDto':
|
||||
return ServerThemeDto.fromJson(value);
|
||||
case 'ServerVersionHistoryResponseDto':
|
||||
return ServerVersionHistoryResponseDto.fromJson(value);
|
||||
case 'ServerVersionResponseDto':
|
||||
return ServerVersionResponseDto.fromJson(value);
|
||||
case 'SessionResponseDto':
|
||||
@@ -514,8 +512,6 @@ class ApiClient {
|
||||
return SystemConfigFFmpegDto.fromJson(value);
|
||||
case 'SystemConfigFacesDto':
|
||||
return SystemConfigFacesDto.fromJson(value);
|
||||
case 'SystemConfigGeneratedImageDto':
|
||||
return SystemConfigGeneratedImageDto.fromJson(value);
|
||||
case 'SystemConfigImageDto':
|
||||
return SystemConfigImageDto.fromJson(value);
|
||||
case 'SystemConfigJobDto':
|
||||
|
||||
20
mobile/openapi/lib/model/random_search_dto.dart
generated
20
mobile/openapi/lib/model/random_search_dto.dart
generated
@@ -29,6 +29,7 @@ class RandomSearchDto {
|
||||
this.libraryId,
|
||||
this.make,
|
||||
this.model,
|
||||
this.page,
|
||||
this.personIds = const [],
|
||||
this.size,
|
||||
this.state,
|
||||
@@ -144,6 +145,15 @@ class RandomSearchDto {
|
||||
|
||||
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;
|
||||
|
||||
/// Minimum value: 1
|
||||
@@ -266,6 +276,7 @@ class RandomSearchDto {
|
||||
other.libraryId == libraryId &&
|
||||
other.make == make &&
|
||||
other.model == model &&
|
||||
other.page == page &&
|
||||
_deepEquality.equals(other.personIds, personIds) &&
|
||||
other.size == size &&
|
||||
other.state == state &&
|
||||
@@ -301,6 +312,7 @@ class RandomSearchDto {
|
||||
(libraryId == null ? 0 : libraryId!.hashCode) +
|
||||
(make == null ? 0 : make!.hashCode) +
|
||||
(model == null ? 0 : model!.hashCode) +
|
||||
(page == null ? 0 : page!.hashCode) +
|
||||
(personIds.hashCode) +
|
||||
(size == null ? 0 : size!.hashCode) +
|
||||
(state == null ? 0 : state!.hashCode) +
|
||||
@@ -318,7 +330,7 @@ class RandomSearchDto {
|
||||
(withStacked == null ? 0 : withStacked!.hashCode);
|
||||
|
||||
@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, 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, 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]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -401,6 +413,11 @@ class RandomSearchDto {
|
||||
json[r'model'] = this.model;
|
||||
} else {
|
||||
// json[r'model'] = null;
|
||||
}
|
||||
if (this.page != null) {
|
||||
json[r'page'] = this.page;
|
||||
} else {
|
||||
// json[r'page'] = null;
|
||||
}
|
||||
json[r'personIds'] = this.personIds;
|
||||
if (this.size != null) {
|
||||
@@ -497,6 +514,7 @@ class RandomSearchDto {
|
||||
libraryId: mapValueOfType<String>(json, r'libraryId'),
|
||||
make: mapValueOfType<String>(json, r'make'),
|
||||
model: mapValueOfType<String>(json, r'model'),
|
||||
page: num.parse('${json[r'page']}'),
|
||||
personIds: json[r'personIds'] is Iterable
|
||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
|
||||
@@ -28,10 +28,6 @@ class ServerAboutResponseDto {
|
||||
this.sourceCommit,
|
||||
this.sourceRef,
|
||||
this.sourceUrl,
|
||||
this.thirdPartyBugFeatureUrl,
|
||||
this.thirdPartyDocumentationUrl,
|
||||
this.thirdPartySourceUrl,
|
||||
this.thirdPartySupportUrl,
|
||||
required this.version,
|
||||
required this.versionUrl,
|
||||
});
|
||||
@@ -150,38 +146,6 @@ class ServerAboutResponseDto {
|
||||
///
|
||||
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 versionUrl;
|
||||
@@ -203,10 +167,6 @@ class ServerAboutResponseDto {
|
||||
other.sourceCommit == sourceCommit &&
|
||||
other.sourceRef == sourceRef &&
|
||||
other.sourceUrl == sourceUrl &&
|
||||
other.thirdPartyBugFeatureUrl == thirdPartyBugFeatureUrl &&
|
||||
other.thirdPartyDocumentationUrl == thirdPartyDocumentationUrl &&
|
||||
other.thirdPartySourceUrl == thirdPartySourceUrl &&
|
||||
other.thirdPartySupportUrl == thirdPartySupportUrl &&
|
||||
other.version == version &&
|
||||
other.versionUrl == versionUrl;
|
||||
|
||||
@@ -228,15 +188,11 @@ class ServerAboutResponseDto {
|
||||
(sourceCommit == null ? 0 : sourceCommit!.hashCode) +
|
||||
(sourceRef == null ? 0 : sourceRef!.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) +
|
||||
(versionUrl.hashCode);
|
||||
|
||||
@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, thirdPartyBugFeatureUrl=$thirdPartyBugFeatureUrl, thirdPartyDocumentationUrl=$thirdPartyDocumentationUrl, thirdPartySourceUrl=$thirdPartySourceUrl, thirdPartySupportUrl=$thirdPartySupportUrl, 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, version=$version, versionUrl=$versionUrl]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -310,26 +266,6 @@ class ServerAboutResponseDto {
|
||||
json[r'sourceUrl'] = this.sourceUrl;
|
||||
} else {
|
||||
// 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'versionUrl'] = this.versionUrl;
|
||||
@@ -360,10 +296,6 @@ class ServerAboutResponseDto {
|
||||
sourceCommit: mapValueOfType<String>(json, r'sourceCommit'),
|
||||
sourceRef: mapValueOfType<String>(json, r'sourceRef'),
|
||||
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')!,
|
||||
versionUrl: mapValueOfType<String>(json, r'versionUrl')!,
|
||||
);
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
//
|
||||
// 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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
//
|
||||
// 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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,42 +15,64 @@ class SystemConfigImageDto {
|
||||
SystemConfigImageDto({
|
||||
required this.colorspace,
|
||||
required this.extractEmbedded,
|
||||
required this.preview,
|
||||
required this.thumbnail,
|
||||
required this.previewFormat,
|
||||
required this.previewSize,
|
||||
required this.quality,
|
||||
required this.thumbnailFormat,
|
||||
required this.thumbnailSize,
|
||||
});
|
||||
|
||||
Colorspace colorspace;
|
||||
|
||||
bool extractEmbedded;
|
||||
|
||||
SystemConfigGeneratedImageDto preview;
|
||||
ImageFormat previewFormat;
|
||||
|
||||
SystemConfigGeneratedImageDto thumbnail;
|
||||
/// Minimum value: 1
|
||||
int previewSize;
|
||||
|
||||
/// Minimum value: 1
|
||||
/// Maximum value: 100
|
||||
int quality;
|
||||
|
||||
ImageFormat thumbnailFormat;
|
||||
|
||||
/// Minimum value: 1
|
||||
int thumbnailSize;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto &&
|
||||
other.colorspace == colorspace &&
|
||||
other.extractEmbedded == extractEmbedded &&
|
||||
other.preview == preview &&
|
||||
other.thumbnail == thumbnail;
|
||||
other.previewFormat == previewFormat &&
|
||||
other.previewSize == previewSize &&
|
||||
other.quality == quality &&
|
||||
other.thumbnailFormat == thumbnailFormat &&
|
||||
other.thumbnailSize == thumbnailSize;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(colorspace.hashCode) +
|
||||
(extractEmbedded.hashCode) +
|
||||
(preview.hashCode) +
|
||||
(thumbnail.hashCode);
|
||||
(previewFormat.hashCode) +
|
||||
(previewSize.hashCode) +
|
||||
(quality.hashCode) +
|
||||
(thumbnailFormat.hashCode) +
|
||||
(thumbnailSize.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, preview=$preview, thumbnail=$thumbnail]';
|
||||
String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'colorspace'] = this.colorspace;
|
||||
json[r'extractEmbedded'] = this.extractEmbedded;
|
||||
json[r'preview'] = this.preview;
|
||||
json[r'thumbnail'] = this.thumbnail;
|
||||
json[r'previewFormat'] = this.previewFormat;
|
||||
json[r'previewSize'] = this.previewSize;
|
||||
json[r'quality'] = this.quality;
|
||||
json[r'thumbnailFormat'] = this.thumbnailFormat;
|
||||
json[r'thumbnailSize'] = this.thumbnailSize;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -65,8 +87,11 @@ class SystemConfigImageDto {
|
||||
return SystemConfigImageDto(
|
||||
colorspace: Colorspace.fromJson(json[r'colorspace'])!,
|
||||
extractEmbedded: mapValueOfType<bool>(json, r'extractEmbedded')!,
|
||||
preview: SystemConfigGeneratedImageDto.fromJson(json[r'preview'])!,
|
||||
thumbnail: SystemConfigGeneratedImageDto.fromJson(json[r'thumbnail'])!,
|
||||
previewFormat: ImageFormat.fromJson(json[r'previewFormat'])!,
|
||||
previewSize: mapValueOfType<int>(json, r'previewSize')!,
|
||||
quality: mapValueOfType<int>(json, r'quality')!,
|
||||
thumbnailFormat: ImageFormat.fromJson(json[r'thumbnailFormat'])!,
|
||||
thumbnailSize: mapValueOfType<int>(json, r'thumbnailSize')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -116,8 +141,11 @@ class SystemConfigImageDto {
|
||||
static const requiredKeys = <String>{
|
||||
'colorspace',
|
||||
'extractEmbedded',
|
||||
'preview',
|
||||
'thumbnail',
|
||||
'previewFormat',
|
||||
'previewSize',
|
||||
'quality',
|
||||
'thumbnailFormat',
|
||||
'thumbnailSize',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user