Compare commits

..

1 Commits

Author SHA1 Message Date
ben-basten
e628e6a807 feat(web): scrollable context menus 2024-09-26 23:13:46 -04:00
266 changed files with 3710 additions and 6386 deletions

View File

@@ -88,7 +88,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }} type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@v6.9.0 uses: docker/build-push-action@v6.7.0
with: with:
file: cli/Dockerfile file: cli/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64

View File

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

View File

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

View File

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

180
cli/package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.22", "version": "2.2.20",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"type": "module", "type": "module",
"exports": "./dist/index.js", "exports": "./dist/index.js",
@@ -20,7 +20,7 @@
"@types/cli-progress": "^3.11.0", "@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^20.16.9", "@types/node": "^20.16.5",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5", "@vitest/coverage-v8": "^2.0.5",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -187,7 +187,7 @@ However, when the trash is emptied, the files will re-appear in the main timelin
### How does smart search work? ### How does smart search work?
Immich uses CLIP models. 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? ### How does facial recognition work?

View File

@@ -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
```

View File

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

View File

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

29
docs/package-lock.json generated
View File

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

View File

@@ -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", "label": "v1.116.0",
"url": "https://v1.116.0.archive.immich.app" "url": "https://v1.116.0.archive.immich.app"

249
e2e/package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -64,19 +64,19 @@ custom_lint:
allowed: allowed:
# required / wanted # required / wanted
- lib/entities/*.entity.dart - lib/entities/*.entity.dart
- lib/repositories/{album,asset,backup,database,etag,exif_info,user}.repository.dart - lib/repositories/{album,asset,backup,exif_info,user}.repository.dart
# acceptable exceptions for the time being (until Isar is fully replaced) # acceptable exceptions for the time being
- integration_test/test_utils/general_helper.dart - integration_test/test_utils/general_helper.dart
- lib/main.dart - lib/main.dart
- lib/pages/common/album_asset_selection.page.dart
- lib/routing/router.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/utils/{db,migration,renderlist_generator}.dart
- lib/widgets/asset_grid/asset_grid_data_structure.dart
- test/**.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/{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: - import_rule_openapi:
message: openapi must only be used through ApiRepositories message: openapi must only be used through ApiRepositories

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 161, "android.injected.version.code" => 160,
"android.injected.version.name" => "1.116.2", "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') upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

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

View File

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

View File

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

View File

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

View File

@@ -401,7 +401,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 177; CURRENT_PROJECT_VERSION = 176;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 177; CURRENT_PROJECT_VERSION = 176;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -571,7 +571,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 177; CURRENT_PROJECT_VERSION = 176;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;

View File

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

View File

@@ -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>"]}}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
}

View File

@@ -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);
}

View File

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

View File

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

View File

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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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,
),
],
),
],
),
),
),
);
}
}

View File

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

View File

@@ -275,14 +275,28 @@ class AssetNotifier extends StateNotifier<bool> {
return isSuccess ? remote.toList() : []; 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); 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); 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;
}
}
} }
} }

View File

@@ -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),
)),
);

View File

@@ -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),
)),
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart'; import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart'; import 'package:immich_mobile/repositories/base_api.repository.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
final assetApiRepositoryProvider = Provider( 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 AssetsApi _api;
final SearchApi _searchApi; final SearchApi _searchApi;

View File

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

View File

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

View File

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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import 'package:http/http.dart';
import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/user_api.interface.dart'; import 'package:immich_mobile/interfaces/user_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart'; import 'package:immich_mobile/repositories/base_api.repository.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
final userApiRepositoryProvider = Provider( 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; final UsersApi _api;
UserApiRepository(this._api); UserApiRepository(this._api);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 ?? '',
);
}
}

View File

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

View File

@@ -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();
}
}
}

View File

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

View File

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

View File

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

View File

@@ -131,7 +131,11 @@ class MultiselectGrid extends HookConsumerWidget {
processing.value = true; processing.value = true;
if (shareLocal) { if (shareLocal) {
// Share = Download + Send to OS specific share sheet // Share = Download + Send to OS specific share sheet
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 { } else {
final ids = final ids =
remoteSelection(errorMessage: "home_page_share_err_local".tr()) remoteSelection(errorMessage: "home_page_share_err_local".tr())

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.116.2 - API version: 1.116.0
- Generator version: 7.8.0 - Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen - Build package: org.openapitools.codegen.languages.DartClientCodegen
@@ -183,7 +183,6 @@ Class | Method | HTTP request | Description
*ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage | *ServerApi* | [**getStorage**](doc//ServerApi.md#getstorage) | **GET** /server/storage |
*ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types | *ServerApi* | [**getSupportedMediaTypes**](doc//ServerApi.md#getsupportedmediatypes) | **GET** /server/media-types |
*ServerApi* | [**getTheme**](doc//ServerApi.md#gettheme) | **GET** /server/theme | *ServerApi* | [**getTheme**](doc//ServerApi.md#gettheme) | **GET** /server/theme |
*ServerApi* | [**getVersionHistory**](doc//ServerApi.md#getversionhistory) | **GET** /server/version-history |
*ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping | *ServerApi* | [**pingServer**](doc//ServerApi.md#pingserver) | **GET** /server/ping |
*ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license | *ServerApi* | [**setServerLicense**](doc//ServerApi.md#setserverlicense) | **PUT** /server/license |
*SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions | *SessionsApi* | [**deleteAllSessions**](doc//SessionsApi.md#deleteallsessions) | **DELETE** /sessions |
@@ -401,7 +400,6 @@ Class | Method | HTTP request | Description
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md) - [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
- [ServerStorageResponseDto](doc//ServerStorageResponseDto.md) - [ServerStorageResponseDto](doc//ServerStorageResponseDto.md)
- [ServerThemeDto](doc//ServerThemeDto.md) - [ServerThemeDto](doc//ServerThemeDto.md)
- [ServerVersionHistoryResponseDto](doc//ServerVersionHistoryResponseDto.md)
- [ServerVersionResponseDto](doc//ServerVersionResponseDto.md) - [ServerVersionResponseDto](doc//ServerVersionResponseDto.md)
- [SessionResponseDto](doc//SessionResponseDto.md) - [SessionResponseDto](doc//SessionResponseDto.md)
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md) - [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
@@ -418,7 +416,6 @@ Class | Method | HTTP request | Description
- [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigDto](doc//SystemConfigDto.md)
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
- [SystemConfigFacesDto](doc//SystemConfigFacesDto.md) - [SystemConfigFacesDto](doc//SystemConfigFacesDto.md)
- [SystemConfigGeneratedImageDto](doc//SystemConfigGeneratedImageDto.md)
- [SystemConfigImageDto](doc//SystemConfigImageDto.md) - [SystemConfigImageDto](doc//SystemConfigImageDto.md)
- [SystemConfigJobDto](doc//SystemConfigJobDto.md) - [SystemConfigJobDto](doc//SystemConfigJobDto.md)
- [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md) - [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md)

View File

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

View File

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

View File

@@ -418,50 +418,6 @@ class ServerApi {
return null; return null;
} }
/// Performs an HTTP 'GET /server/version-history' operation and returns the [Response].
Future<Response> getVersionHistoryWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/server/version-history';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<List<ServerVersionHistoryResponseDto>?> getVersionHistory() async {
final response = await getVersionHistoryWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<ServerVersionHistoryResponseDto>') as List)
.cast<ServerVersionHistoryResponseDto>()
.toList(growable: false);
}
return null;
}
/// Performs an HTTP 'GET /server/ping' operation and returns the [Response]. /// Performs an HTTP 'GET /server/ping' operation and returns the [Response].
Future<Response> pingServerWithHttpInfo() async { Future<Response> pingServerWithHttpInfo() async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations

View File

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

View File

@@ -29,6 +29,7 @@ class RandomSearchDto {
this.libraryId, this.libraryId,
this.make, this.make,
this.model, this.model,
this.page,
this.personIds = const [], this.personIds = const [],
this.size, this.size,
this.state, this.state,
@@ -144,6 +145,15 @@ class RandomSearchDto {
String? model; String? model;
/// Minimum value: 1
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
num? page;
List<String> personIds; List<String> personIds;
/// Minimum value: 1 /// Minimum value: 1
@@ -266,6 +276,7 @@ class RandomSearchDto {
other.libraryId == libraryId && other.libraryId == libraryId &&
other.make == make && other.make == make &&
other.model == model && other.model == model &&
other.page == page &&
_deepEquality.equals(other.personIds, personIds) && _deepEquality.equals(other.personIds, personIds) &&
other.size == size && other.size == size &&
other.state == state && other.state == state &&
@@ -301,6 +312,7 @@ class RandomSearchDto {
(libraryId == null ? 0 : libraryId!.hashCode) + (libraryId == null ? 0 : libraryId!.hashCode) +
(make == null ? 0 : make!.hashCode) + (make == null ? 0 : make!.hashCode) +
(model == null ? 0 : model!.hashCode) + (model == null ? 0 : model!.hashCode) +
(page == null ? 0 : page!.hashCode) +
(personIds.hashCode) + (personIds.hashCode) +
(size == null ? 0 : size!.hashCode) + (size == null ? 0 : size!.hashCode) +
(state == null ? 0 : state!.hashCode) + (state == null ? 0 : state!.hashCode) +
@@ -318,7 +330,7 @@ class RandomSearchDto {
(withStacked == null ? 0 : withStacked!.hashCode); (withStacked == null ? 0 : withStacked!.hashCode);
@override @override
String toString() => 'RandomSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, 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() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -401,6 +413,11 @@ class RandomSearchDto {
json[r'model'] = this.model; json[r'model'] = this.model;
} else { } else {
// json[r'model'] = null; // json[r'model'] = null;
}
if (this.page != null) {
json[r'page'] = this.page;
} else {
// json[r'page'] = null;
} }
json[r'personIds'] = this.personIds; json[r'personIds'] = this.personIds;
if (this.size != null) { if (this.size != null) {
@@ -497,6 +514,7 @@ class RandomSearchDto {
libraryId: mapValueOfType<String>(json, r'libraryId'), libraryId: mapValueOfType<String>(json, r'libraryId'),
make: mapValueOfType<String>(json, r'make'), make: mapValueOfType<String>(json, r'make'),
model: mapValueOfType<String>(json, r'model'), model: mapValueOfType<String>(json, r'model'),
page: num.parse('${json[r'page']}'),
personIds: json[r'personIds'] is Iterable personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],

View File

@@ -28,10 +28,6 @@ class ServerAboutResponseDto {
this.sourceCommit, this.sourceCommit,
this.sourceRef, this.sourceRef,
this.sourceUrl, this.sourceUrl,
this.thirdPartyBugFeatureUrl,
this.thirdPartyDocumentationUrl,
this.thirdPartySourceUrl,
this.thirdPartySupportUrl,
required this.version, required this.version,
required this.versionUrl, required this.versionUrl,
}); });
@@ -150,38 +146,6 @@ class ServerAboutResponseDto {
/// ///
String? sourceUrl; String? sourceUrl;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? thirdPartyBugFeatureUrl;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? thirdPartyDocumentationUrl;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? thirdPartySourceUrl;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? thirdPartySupportUrl;
String version; String version;
String versionUrl; String versionUrl;
@@ -203,10 +167,6 @@ class ServerAboutResponseDto {
other.sourceCommit == sourceCommit && other.sourceCommit == sourceCommit &&
other.sourceRef == sourceRef && other.sourceRef == sourceRef &&
other.sourceUrl == sourceUrl && other.sourceUrl == sourceUrl &&
other.thirdPartyBugFeatureUrl == thirdPartyBugFeatureUrl &&
other.thirdPartyDocumentationUrl == thirdPartyDocumentationUrl &&
other.thirdPartySourceUrl == thirdPartySourceUrl &&
other.thirdPartySupportUrl == thirdPartySupportUrl &&
other.version == version && other.version == version &&
other.versionUrl == versionUrl; other.versionUrl == versionUrl;
@@ -228,15 +188,11 @@ class ServerAboutResponseDto {
(sourceCommit == null ? 0 : sourceCommit!.hashCode) + (sourceCommit == null ? 0 : sourceCommit!.hashCode) +
(sourceRef == null ? 0 : sourceRef!.hashCode) + (sourceRef == null ? 0 : sourceRef!.hashCode) +
(sourceUrl == null ? 0 : sourceUrl!.hashCode) + (sourceUrl == null ? 0 : sourceUrl!.hashCode) +
(thirdPartyBugFeatureUrl == null ? 0 : thirdPartyBugFeatureUrl!.hashCode) +
(thirdPartyDocumentationUrl == null ? 0 : thirdPartyDocumentationUrl!.hashCode) +
(thirdPartySourceUrl == null ? 0 : thirdPartySourceUrl!.hashCode) +
(thirdPartySupportUrl == null ? 0 : thirdPartySupportUrl!.hashCode) +
(version.hashCode) + (version.hashCode) +
(versionUrl.hashCode); (versionUrl.hashCode);
@override @override
String toString() => 'ServerAboutResponseDto[build=$build, buildImage=$buildImage, buildImageUrl=$buildImageUrl, buildUrl=$buildUrl, exiftool=$exiftool, ffmpeg=$ffmpeg, imagemagick=$imagemagick, libvips=$libvips, licensed=$licensed, nodejs=$nodejs, repository=$repository, repositoryUrl=$repositoryUrl, sourceCommit=$sourceCommit, sourceRef=$sourceRef, sourceUrl=$sourceUrl, 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() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -310,26 +266,6 @@ class ServerAboutResponseDto {
json[r'sourceUrl'] = this.sourceUrl; json[r'sourceUrl'] = this.sourceUrl;
} else { } else {
// json[r'sourceUrl'] = null; // json[r'sourceUrl'] = null;
}
if (this.thirdPartyBugFeatureUrl != null) {
json[r'thirdPartyBugFeatureUrl'] = this.thirdPartyBugFeatureUrl;
} else {
// json[r'thirdPartyBugFeatureUrl'] = null;
}
if (this.thirdPartyDocumentationUrl != null) {
json[r'thirdPartyDocumentationUrl'] = this.thirdPartyDocumentationUrl;
} else {
// json[r'thirdPartyDocumentationUrl'] = null;
}
if (this.thirdPartySourceUrl != null) {
json[r'thirdPartySourceUrl'] = this.thirdPartySourceUrl;
} else {
// json[r'thirdPartySourceUrl'] = null;
}
if (this.thirdPartySupportUrl != null) {
json[r'thirdPartySupportUrl'] = this.thirdPartySupportUrl;
} else {
// json[r'thirdPartySupportUrl'] = null;
} }
json[r'version'] = this.version; json[r'version'] = this.version;
json[r'versionUrl'] = this.versionUrl; json[r'versionUrl'] = this.versionUrl;
@@ -360,10 +296,6 @@ class ServerAboutResponseDto {
sourceCommit: mapValueOfType<String>(json, r'sourceCommit'), sourceCommit: mapValueOfType<String>(json, r'sourceCommit'),
sourceRef: mapValueOfType<String>(json, r'sourceRef'), sourceRef: mapValueOfType<String>(json, r'sourceRef'),
sourceUrl: mapValueOfType<String>(json, r'sourceUrl'), sourceUrl: mapValueOfType<String>(json, r'sourceUrl'),
thirdPartyBugFeatureUrl: mapValueOfType<String>(json, r'thirdPartyBugFeatureUrl'),
thirdPartyDocumentationUrl: mapValueOfType<String>(json, r'thirdPartyDocumentationUrl'),
thirdPartySourceUrl: mapValueOfType<String>(json, r'thirdPartySourceUrl'),
thirdPartySupportUrl: mapValueOfType<String>(json, r'thirdPartySupportUrl'),
version: mapValueOfType<String>(json, r'version')!, version: mapValueOfType<String>(json, r'version')!,
versionUrl: mapValueOfType<String>(json, r'versionUrl')!, versionUrl: mapValueOfType<String>(json, r'versionUrl')!,
); );

View File

@@ -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',
};
}

View File

@@ -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',
};
}

View File

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

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