Compare commits

..

24 Commits

Author SHA1 Message Date
shenlong-tanwen
438fa46cbc rename cloudId to icloudId 2025-09-09 22:17:11 +05:30
shenlong-tanwen
89bb0ca0c9 rename cloud id 2025-09-09 22:12:16 +05:30
shenlong-tanwen
84fcbd4df8 fix sync and show the button on iOS only 2025-09-09 21:38:29 +05:30
shenlong-tanwen
de1c6d7182 update migration 2025-09-09 01:11:29 +05:30
shenlong-tanwen
53fc603d91 merge main
# Conflicts:
#	mobile/drift_schemas/main/drift_schema_v10.json
#	mobile/lib/infrastructure/repositories/db.repository.dart
#	mobile/lib/infrastructure/repositories/db.repository.drift.dart
#	mobile/lib/infrastructure/repositories/db.repository.steps.dart
#	mobile/test/drift/main/generated/schema_v10.dart
2025-09-09 01:06:03 +05:30
shenlong-tanwen
67fb9b7a2a hash after remote sync 2025-09-06 02:35:56 +05:30
shenlong-tanwen
03cd491197 use a separate table instead of a column on remote asset 2025-09-06 02:16:53 +05:30
shenlong-tanwen
5236a72fb3 migrate hashes from remote asset table 2025-09-06 01:58:08 +05:30
shenlong-tanwen
b11ea52704 re-add migration and fix upload from main timeline 2025-09-06 01:57:40 +05:30
shenlong-tanwen
e2c87c2042 more fixes 2025-09-06 01:57:40 +05:30
shenlong-tanwen
9cf5d83707 more fixes 2025-09-06 01:57:30 +05:30
shenlong-tanwen
254ca4a13d fix types 2025-09-06 01:57:24 +05:30
shenlong-tanwen
82c93cf325 handle cloud id migration 2025-09-06 01:57:16 +05:30
shenlong-tanwen
d02d3b5472 add generated cloudID column with index 2025-09-06 01:56:31 +05:30
shenlong-tanwen
867f4fc53a retry cloud id mapping on migration 2025-09-06 01:56:22 +05:30
shenlong-tanwen
d81ee18238 sync remote asset metadata 2025-09-06 01:56:22 +05:30
shenlong-tanwen
b93b07f461 rebase main 2025-09-06 01:56:09 +05:30
shenlong-tanwen
36680e4279 send cloudId during upload
# Conflicts:
#	mobile/lib/services/upload.service.dart
2025-09-06 01:55:54 +05:30
shenlong-tanwen
816fb1746a rebase on server changes 2025-09-06 01:55:54 +05:30
shenlong-tanwen
4b7d61ce97 add migration
# Conflicts:
#	mobile/lib/utils/migration.dart
2025-09-06 01:55:54 +05:30
shenlong-tanwen
bfadf68e15 map cloud Ids during local sync 2025-09-06 01:55:23 +05:30
shenlong-tanwen
61e079a63e store cloudId in sqlite 2025-09-06 01:55:23 +05:30
shenlong-tanwen
ecbaca3cee feat: add cloud id during native sync
# Conflicts:
#	mobile/lib/platform/native_sync_api.g.dart
2025-09-06 01:55:23 +05:30
Jason Rasmussen
ada4265cf9 feat: asset metadata
# Conflicts:
#	mobile/openapi/README.md
#	mobile/openapi/lib/api.dart
#	mobile/openapi/lib/api_client.dart
#	mobile/openapi/lib/api_helper.dart
#	mobile/openapi/lib/model/sync_entity_type.dart
#	server/src/dtos/asset.dto.ts
#	server/src/queries/asset.repository.sql
#	server/src/queries/sync.repository.sql
#	server/src/repositories/asset.repository.ts
#	server/src/repositories/sync.repository.ts
#	server/src/services/sync.service.ts
2025-09-06 01:55:23 +05:30
135 changed files with 11603 additions and 1817 deletions

View File

@@ -35,21 +35,22 @@ jobs:
needs: [get_body, should_run] needs: [get_body, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' }} if: ${{ needs.should_run.outputs.should_run == 'true' }}
container: container:
image: yshavit/mdq:0.9.0@sha256:4399483ca857fb1a7ed28a596f754c7373e358647de31ce14b79a27c91e1e35e image: yshavit/mdq:0.8.0@sha256:c69224d34224a0043d9a3ee46679ba4a2a25afaac445f293d92afe13cd47fcea
outputs: outputs:
checked: ${{ steps.get_checkbox.outputs.checked }} json: ${{ steps.get_checkbox.outputs.json }}
steps: steps:
- id: get_checkbox - id: get_checkbox
env: env:
BODY: ${{ needs.get_body.outputs.body }} BODY: ${{ needs.get_body.outputs.body }}
# TODO: We should detect if the checkbox is missing entirely and also close_and_comment in that case.
run: | run: |
CHECKED=$(echo "$BODY" | base64 -d | /mdq --output json '# I have searched | - [?] Yes' | jq '.items[0].list[0].checked // false') JSON=$(echo "$BODY" | base64 -d | /mdq --output json '# I have searched | - [?] Yes')
echo "checked=$CHECKED" >> $GITHUB_OUTPUT echo "json=$JSON" >> $GITHUB_OUTPUT
close_and_comment: close_and_comment:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [get_checkbox_json, should_run] needs: [get_checkbox_json, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' && needs.get_checkbox_json.outputs.checked != 'true' }} if: ${{ needs.should_run.outputs.should_run == 'true' && !fromJSON(needs.get_checkbox_json.outputs.json).items[0].list[0].checked }}
permissions: permissions:
issues: write issues: write
discussions: write discussions: write

View File

@@ -50,7 +50,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 uses: github/codeql-action/init@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -63,7 +63,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 uses: github/codeql-action/autobuild@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -76,6 +76,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 uses: github/codeql-action/analyze@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'

View File

@@ -20,7 +20,7 @@ jobs:
run: echo 'The triggering workflow did not succeed' && exit 1 run: echo 'The triggering workflow did not succeed' && exit 1
- name: Get artifact - name: Get artifact
id: get-artifact id: get-artifact
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with: with:
script: | script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
@@ -38,7 +38,7 @@ jobs:
return { found: true, id: matchArtifact.id }; return { found: true, id: matchArtifact.id };
- name: Determine deploy parameters - name: Determine deploy parameters
id: parameters id: parameters
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
env: env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }} HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
with: with:
@@ -114,7 +114,7 @@ jobs:
- name: Load parameters - name: Load parameters
id: parameters id: parameters
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
env: env:
PARAM_JSON: ${{ needs.checks.outputs.parameters }} PARAM_JSON: ${{ needs.checks.outputs.parameters }}
with: with:
@@ -125,7 +125,7 @@ jobs:
core.setOutput("shouldDeploy", parameters.shouldDeploy); core.setOutput("shouldDeploy", parameters.shouldDeploy);
- name: Download artifact - name: Download artifact
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
env: env:
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }} ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
with: with:

View File

@@ -45,7 +45,7 @@ jobs:
message: 'chore: fix formatting' message: 'chore: fix formatting'
- name: Remove label - name: Remove label
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
if: always() if: always()
with: with:
script: | script: |

View File

@@ -23,24 +23,21 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
PR=$(gh pr list --repo $GITHUB_REPOSITORY --author weblate --json number,mergeable) gh pr list --repo $GITHUB_REPOSITORY --author weblate --json number,mergeable | read PR
echo "$PR" echo "$PR"
PR_NUMBER=$(echo "$PR" | jq ' echo "$PR" | jq '
if length == 1 then if length == 1 then
.[0].number .[0].number
else else
error("Expected exactly 1 entry, got \(length)") error("Expected exactly 1 entry, got \(length)")
end end
' 2>&1) || exit 1 ' 2>&1 | read PR_NUMBER || exit 1
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "Selected PR $PR_NUMBER" echo "Selected PR $PR_NUMBER"
if ! echo "$PR" | jq -e '.[0].mergeable == "MERGEABLE"'; then echo "$PR" | jq -e '.[0].mergeable == "MERGEABLE"' || { echo "PR is not mergeable" ; exit 1 }
echo "PR is not mergeable"
exit 1
fi
- name: Generate a token - name: Generate a token
id: generate_token id: generate_token
@@ -67,8 +64,8 @@ jobs:
GH_TOKEN: ${{ steps.generate_token.outputs.token }} GH_TOKEN: ${{ steps.generate_token.outputs.token }}
PR_NUMBER: ${{ steps.find_pr.outputs.PR_NUMBER }} PR_NUMBER: ${{ steps.find_pr.outputs.PR_NUMBER }}
run: | run: |
REVIEW_ID=$(gh api -X POST "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" --field event='APPROVE' --field body='Automatically merging translations PR' \ gh api -X POST "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" --field event='APPROVE' --field body='Automatically merging translations PR' \
| jq '.id') | jq '.id' | read REVIEW_ID
echo "REVIEW_ID=$REVIEW_ID" >> $GITHUB_OUTPUT echo "REVIEW_ID=$REVIEW_ID" >> $GITHUB_OUTPUT
gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --auto --squash gh pr merge "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --auto --squash

View File

@@ -24,6 +24,15 @@ concurrency:
permissions: {} permissions: {}
jobs: jobs:
merge_translations:
uses: ./.github/workflows/merge-translations.yml
permissions:
pull-requests: write
secrets:
PUSH_O_MATIC_APP_ID: ${{ secrets.PUSH_O_MATIC_APP_ID }}
PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
bump_version: bump_version:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:

View File

@@ -24,7 +24,7 @@ jobs:
permissions: permissions:
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with: with:
script: | script: |
github.rest.issues.removeLabel({ github.rest.issues.removeLabel({

View File

@@ -129,7 +129,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file - name: Upload SARIF file
uses: github/codeql-action/upload-sarif@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0 uses: github/codeql-action/upload-sarif@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
with: with:
sarif_file: results.sarif sarif_file: results.sarif
category: zizmor category: zizmor

View File

@@ -594,7 +594,7 @@ jobs:
contents: read contents: read
services: services:
postgres: postgres:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:4f7ee144d4738ad02f6d9376defed7a767b748d185d47eba241578c26a63064b image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3@sha256:ec713143dca1a426eba2e03707c319e2ec3cc9d304ef767f777f8e297dee820c
env: env:
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres POSTGRES_USER: postgres

2
.gitignore vendored
View File

@@ -25,5 +25,3 @@ mobile/ios/fastlane/report.xml
vite.config.js.timestamp-* vite.config.js.timestamp-*
.pnpm-store .pnpm-store
.devcontainer/library
.devcontainer/.env*

View File

@@ -13,6 +13,7 @@
"cli" "cli"
], ],
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.8.0", "@eslint/js": "^9.8.0",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@types/byte-size": "^8.1.0", "@types/byte-size": "^8.1.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.52.3" version = "4.52.1"
constraints = "4.52.3" constraints = "4.52.1"
hashes = [ hashes = [
"h1:3jU62KY4Oj3xzMwkTQWon1nlIvFkgTCqI93IzUGaa0c=", "h1:2lHvafwGbLdmc9lYkuJFw3nsInaQjRpjX/JfIRKmq/M=",
"h1:BWimtYXrvbzbbuoVcyobjQnXjjOb9X69JFTw+GuPxfk=", "h1:596JomwjrtUrOSreq9NNCS+rj70+jOV+0pfja5MXiTI=",
"h1:C/KvLEm8dVQ6zG2X4asLDtmw2JW/xu7E8MddtaXniO0=", "h1:7mBOA5TVAIt3qAwPXKCtE0RSYeqij9v30mnksuBbpEg=",
"h1:Doo0xcLFf+CnfDWjsA7G1NvSLURuwcgyVy8k0NF1gJA=", "h1:ELVgzh4kHKBCYdL+2A8JjWS0E1snLUN3Mmz3Vo6qSfw=",
"h1:Gc3FGDtR8lUWsi9VImnnE5/USDXiIwYsv4Hbl+d2lwY=", "h1:FGGM5yLFf72g3kSXM3LAN64Gf/AkXr5WCmhixgnP+l4=",
"h1:HsDY6s1gup5fW9TeuTUy85QMIld1nDOUFlwsfxIq1ig=", "h1:JupkJbQALcIVoMhHImrLeLDsQR1ET7VJLGC7ONxjqGU=",
"h1:MnHkB56E4b/kT6WZigsZJnB5rgnCfDVbrLBNxIsEXPY=", "h1:KsaE4JNq+1uV1nJsuTcYar/8lyY6zKS5UBEpfYg3wvc=",
"h1:O/FUQEqhtknJNdsaMbIBi2pLWBds2VvN5FsTVVntzb0=", "h1:NHZ5RJIzQDLhie/ykl3uI6UPfNQR9Lu5Ti7JPR6X904=",
"h1:OKQBynkp0J5DIf5FOl/NR3S2rvh89pY+t5wevYxdTJs=", "h1:NfAuMbn6LQPLDtJhbzO1MX9JMIGLMa8K6CpekvtsuX8=",
"h1:On+vPsYV8U/J/8wFZPXjeAgNJqFFQj42vNOKuNKURkY=", "h1:e+vNKokamDsp/kJvFr2pRudzwEz2r49iZ/oSggw+1LY=",
"h1:SPkrMRJahxK0uum7FnUugbGN/JepHMH8M71DBtYrvG0=", "h1:jnb4VdfNZ79I3yj7Q8x+JmOT+FxbfjjRfrF0dL0yCW8=",
"h1:bEh1ASPMiin3F36+hTfjMQTBnuDl2DzjzSCdova3JEM=", "h1:kmF//O539d7NuHU7qIxDj7Wz4eJmLKFiI5glwQivldU=",
"h1:dtIK+x5Q1sh5SMPaHBHXhL9XDIqbRW0EBmVZ+KHQB8E=", "h1:s6XriaKwOgV4jvKAGPXkrxhhOQxpNU5dceZwi9Z/1k8=",
"h1:kZcwWfODMWWyauZ66oaO/X+xXkqBtrbYwfUFEtspwEc=", "h1:wt3WBEBAeSGTlC9OlnTlAALxRiK4SQgLy0KgBIS7qzs=",
"zh:53946fce4a631f1d98c61550821c88edede9169dfe5cc254e09a2ab207f76b3f", "zh:2fb95e1d3229b9b6c704e1a413c7481c60f139780d9641f657b6eb9b633b90f2",
"zh:61654a21f1dd4331492d4ef77e9ebff066bc01e1281f92b925e5697c9138d681", "zh:379c7680983383862236e9e6e720c3114195c40526172188e88d0ffcf50dfe2e",
"zh:6a54e9d129b276f052a2f1b73ad0b8735fe6a7403c6a8f6aa111e525eeefaf35", "zh:55533beb6cfc02d22ffda8cba8027bc2c841bb172cd637ed0d28323d41395f8f",
"zh:7692374e655c346a630b5a7cd776c5e0b2388900dcd7ab69a3af85d0c31c6c43", "zh:5abd70760e4eb1f37a1c307cbd2989ea7c9ba0afb93818c67c1d363a31f75703",
"zh:699f1c8cd66129176fe659ebf0e6337632a8967a28d2630b6ae5948665c0c2ae",
"zh:69c15acd73c451e89de6477059cda2f3ec200b48ae4b9ff3646c4d389fd3205e",
"zh:6e02b687de21b844f8266dff99e93e7c61fc8eb688f4bbb23803caceb251839e",
"zh:7a51d17b87ed87b7bebf2ad9fc7c3a74f16a1b44eee92c779c08eb89258c0496",
"zh:88ad84436837b0f55302f22748505972634e87400d6902260fd6b7ba1610f937",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8fe5b792a4d2b1c3a0e573649642962494faa00299baa6aaf813b9a43203dc02", "zh:8d46c3d9f4f7ad20ac6ef01daa63f4e30a2d16dcb1bb5c7c7ee3dc6be38e9ca1",
"zh:a0f403a4862df90f09de65c6e939d6cfd069a8dda2dd33f82948bf6f5f1124ef", "zh:913d64e72a4929dae1d4793e2004f4f9a58b138ea337d9d94fa35cafbf06550a",
"zh:a25dc3eb60777b600f8f125d321fe7c50b811c5302b58e9a727ceb749a04e35d", "zh:c8d93cf86e2e49f6cec665cfe78b82c144cce15a8b2e30f343385fadd1251849",
"zh:a2f2ac7dc703c69d2e8c67c9cb5620b5348cb4fd6b98515fbe3f478517b56602", "zh:cc4f69397d9bc34a528a5609a024c3a48f54f21616c0008792dd417297add955",
"zh:d452e7bd24445ee14166470cf50f3aca566d46cab5f26f1c5c988c0f3106b697", "zh:df99cdb8b064aad35ffea77e645cf6541d0b1b2ebc51b6d26c42031de60ab69e",
"zh:e10a52b0294735659eb3f0821ad2006ec097918efe58d31d37a5e3c47efef5f6",
"zh:e28dd0954cef9f05adf4d4b440d6f134f605344dfa56307181996675e6550af2",
"zh:f1e3b2f43a472280442f01ba71a3c06c9167432e553381132ea5c4a77e0b6dd5",
"zh:f71fd63718d38fd43829861e91fe79e16d7b4c7c3d508ae3d077368d89b8e5a0",
"zh:faf8d3da4b819c4ae8e565d2b1a684c6a948a086cb299189a5e7b30b2178409d",
] ]
} }

View File

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

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.52.3" version = "4.52.1"
constraints = "4.52.3" constraints = "4.52.1"
hashes = [ hashes = [
"h1:3jU62KY4Oj3xzMwkTQWon1nlIvFkgTCqI93IzUGaa0c=", "h1:2lHvafwGbLdmc9lYkuJFw3nsInaQjRpjX/JfIRKmq/M=",
"h1:BWimtYXrvbzbbuoVcyobjQnXjjOb9X69JFTw+GuPxfk=", "h1:596JomwjrtUrOSreq9NNCS+rj70+jOV+0pfja5MXiTI=",
"h1:C/KvLEm8dVQ6zG2X4asLDtmw2JW/xu7E8MddtaXniO0=", "h1:7mBOA5TVAIt3qAwPXKCtE0RSYeqij9v30mnksuBbpEg=",
"h1:Doo0xcLFf+CnfDWjsA7G1NvSLURuwcgyVy8k0NF1gJA=", "h1:ELVgzh4kHKBCYdL+2A8JjWS0E1snLUN3Mmz3Vo6qSfw=",
"h1:Gc3FGDtR8lUWsi9VImnnE5/USDXiIwYsv4Hbl+d2lwY=", "h1:FGGM5yLFf72g3kSXM3LAN64Gf/AkXr5WCmhixgnP+l4=",
"h1:HsDY6s1gup5fW9TeuTUy85QMIld1nDOUFlwsfxIq1ig=", "h1:JupkJbQALcIVoMhHImrLeLDsQR1ET7VJLGC7ONxjqGU=",
"h1:MnHkB56E4b/kT6WZigsZJnB5rgnCfDVbrLBNxIsEXPY=", "h1:KsaE4JNq+1uV1nJsuTcYar/8lyY6zKS5UBEpfYg3wvc=",
"h1:O/FUQEqhtknJNdsaMbIBi2pLWBds2VvN5FsTVVntzb0=", "h1:NHZ5RJIzQDLhie/ykl3uI6UPfNQR9Lu5Ti7JPR6X904=",
"h1:OKQBynkp0J5DIf5FOl/NR3S2rvh89pY+t5wevYxdTJs=", "h1:NfAuMbn6LQPLDtJhbzO1MX9JMIGLMa8K6CpekvtsuX8=",
"h1:On+vPsYV8U/J/8wFZPXjeAgNJqFFQj42vNOKuNKURkY=", "h1:e+vNKokamDsp/kJvFr2pRudzwEz2r49iZ/oSggw+1LY=",
"h1:SPkrMRJahxK0uum7FnUugbGN/JepHMH8M71DBtYrvG0=", "h1:jnb4VdfNZ79I3yj7Q8x+JmOT+FxbfjjRfrF0dL0yCW8=",
"h1:bEh1ASPMiin3F36+hTfjMQTBnuDl2DzjzSCdova3JEM=", "h1:kmF//O539d7NuHU7qIxDj7Wz4eJmLKFiI5glwQivldU=",
"h1:dtIK+x5Q1sh5SMPaHBHXhL9XDIqbRW0EBmVZ+KHQB8E=", "h1:s6XriaKwOgV4jvKAGPXkrxhhOQxpNU5dceZwi9Z/1k8=",
"h1:kZcwWfODMWWyauZ66oaO/X+xXkqBtrbYwfUFEtspwEc=", "h1:wt3WBEBAeSGTlC9OlnTlAALxRiK4SQgLy0KgBIS7qzs=",
"zh:53946fce4a631f1d98c61550821c88edede9169dfe5cc254e09a2ab207f76b3f", "zh:2fb95e1d3229b9b6c704e1a413c7481c60f139780d9641f657b6eb9b633b90f2",
"zh:61654a21f1dd4331492d4ef77e9ebff066bc01e1281f92b925e5697c9138d681", "zh:379c7680983383862236e9e6e720c3114195c40526172188e88d0ffcf50dfe2e",
"zh:6a54e9d129b276f052a2f1b73ad0b8735fe6a7403c6a8f6aa111e525eeefaf35", "zh:55533beb6cfc02d22ffda8cba8027bc2c841bb172cd637ed0d28323d41395f8f",
"zh:7692374e655c346a630b5a7cd776c5e0b2388900dcd7ab69a3af85d0c31c6c43", "zh:5abd70760e4eb1f37a1c307cbd2989ea7c9ba0afb93818c67c1d363a31f75703",
"zh:699f1c8cd66129176fe659ebf0e6337632a8967a28d2630b6ae5948665c0c2ae",
"zh:69c15acd73c451e89de6477059cda2f3ec200b48ae4b9ff3646c4d389fd3205e",
"zh:6e02b687de21b844f8266dff99e93e7c61fc8eb688f4bbb23803caceb251839e",
"zh:7a51d17b87ed87b7bebf2ad9fc7c3a74f16a1b44eee92c779c08eb89258c0496",
"zh:88ad84436837b0f55302f22748505972634e87400d6902260fd6b7ba1610f937",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8fe5b792a4d2b1c3a0e573649642962494faa00299baa6aaf813b9a43203dc02", "zh:8d46c3d9f4f7ad20ac6ef01daa63f4e30a2d16dcb1bb5c7c7ee3dc6be38e9ca1",
"zh:a0f403a4862df90f09de65c6e939d6cfd069a8dda2dd33f82948bf6f5f1124ef", "zh:913d64e72a4929dae1d4793e2004f4f9a58b138ea337d9d94fa35cafbf06550a",
"zh:a25dc3eb60777b600f8f125d321fe7c50b811c5302b58e9a727ceb749a04e35d", "zh:c8d93cf86e2e49f6cec665cfe78b82c144cce15a8b2e30f343385fadd1251849",
"zh:a2f2ac7dc703c69d2e8c67c9cb5620b5348cb4fd6b98515fbe3f478517b56602", "zh:cc4f69397d9bc34a528a5609a024c3a48f54f21616c0008792dd417297add955",
"zh:d452e7bd24445ee14166470cf50f3aca566d46cab5f26f1c5c988c0f3106b697", "zh:df99cdb8b064aad35ffea77e645cf6541d0b1b2ebc51b6d26c42031de60ab69e",
"zh:e10a52b0294735659eb3f0821ad2006ec097918efe58d31d37a5e3c47efef5f6",
"zh:e28dd0954cef9f05adf4d4b440d6f134f605344dfa56307181996675e6550af2",
"zh:f1e3b2f43a472280442f01ba71a3c06c9167432e553381132ea5c4a77e0b6dd5",
"zh:f71fd63718d38fd43829861e91fe79e16d7b4c7c3d508ae3d077368d89b8e5a0",
"zh:faf8d3da4b819c4ae8e565d2b1a684c6a948a086cb299189a5e7b30b2178409d",
] ]
} }

View File

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

View File

@@ -143,13 +143,13 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:fea8b3e67b15729d4bb70589eb03367bab9ad1ee89c876f54327fc7c6e618571 image: docker.io/valkey/valkey:8-bookworm@sha256:a137a2b60aca1a75130022d6bb96af423fefae4eb55faf395732db3544803280
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
database: database:
container_name: immich_postgres container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:8d292bdb796aa58bbbaa47fe971c8516f6f57d6a47e7172e62754feb6ed4e7b0 image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:32324a2f41df5de9efe1af166b7008c3f55646f8d0e00d9550c16c9822366b4a
env_file: env_file:
- .env - .env
environment: environment:

View File

@@ -56,14 +56,14 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:fea8b3e67b15729d4bb70589eb03367bab9ad1ee89c876f54327fc7c6e618571 image: docker.io/valkey/valkey:8-bookworm@sha256:a137a2b60aca1a75130022d6bb96af423fefae4eb55faf395732db3544803280
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:8d292bdb796aa58bbbaa47fe971c8516f6f57d6a47e7172e62754feb6ed4e7b0 image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:32324a2f41df5de9efe1af166b7008c3f55646f8d0e00d9550c16c9822366b4a
env_file: env_file:
- .env - .env
environment: environment:

View File

@@ -49,14 +49,14 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:8-bookworm@sha256:fea8b3e67b15729d4bb70589eb03367bab9ad1ee89c876f54327fc7c6e618571 image: docker.io/valkey/valkey:8-bookworm@sha256:a137a2b60aca1a75130022d6bb96af423fefae4eb55faf395732db3544803280
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:8d292bdb796aa58bbbaa47fe971c8516f6f57d6a47e7172e62754feb6ed4e7b0 image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:32324a2f41df5de9efe1af166b7008c3f55646f8d0e00d9550c16c9822366b4a
environment: environment:
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME} POSTGRES_USER: ${DB_USERNAME}

View File

@@ -66,7 +66,7 @@ The provided file is just a starting point. There are a ton of ways to configure
After bringing down the containers with `docker compose down` and back up with `docker compose up -d`, a Prometheus instance will now collect metrics from the immich server and microservices containers. Note that we didn't need to expose any new ports for these containers - the communication is handled in the internal Docker network. After bringing down the containers with `docker compose down` and back up with `docker compose up -d`, a Prometheus instance will now collect metrics from the immich server and microservices containers. Note that we didn't need to expose any new ports for these containers - the communication is handled in the internal Docker network.
:::note :::note
To see exactly what metrics are made available, you can additionally add `8081:8081` (API metrics) and `8082:8082` (microservices metrics) to the immich_server container's ports. To see exactly what metrics are made available, you can additionally add `8081:8081` to the server container's ports and `8082:8082` to the microservices container's ports.
Visiting the `/metrics` endpoint for these services will show the same raw data that Prometheus collects. Visiting the `/metrics` endpoint for these services will show the same raw data that Prometheus collects.
To configure these ports see [`IMMICH_API_METRICS_PORT` & `IMMICH_MICROSERVICES_METRICS_PORT`](/docs/install/environment-variables/#general). To configure these ports see [`IMMICH_API_METRICS_PORT` & `IMMICH_MICROSERVICES_METRICS_PORT`](/docs/install/environment-variables/#general).
::: :::

View File

@@ -147,10 +147,7 @@ SELECT "key", "value" FROM "system_metadata" WHERE "key" = 'system-config';
### File properties ### File properties
```sql title="Without thumbnails" ```sql title="Without thumbnails"
SELECT * FROM "asset" SELECT * FROM "asset" WHERE "asset"."previewPath" IS NULL OR "asset"."thumbnailPath" IS NULL;
WHERE (NOT EXISTS (SELECT 1 FROM "asset_file" WHERE "asset"."id" = "asset_file"."assetId" AND "asset_file"."type" = 'thumbnail')
OR NOT EXISTS (SELECT 1 FROM "asset_file" WHERE "asset"."id" = "asset_file"."assetId" AND "asset_file"."type" = 'preview'))
AND "asset"."visibility" = 'timeline';
``` ```
```sql title="Failed file movements" ```sql title="Failed file movements"

View File

@@ -24,6 +24,8 @@
"@mdi/react": "^1.6.1", "@mdi/react": "^1.6.1",
"@mdx-js/react": "^3.0.0", "@mdx-js/react": "^3.0.0",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.17",
"classnames": "^2.3.2",
"clsx": "^2.0.0",
"docusaurus-lunr-search": "^3.3.2", "docusaurus-lunr-search": "^3.3.2",
"docusaurus-preset-openapi": "^0.7.5", "docusaurus-preset-openapi": "^0.7.5",
"lunr": "^2.3.9", "lunr": "^2.3.9",

View File

@@ -105,11 +105,6 @@ const projects: CommunityProjectProps[] = [
description: 'Speed up your machine learning by load balancing your requests to multiple computers', description: 'Speed up your machine learning by load balancing your requests to multiple computers',
url: 'https://github.com/apetersson/immich_ml_balancer', url: 'https://github.com/apetersson/immich_ml_balancer',
}, },
{
title: 'Immich Drop Uploader',
description: 'A tiny, zero-login web app for collecting photos/videos from anyone into your Immich server.',
url: 'https://github.com/Nasogaa/immich-drop',
},
]; ];
function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element { function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {

View File

@@ -19,6 +19,7 @@
"author": "", "author": "",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.8.0", "@eslint/js": "^9.8.0",
"@immich/cli": "file:../cli", "@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
@@ -30,6 +31,7 @@
"@types/pg": "^8.15.1", "@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@vitest/coverage-v8": "^3.0.0",
"eslint": "^9.14.0", "eslint": "^9.14.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",

View File

@@ -1466,10 +1466,10 @@ describe('/asset', () => {
expectedDate: '2023-04-04T04:00:00.000Z', expectedDate: '2023-04-04T04:00:00.000Z',
}, },
{ {
name: 'CreationDate when DateTimeOriginal missing', name: 'CreateDate when DateTimeOriginal missing',
exifData: { exifData: {
CreationDate: '2023:05:05 05:00:00', // TESTABLE CreateDate: '2023:05:05 05:00:00', // TESTABLE
CreateDate: '2023:07:07 07:00:00', // TESTABLE CreationDate: '2023:07:07 07:00:00', // TESTABLE
GPSDateTime: '2023:10:10 10:00:00', // TESTABLE GPSDateTime: '2023:10:10 10:00:00', // TESTABLE
}, },
expectedDate: '2023-05-05T05:00:00.000Z', expectedDate: '2023-05-05T05:00:00.000Z',

View File

@@ -597,6 +597,8 @@
"backup_setting_subtitle": "Manage background and foreground upload settings", "backup_setting_subtitle": "Manage background and foreground upload settings",
"backup_settings_subtitle": "Manage upload settings", "backup_settings_subtitle": "Manage upload settings",
"backward": "Backward", "backward": "Backward",
"beta_sync": "Beta Sync Status",
"beta_sync_subtitle": "Manage the new sync system",
"biometric_auth_enabled": "Biometric authentication enabled", "biometric_auth_enabled": "Biometric authentication enabled",
"biometric_locked_out": "You are locked out of biometric authentication", "biometric_locked_out": "You are locked out of biometric authentication",
"biometric_no_options": "No biometric options available", "biometric_no_options": "No biometric options available",
@@ -1074,7 +1076,10 @@
"gcast_enabled": "Google Cast", "gcast_enabled": "Google Cast",
"gcast_enabled_description": "This feature loads external resources from Google in order to work.", "gcast_enabled_description": "This feature loads external resources from Google in order to work.",
"general": "General", "general": "General",
"geolocation_instruction_all_have_location": "All assets for this date already have location data. Try showing all assets or select a different date",
"geolocation_instruction_location": "Click on an asset with GPS coordinates to use its location, or select a location directly from the map", "geolocation_instruction_location": "Click on an asset with GPS coordinates to use its location, or select a location directly from the map",
"geolocation_instruction_no_date": "Select a date to manage location data for photos and videos from that day",
"geolocation_instruction_no_photos": "No photos or videos found for this date. Select a different date to show them",
"get_help": "Get Help", "get_help": "Get Help",
"get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
"getting_started": "Getting Started", "getting_started": "Getting Started",
@@ -1515,7 +1520,7 @@
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
"profile_drawer_github": "GitHub", "profile_drawer_github": "GitHub",
"profile_drawer_readonly_mode": "Read-only mode enabled. Long-press the user avatar icon to exit.", "profile_drawer_readonly_mode": "Read-only mode enabled. Double-tap the user avatar icon to exit.",
"profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.",
"profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
"profile_image_of_user": "Profile image of {user}", "profile_image_of_user": "Profile image of {user}",
@@ -1640,7 +1645,6 @@
"restore_user": "Restore user", "restore_user": "Restore user",
"restored_asset": "Restored asset", "restored_asset": "Restored asset",
"resume": "Resume", "resume": "Resume",
"resume_paused_jobs": "Resume {count, plural, one {# paused job} other {# paused jobs}}",
"retry_upload": "Retry upload", "retry_upload": "Retry upload",
"review_duplicates": "Review duplicates", "review_duplicates": "Review duplicates",
"review_large_files": "Review large files", "review_large_files": "Review large files",
@@ -1845,8 +1849,10 @@
"shift_to_permanent_delete": "press ⇧ to permanently delete asset", "shift_to_permanent_delete": "press ⇧ to permanently delete asset",
"show_album_options": "Show album options", "show_album_options": "Show album options",
"show_albums": "Show albums", "show_albums": "Show albums",
"show_all_assets": "Show all assets",
"show_all_people": "Show all people", "show_all_people": "Show all people",
"show_and_hide_people": "Show & hide people", "show_and_hide_people": "Show & hide people",
"show_assets_without_location": "Show assets without location",
"show_file_location": "Show file location", "show_file_location": "Show file location",
"show_gallery": "Show gallery", "show_gallery": "Show gallery",
"show_hidden_people": "Show hidden people", "show_hidden_people": "Show hidden people",
@@ -1915,10 +1921,9 @@
"sync": "Sync", "sync": "Sync",
"sync_albums": "Sync albums", "sync_albums": "Sync albums",
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
"sync_cloud_ids": "Sync Cloud IDs",
"sync_local": "Sync Local", "sync_local": "Sync Local",
"sync_remote": "Sync Remote", "sync_remote": "Sync Remote",
"sync_status": "Sync Status",
"sync_status_subtitle": "View and manage the sync system",
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
"tag": "Tag", "tag": "Tag",
"tag_assets": "Tag assets", "tag_assets": "Tag assets",
@@ -1978,7 +1983,6 @@
"trash_page_select_assets_btn": "Select assets", "trash_page_select_assets_btn": "Select assets",
"trash_page_title": "Trash ({count})", "trash_page_title": "Trash ({count})",
"trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.", "trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
"troubleshoot": "Troubleshoot",
"type": "Type", "type": "Type",
"unable_to_change_pin_code": "Unable to change PIN code", "unable_to_change_pin_code": "Unable to change PIN code",
"unable_to_setup_pin_code": "Unable to setup PIN code", "unable_to_setup_pin_code": "Unable to setup PIN code",
@@ -2034,6 +2038,7 @@
"use_biometric": "Use biometric", "use_biometric": "Use biometric",
"use_current_connection": "use current connection", "use_current_connection": "use current connection",
"use_custom_date_range": "Use custom date range instead", "use_custom_date_range": "Use custom date range instead",
"use_this_location": "Click to use location",
"user": "User", "user": "User",
"user_has_been_deleted": "This user has been deleted.", "user_has_been_deleted": "This user has been deleted.",
"user_id": "User ID", "user_id": "User ID",

View File

@@ -1,6 +1,6 @@
ARG DEVICE=cpu ARG DEVICE=cpu
FROM python:3.11-bookworm@sha256:fc1f2e357c307c4044133952b203e66a47e7726821a664f603a180a0c5823844 AS builder-cpu FROM python:3.11-bookworm@sha256:c642d5dfaf9115a12086785f23008558ae2e13bcd0c4794536340bcb777a4381 AS builder-cpu
FROM builder-cpu AS builder-openvino FROM builder-cpu AS builder-openvino
@@ -68,11 +68,11 @@ RUN if [ "$DEVICE" = "rocm" ]; then \
uv pip install /opt/onnxruntime_rocm-*.whl; \ uv pip install /opt/onnxruntime_rocm-*.whl; \
fi fi
FROM python:3.11-slim-bookworm@sha256:873f91540d53b36327ed4fb018c9669107a4e2a676719720edb4209c4b15d029 AS prod-cpu FROM python:3.11-slim-bookworm@sha256:838ff46ae6c481e85e369706fa3dea5166953824124735639f3c9f52af85f319 AS prod-cpu
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2
FROM python:3.11-slim-bookworm@sha256:873f91540d53b36327ed4fb018c9669107a4e2a676719720edb4209c4b15d029 AS prod-openvino FROM python:3.11-slim-bookworm@sha256:838ff46ae6c481e85e369706fa3dea5166953824124735639f3c9f52af85f319 AS prod-openvino
RUN apt-get update && \ RUN apt-get update && \
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \

View File

@@ -1341,7 +1341,7 @@ wheels = [
[[package]] [[package]]
name = "locust" name = "locust"
version = "2.40.2" version = "2.39.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "configargparse" }, { name = "configargparse" },
@@ -1353,7 +1353,6 @@ dependencies = [
{ name = "locust-cloud" }, { name = "locust-cloud" },
{ name = "msgpack" }, { name = "msgpack" },
{ name = "psutil" }, { name = "psutil" },
{ name = "pytest" },
{ name = "python-engineio" }, { name = "python-engineio" },
{ name = "python-socketio", extra = ["client"] }, { name = "python-socketio", extra = ["client"] },
{ name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "pywin32", marker = "sys_platform == 'win32'" },
@@ -1361,12 +1360,12 @@ dependencies = [
{ name = "requests" }, { name = "requests" },
{ name = "setuptools" }, { name = "setuptools" },
{ name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions", marker = "python_full_version < '3.12'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" },
{ name = "werkzeug" }, { name = "werkzeug" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/27/e0/a99401e233ad1b9ad26265ad8f45f2466abb6ef954e7747e8484864eb6df/locust-2.40.2.tar.gz", hash = "sha256:9ffdf900d1ad949d4c5809e2a4e526bba582175f025f24da2755f43f4b5cb23e", size = 1411854, upload-time = "2025-09-08T12:55:28.664Z" } sdist = { url = "https://files.pythonhosted.org/packages/95/c8/10aa5445c404eed389b56877e6714c1787190cc09dd70059ce3765979ec5/locust-2.39.1.tar.gz", hash = "sha256:6bdd19e27edf9a1c84391d6cf6e9a737dfb832be7dfbf39053191ae31b9cc498", size = 1409902, upload-time = "2025-08-29T17:41:01.544Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f5/e7/85ddb125d91b3a2bfa2a52eeae2d4c7da062239aaa475d6aebddb5688f41/locust-2.40.2-py3-none-any.whl", hash = "sha256:c8f0060d2bd8479034e9e61e6473669c4c8216930d99ee61ec0e627340b89d3e", size = 1430483, upload-time = "2025-09-08T12:55:25.659Z" }, { url = "https://files.pythonhosted.org/packages/ec/b3/b2f4b2ca88b1e72eba7be2b2982533b887f8b709d222db78eb9602aa5121/locust-2.39.1-py3-none-any.whl", hash = "sha256:fd5148f2f1a4ed34aee968abc4393674e69d1b5e1b54db50a397f6eb09ce0b04", size = 1428155, upload-time = "2025-08-29T17:41:00.245Z" },
] ]
[[package]] [[package]]
@@ -2205,7 +2204,7 @@ wheels = [
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.4.2" version = "8.4.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
@@ -2216,9 +2215,9 @@ dependencies = [
{ name = "pygments" }, { name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tomli", marker = "python_full_version < '3.11'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
] ]
[[package]] [[package]]
@@ -2250,14 +2249,14 @@ wheels = [
[[package]] [[package]]
name = "pytest-mock" name = "pytest-mock"
version = "3.15.0" version = "3.14.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pytest" }, { name = "pytest" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/61/99/3323ee5c16b3637b4d941c362182d3e749c11e400bea31018c42219f3a98/pytest_mock-3.15.0.tar.gz", hash = "sha256:ab896bd190316b9d5d87b277569dfcdf718b2d049a2ccff5f7aca279c002a1cf", size = 33838, upload-time = "2025-09-04T20:57:48.679Z" } sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/b3/7fefc43fb706380144bcd293cc6e446e6f637ddfa8b83f48d1734156b529/pytest_mock-3.15.0-py3-none-any.whl", hash = "sha256:ef2219485fb1bd256b00e7ad7466ce26729b30eadfc7cbcdb4fa9a92ca68db6f", size = 10050, upload-time = "2025-09-04T20:57:47.274Z" }, { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" },
] ]
[[package]] [[package]]
@@ -2533,28 +2532,28 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.13.0" version = "0.12.11"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6e/1a/1f4b722862840295bcaba8c9e5261572347509548faaa99b2d57ee7bfe6a/ruff-0.13.0.tar.gz", hash = "sha256:5b4b1ee7eb35afae128ab94459b13b2baaed282b1fb0f472a73c82c996c8ae60", size = 5372863, upload-time = "2025-09-10T16:25:37.917Z" } sdist = { url = "https://files.pythonhosted.org/packages/de/55/16ab6a7d88d93001e1ae4c34cbdcfb376652d761799459ff27c1dc20f6fa/ruff-0.12.11.tar.gz", hash = "sha256:c6b09ae8426a65bbee5425b9d0b82796dbb07cb1af045743c79bfb163001165d", size = 5347103, upload-time = "2025-08-28T13:59:08.87Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ac/fe/6f87b419dbe166fd30a991390221f14c5b68946f389ea07913e1719741e0/ruff-0.13.0-py3-none-linux_armv6l.whl", hash = "sha256:137f3d65d58ee828ae136a12d1dc33d992773d8f7644bc6b82714570f31b2004", size = 12187826, upload-time = "2025-09-10T16:24:39.5Z" }, { url = "https://files.pythonhosted.org/packages/d6/a2/3b3573e474de39a7a475f3fbaf36a25600bfeb238e1a90392799163b64a0/ruff-0.12.11-py3-none-linux_armv6l.whl", hash = "sha256:93fce71e1cac3a8bf9200e63a38ac5c078f3b6baebffb74ba5274fb2ab276065", size = 11979885, upload-time = "2025-08-28T13:58:26.654Z" },
{ url = "https://files.pythonhosted.org/packages/e4/25/c92296b1fc36d2499e12b74a3fdb230f77af7bdf048fad7b0a62e94ed56a/ruff-0.13.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:21ae48151b66e71fd111b7d79f9ad358814ed58c339631450c66a4be33cc28b9", size = 12933428, upload-time = "2025-09-10T16:24:43.866Z" }, { url = "https://files.pythonhosted.org/packages/76/e4/235ad6d1785a2012d3ded2350fd9bc5c5af8c6f56820e696b0118dfe7d24/ruff-0.12.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8e33ac7b28c772440afa80cebb972ffd823621ded90404f29e5ab6d1e2d4b93", size = 12742364, upload-time = "2025-08-28T13:58:30.256Z" },
{ url = "https://files.pythonhosted.org/packages/44/cf/40bc7221a949470307d9c35b4ef5810c294e6cfa3caafb57d882731a9f42/ruff-0.13.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:64de45f4ca5441209e41742d527944635a05a6e7c05798904f39c85bafa819e3", size = 12095543, upload-time = "2025-09-10T16:24:46.638Z" }, { url = "https://files.pythonhosted.org/packages/2c/0d/15b72c5fe6b1e402a543aa9d8960e0a7e19dfb079f5b0b424db48b7febab/ruff-0.12.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d69fb9d4937aa19adb2e9f058bc4fbfe986c2040acb1a4a9747734834eaa0bfd", size = 11920111, upload-time = "2025-08-28T13:58:33.677Z" },
{ url = "https://files.pythonhosted.org/packages/f1/03/8b5ff2a211efb68c63a1d03d157e924997ada87d01bebffbd13a0f3fcdeb/ruff-0.13.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b2c653ae9b9d46e0ef62fc6fbf5b979bda20a0b1d2b22f8f7eb0cde9f4963b8", size = 12312489, upload-time = "2025-09-10T16:24:49.556Z" }, { url = "https://files.pythonhosted.org/packages/3e/c0/f66339d7893798ad3e17fa5a1e587d6fd9806f7c1c062b63f8b09dda6702/ruff-0.12.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:411954eca8464595077a93e580e2918d0a01a19317af0a72132283e28ae21bee", size = 12160060, upload-time = "2025-08-28T13:58:35.74Z" },
{ url = "https://files.pythonhosted.org/packages/37/fc/2336ef6d5e9c8d8ea8305c5f91e767d795cd4fc171a6d97ef38a5302dadc/ruff-0.13.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cec632534332062bc9eb5884a267b689085a1afea9801bf94e3ba7498a2d207", size = 11991631, upload-time = "2025-09-10T16:24:53.439Z" }, { url = "https://files.pythonhosted.org/packages/03/69/9870368326db26f20c946205fb2d0008988aea552dbaec35fbacbb46efaa/ruff-0.12.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a2c0a2e1a450f387bf2c6237c727dd22191ae8c00e448e0672d624b2bbd7fb0", size = 11799848, upload-time = "2025-08-28T13:58:38.051Z" },
{ url = "https://files.pythonhosted.org/packages/39/7f/f6d574d100fca83d32637d7f5541bea2f5e473c40020bbc7fc4a4d5b7294/ruff-0.13.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd628101d9f7d122e120ac7c17e0a0f468b19bc925501dbe03c1cb7f5415b24", size = 13720602, upload-time = "2025-09-10T16:24:56.392Z" }, { url = "https://files.pythonhosted.org/packages/25/8c/dd2c7f990e9b3a8a55eee09d4e675027d31727ce33cdb29eab32d025bdc9/ruff-0.12.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ca4c3a7f937725fd2413c0e884b5248a19369ab9bdd850b5781348ba283f644", size = 13536288, upload-time = "2025-08-28T13:58:40.046Z" },
{ url = "https://files.pythonhosted.org/packages/fd/c8/a8a5b81d8729b5d1f663348d11e2a9d65a7a9bd3c399763b1a51c72be1ce/ruff-0.13.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afe37db8e1466acb173bb2a39ca92df00570e0fd7c94c72d87b51b21bb63efea", size = 14697751, upload-time = "2025-09-10T16:24:59.89Z" }, { url = "https://files.pythonhosted.org/packages/7a/30/d5496fa09aba59b5e01ea76775a4c8897b13055884f56f1c35a4194c2297/ruff-0.12.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4d1df0098124006f6a66ecf3581a7f7e754c4df7644b2e6704cd7ca80ff95211", size = 14490633, upload-time = "2025-08-28T13:58:42.285Z" },
{ url = "https://files.pythonhosted.org/packages/57/f5/183ec292272ce7ec5e882aea74937f7288e88ecb500198b832c24debc6d3/ruff-0.13.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f96a8d90bb258d7d3358b372905fe7333aaacf6c39e2408b9f8ba181f4b6ef2", size = 14095317, upload-time = "2025-09-10T16:25:03.025Z" }, { url = "https://files.pythonhosted.org/packages/9b/2f/81f998180ad53445d403c386549d6946d0748e536d58fce5b5e173511183/ruff-0.12.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a8dd5f230efc99a24ace3b77e3555d3fbc0343aeed3fc84c8d89e75ab2ff793", size = 13888430, upload-time = "2025-08-28T13:58:44.641Z" },
{ url = "https://files.pythonhosted.org/packages/9f/8d/7f9771c971724701af7926c14dab31754e7b303d127b0d3f01116faef456/ruff-0.13.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b5e3d883e4f924c5298e3f2ee0f3085819c14f68d1e5b6715597681433f153", size = 13144418, upload-time = "2025-09-10T16:25:06.272Z" }, { url = "https://files.pythonhosted.org/packages/87/71/23a0d1d5892a377478c61dbbcffe82a3476b050f38b5162171942a029ef3/ruff-0.12.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dc75533039d0ed04cd33fb8ca9ac9620b99672fe7ff1533b6402206901c34ee", size = 12913133, upload-time = "2025-08-28T13:58:47.039Z" },
{ url = "https://files.pythonhosted.org/packages/a8/a6/7985ad1778e60922d4bef546688cd8a25822c58873e9ff30189cfe5dc4ab/ruff-0.13.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03447f3d18479df3d24917a92d768a89f873a7181a064858ea90a804a7538991", size = 13370843, upload-time = "2025-09-10T16:25:09.965Z" }, { url = "https://files.pythonhosted.org/packages/80/22/3c6cef96627f89b344c933781ed38329bfb87737aa438f15da95907cbfd5/ruff-0.12.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fc58f9266d62c6eccc75261a665f26b4ef64840887fc6cbc552ce5b29f96cc8", size = 13169082, upload-time = "2025-08-28T13:58:49.157Z" },
{ url = "https://files.pythonhosted.org/packages/64/1c/bafdd5a7a05a50cc51d9f5711da704942d8dd62df3d8c70c311e98ce9f8a/ruff-0.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fbc6b1934eb1c0033da427c805e27d164bb713f8e273a024a7e86176d7f462cf", size = 13321891, upload-time = "2025-09-10T16:25:12.969Z" }, { url = "https://files.pythonhosted.org/packages/05/b5/68b3ff96160d8b49e8dd10785ff3186be18fd650d356036a3770386e6c7f/ruff-0.12.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5a0113bd6eafd545146440225fe60b4e9489f59eb5f5f107acd715ba5f0b3d2f", size = 13139490, upload-time = "2025-08-28T13:58:51.593Z" },
{ url = "https://files.pythonhosted.org/packages/bc/3e/7817f989cb9725ef7e8d2cee74186bf90555279e119de50c750c4b7a72fe/ruff-0.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8ab6a3e03665d39d4a25ee199d207a488724f022db0e1fe4002968abdb8001b", size = 12119119, upload-time = "2025-09-10T16:25:16.621Z" }, { url = "https://files.pythonhosted.org/packages/59/b9/050a3278ecd558f74f7ee016fbdf10591d50119df8d5f5da45a22c6afafc/ruff-0.12.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0d737b4059d66295c3ea5720e6efc152623bb83fde5444209b69cd33a53e2000", size = 11958928, upload-time = "2025-08-28T13:58:53.943Z" },
{ url = "https://files.pythonhosted.org/packages/58/07/9df080742e8d1080e60c426dce6e96a8faf9a371e2ce22eef662e3839c95/ruff-0.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2a5c62f8ccc6dd2fe259917482de7275cecc86141ee10432727c4816235bc41", size = 11961594, upload-time = "2025-09-10T16:25:19.49Z" }, { url = "https://files.pythonhosted.org/packages/f9/bc/93be37347db854806904a43b0493af8d6873472dfb4b4b8cbb27786eb651/ruff-0.12.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:916fc5defee32dbc1fc1650b576a8fed68f5e8256e2180d4d9855aea43d6aab2", size = 11764513, upload-time = "2025-08-28T13:58:55.976Z" },
{ url = "https://files.pythonhosted.org/packages/6a/f4/ae1185349197d26a2316840cb4d6c3fba61d4ac36ed728bf0228b222d71f/ruff-0.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b7b85ca27aeeb1ab421bc787009831cffe6048faae08ad80867edab9f2760945", size = 12933377, upload-time = "2025-09-10T16:25:22.371Z" }, { url = "https://files.pythonhosted.org/packages/7a/a1/1471751e2015a81fd8e166cd311456c11df74c7e8769d4aabfbc7584c7ac/ruff-0.12.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c984f07d7adb42d3ded5be894fb4007f30f82c87559438b4879fe7aa08c62b39", size = 12745154, upload-time = "2025-08-28T13:58:58.16Z" },
{ url = "https://files.pythonhosted.org/packages/b6/39/e776c10a3b349fc8209a905bfb327831d7516f6058339a613a8d2aaecacd/ruff-0.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:79ea0c44a3032af768cabfd9616e44c24303af49d633b43e3a5096e009ebe823", size = 13418555, upload-time = "2025-09-10T16:25:25.681Z" }, { url = "https://files.pythonhosted.org/packages/68/ab/2542b14890d0f4872dd81b7b2a6aed3ac1786fae1ce9b17e11e6df9e31e3/ruff-0.12.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e07fbb89f2e9249f219d88331c833860489b49cdf4b032b8e4432e9b13e8a4b9", size = 13227653, upload-time = "2025-08-28T13:59:00.276Z" },
{ url = "https://files.pythonhosted.org/packages/46/09/dca8df3d48e8b3f4202bf20b1658898e74b6442ac835bfe2c1816d926697/ruff-0.13.0-py3-none-win32.whl", hash = "sha256:4e473e8f0e6a04e4113f2e1de12a5039579892329ecc49958424e5568ef4f768", size = 12141613, upload-time = "2025-09-10T16:25:28.664Z" }, { url = "https://files.pythonhosted.org/packages/22/16/2fbfc61047dbfd009c58a28369a693a1484ad15441723be1cd7fe69bb679/ruff-0.12.11-py3-none-win32.whl", hash = "sha256:c792e8f597c9c756e9bcd4d87cf407a00b60af77078c96f7b6366ea2ce9ba9d3", size = 11944270, upload-time = "2025-08-28T13:59:02.347Z" },
{ url = "https://files.pythonhosted.org/packages/61/21/0647eb71ed99b888ad50e44d8ec65d7148babc0e242d531a499a0bbcda5f/ruff-0.13.0-py3-none-win_amd64.whl", hash = "sha256:48e5c25c7a3713eea9ce755995767f4dcd1b0b9599b638b12946e892123d1efb", size = 13258250, upload-time = "2025-09-10T16:25:31.773Z" }, { url = "https://files.pythonhosted.org/packages/08/a5/34276984705bfe069cd383101c45077ee029c3fe3b28225bf67aa35f0647/ruff-0.12.11-py3-none-win_amd64.whl", hash = "sha256:a3283325960307915b6deb3576b96919ee89432ebd9c48771ca12ee8afe4a0fd", size = 13046600, upload-time = "2025-08-28T13:59:04.751Z" },
{ url = "https://files.pythonhosted.org/packages/e1/a3/03216a6a86c706df54422612981fb0f9041dbb452c3401501d4a22b942c9/ruff-0.13.0-py3-none-win_arm64.whl", hash = "sha256:ab80525317b1e1d38614addec8ac954f1b3e662de9d59114ecbf771d00cf613e", size = 12312357, upload-time = "2025-09-10T16:25:35.595Z" }, { url = "https://files.pythonhosted.org/packages/84/a8/001d4a7c2b37623a3fd7463208267fb906df40ff31db496157549cfd6e72/ruff-0.12.11-py3-none-win_arm64.whl", hash = "sha256:bae4d6e6a2676f8fb0f98b74594a048bae1b944aab17e9f5d504062303c6dbea", size = 12135290, upload-time = "2025-08-28T13:59:06.933Z" },
] ]
[[package]] [[package]]
@@ -2877,27 +2876,27 @@ wheels = [
[[package]] [[package]]
name = "tokenizers" name = "tokenizers"
version = "0.22.0" version = "0.21.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "huggingface-hub" }, { name = "huggingface-hub" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/5e/b4/c1ce3699e81977da2ace8b16d2badfd42b060e7d33d75c4ccdbf9dc920fa/tokenizers-0.22.0.tar.gz", hash = "sha256:2e33b98525be8453f355927f3cab312c36cd3e44f4d7e9e97da2fa94d0a49dcb", size = 362771, upload-time = "2025-08-29T10:25:33.914Z" } sdist = { url = "https://files.pythonhosted.org/packages/c2/2f/402986d0823f8d7ca139d969af2917fefaa9b947d1fb32f6168c509f2492/tokenizers-0.21.4.tar.gz", hash = "sha256:fa23f85fbc9a02ec5c6978da172cdcbac23498c3ca9f3645c5c68740ac007880", size = 351253, upload-time = "2025-07-28T15:48:54.325Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/b1/18c13648edabbe66baa85fe266a478a7931ddc0cd1ba618802eb7b8d9865/tokenizers-0.22.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:eaa9620122a3fb99b943f864af95ed14c8dfc0f47afa3b404ac8c16b3f2bb484", size = 3081954, upload-time = "2025-08-29T10:25:24.993Z" }, { url = "https://files.pythonhosted.org/packages/98/c6/fdb6f72bf6454f52eb4a2510be7fb0f614e541a2554d6210e370d85efff4/tokenizers-0.21.4-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ccc10a7c3bcefe0f242867dc914fc1226ee44321eb618cfe3019b5df3400133", size = 2863987, upload-time = "2025-07-28T15:48:44.877Z" },
{ url = "https://files.pythonhosted.org/packages/c2/02/c3c454b641bd7c4f79e4464accfae9e7dfc913a777d2e561e168ae060362/tokenizers-0.22.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:71784b9ab5bf0ff3075bceeb198149d2c5e068549c0d18fe32d06ba0deb63f79", size = 2945644, upload-time = "2025-08-29T10:25:23.405Z" }, { url = "https://files.pythonhosted.org/packages/8d/a6/28975479e35ddc751dc1ddc97b9b69bf7fcf074db31548aab37f8116674c/tokenizers-0.21.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5e2f601a8e0cd5be5cc7506b20a79112370b9b3e9cb5f13f68ab11acd6ca7d60", size = 2732457, upload-time = "2025-07-28T15:48:43.265Z" },
{ url = "https://files.pythonhosted.org/packages/55/02/d10185ba2fd8c2d111e124c9d92de398aee0264b35ce433f79fb8472f5d0/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec5b71f668a8076802b0241a42387d48289f25435b86b769ae1837cad4172a17", size = 3254764, upload-time = "2025-08-29T10:25:12.445Z" }, { url = "https://files.pythonhosted.org/packages/aa/8f/24f39d7b5c726b7b0be95dca04f344df278a3fe3a4deb15a975d194cbb32/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b376f5a1aee67b4d29032ee85511bbd1b99007ec735f7f35c8a2eb104eade5", size = 3012624, upload-time = "2025-07-28T13:22:43.895Z" },
{ url = "https://files.pythonhosted.org/packages/13/89/17514bd7ef4bf5bfff58e2b131cec0f8d5cea2b1c8ffe1050a2c8de88dbb/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ea8562fa7498850d02a16178105b58803ea825b50dc9094d60549a7ed63654bb", size = 3161654, upload-time = "2025-08-29T10:25:15.493Z" }, { url = "https://files.pythonhosted.org/packages/58/47/26358925717687a58cb74d7a508de96649544fad5778f0cd9827398dc499/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2107ad649e2cda4488d41dfd031469e9da3fcbfd6183e74e4958fa729ffbf9c6", size = 2939681, upload-time = "2025-07-28T13:22:47.499Z" },
{ url = "https://files.pythonhosted.org/packages/5a/d8/bac9f3a7ef6dcceec206e3857c3b61bb16c6b702ed7ae49585f5bd85c0ef/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4136e1558a9ef2e2f1de1555dcd573e1cbc4a320c1a06c4107a3d46dc8ac6e4b", size = 3511484, upload-time = "2025-08-29T10:25:20.477Z" }, { url = "https://files.pythonhosted.org/packages/99/6f/cc300fea5db2ab5ddc2c8aea5757a27b89c84469899710c3aeddc1d39801/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c73012da95afafdf235ba80047699df4384fdc481527448a078ffd00e45a7d9", size = 3247445, upload-time = "2025-07-28T15:48:39.711Z" },
{ url = "https://files.pythonhosted.org/packages/aa/27/9c9800eb6763683010a4851db4d1802d8cab9cec114c17056eccb4d4a6e0/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf5954de3962a5fd9781dc12048d24a1a6f1f5df038c6e95db328cd22964206", size = 3712829, upload-time = "2025-08-29T10:25:17.154Z" }, { url = "https://files.pythonhosted.org/packages/be/bf/98cb4b9c3c4afd8be89cfa6423704337dc20b73eb4180397a6e0d456c334/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f23186c40395fc390d27f519679a58023f368a0aad234af145e0f39ad1212732", size = 3428014, upload-time = "2025-07-28T13:22:49.569Z" },
{ url = "https://files.pythonhosted.org/packages/10/e3/b1726dbc1f03f757260fa21752e1921445b5bc350389a8314dd3338836db/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8337ca75d0731fc4860e6204cc24bb36a67d9736142aa06ed320943b50b1e7ed", size = 3408934, upload-time = "2025-08-29T10:25:18.76Z" }, { url = "https://files.pythonhosted.org/packages/75/c7/96c1cc780e6ca7f01a57c13235dd05b7bc1c0f3588512ebe9d1331b5f5ae/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc88bb34e23a54cc42713d6d98af5f1bf79c07653d24fe984d2d695ba2c922a2", size = 3193197, upload-time = "2025-07-28T13:22:51.471Z" },
{ url = "https://files.pythonhosted.org/packages/d4/61/aeab3402c26874b74bb67a7f2c4b569dde29b51032c5384db592e7b216f4/tokenizers-0.22.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a89264e26f63c449d8cded9061adea7b5de53ba2346fc7e87311f7e4117c1cc8", size = 3345585, upload-time = "2025-08-29T10:25:22.08Z" }, { url = "https://files.pythonhosted.org/packages/f2/90/273b6c7ec78af547694eddeea9e05de771278bd20476525ab930cecaf7d8/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51b7eabb104f46c1c50b486520555715457ae833d5aee9ff6ae853d1130506ff", size = 3115426, upload-time = "2025-07-28T15:48:41.439Z" },
{ url = "https://files.pythonhosted.org/packages/bc/d3/498b4a8a8764cce0900af1add0f176ff24f475d4413d55b760b8cdf00893/tokenizers-0.22.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:790bad50a1b59d4c21592f9c3cf5e5cf9c3c7ce7e1a23a739f13e01fb1be377a", size = 9322986, upload-time = "2025-08-29T10:25:26.607Z" }, { url = "https://files.pythonhosted.org/packages/91/43/c640d5a07e95f1cf9d2c92501f20a25f179ac53a4f71e1489a3dcfcc67ee/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:714b05b2e1af1288bd1bc56ce496c4cebb64a20d158ee802887757791191e6e2", size = 9089127, upload-time = "2025-07-28T15:48:46.472Z" },
{ url = "https://files.pythonhosted.org/packages/a2/62/92378eb1c2c565837ca3cb5f9569860d132ab9d195d7950c1ea2681dffd0/tokenizers-0.22.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:76cf6757c73a10ef10bf06fa937c0ec7393d90432f543f49adc8cab3fb6f26cb", size = 9276630, upload-time = "2025-08-29T10:25:28.349Z" }, { url = "https://files.pythonhosted.org/packages/44/a1/dd23edd6271d4dca788e5200a807b49ec3e6987815cd9d0a07ad9c96c7c2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1340ff877ceedfa937544b7d79f5b7becf33a4cfb58f89b3b49927004ef66f78", size = 9055243, upload-time = "2025-07-28T15:48:48.539Z" },
{ url = "https://files.pythonhosted.org/packages/eb/f0/342d80457aa1cda7654327460f69db0d69405af1e4c453f4dc6ca7c4a76e/tokenizers-0.22.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1626cb186e143720c62c6c6b5371e62bbc10af60481388c0da89bc903f37ea0c", size = 9547175, upload-time = "2025-08-29T10:25:29.989Z" }, { url = "https://files.pythonhosted.org/packages/21/2b/b410d6e9021c4b7ddb57248304dc817c4d4970b73b6ee343674914701197/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3c1f4317576e465ac9ef0d165b247825a2a4078bcd01cba6b54b867bdf9fdd8b", size = 9298237, upload-time = "2025-07-28T15:48:50.443Z" },
{ url = "https://files.pythonhosted.org/packages/14/84/8aa9b4adfc4fbd09381e20a5bc6aa27040c9c09caa89988c01544e008d18/tokenizers-0.22.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:da589a61cbfea18ae267723d6b029b84598dc8ca78db9951d8f5beff72d8507c", size = 9692735, upload-time = "2025-08-29T10:25:32.089Z" }, { url = "https://files.pythonhosted.org/packages/b7/0a/42348c995c67e2e6e5c89ffb9cfd68507cbaeb84ff39c49ee6e0a6dd0fd2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c212aa4e45ec0bb5274b16b6f31dd3f1c41944025c2358faaa5782c754e84c24", size = 9461980, upload-time = "2025-07-28T15:48:52.325Z" },
{ url = "https://files.pythonhosted.org/packages/bf/24/83ee2b1dc76bfe05c3142e7d0ccdfe69f0ad2f1ebf6c726cea7f0874c0d0/tokenizers-0.22.0-cp39-abi3-win32.whl", hash = "sha256:dbf9d6851bddae3e046fedfb166f47743c1c7bd11c640f0691dd35ef0bcad3be", size = 2471915, upload-time = "2025-08-29T10:25:36.411Z" }, { url = "https://files.pythonhosted.org/packages/3d/d3/dacccd834404cd71b5c334882f3ba40331ad2120e69ded32cf5fda9a7436/tokenizers-0.21.4-cp39-abi3-win32.whl", hash = "sha256:6c42a930bc5f4c47f4ea775c91de47d27910881902b0f20e4990ebe045a415d0", size = 2329871, upload-time = "2025-07-28T15:48:56.841Z" },
{ url = "https://files.pythonhosted.org/packages/d1/9b/0e0bf82214ee20231845b127aa4a8015936ad5a46779f30865d10e404167/tokenizers-0.22.0-cp39-abi3-win_amd64.whl", hash = "sha256:c78174859eeaee96021f248a56c801e36bfb6bd5b067f2e95aa82445ca324f00", size = 2680494, upload-time = "2025-08-29T10:25:35.14Z" }, { url = "https://files.pythonhosted.org/packages/41/f2/fd673d979185f5dcbac4be7d09461cbb99751554ffb6718d0013af8604cb/tokenizers-0.21.4-cp39-abi3-win_amd64.whl", hash = "sha256:475d807a5c3eb72c59ad9b5fcdb254f6e17f53dfcbb9903233b0dfa9c943b597", size = 2507568, upload-time = "2025-07-28T15:48:55.456Z" },
] ]
[[package]] [[package]]

View File

@@ -260,6 +260,7 @@ interface NativeSyncApi {
fun getAssetsCountSince(albumId: String, timestamp: Long): Long fun getAssetsCountSince(albumId: String, timestamp: Long): Long
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset>
fun hashPaths(paths: List<String>): List<ByteArray?> fun hashPaths(paths: List<String>): List<ByteArray?>
fun getCloudIdForAssetIds(assetIds: List<String>): Map<String, String?>
companion object { companion object {
/** The codec used by NativeSyncApi. */ /** The codec used by NativeSyncApi. */
@@ -418,6 +419,23 @@ interface NativeSyncApi {
channel.setMessageHandler(null) channel.setMessageHandler(null)
} }
} }
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val assetIdsArg = args[0] as List<String>
val wrapped: List<Any?> = try {
listOf(api.getCloudIdForAssetIds(assetIdsArg))
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
} }
} }
} }

View File

@@ -234,4 +234,10 @@ open class NativeSyncApiImplBase(context: Context) {
} }
} }
} }
// This method is only implemented on iOS; on Android, we do not have a concept of cloud IDs
@Suppress("unused", "UNUSED_PARAMETER")
fun getCloudIdForAssetIds(assetIds: List<String>): Map<String, String?> {
return emptyMap()
}
} }

File diff suppressed because one or more lines are too long

View File

@@ -324,6 +324,7 @@ protocol NativeSyncApi {
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset]
func hashPaths(paths: [String]) throws -> [FlutterStandardTypedData?] func hashPaths(paths: [String]) throws -> [FlutterStandardTypedData?]
func getCloudIdForAssetIds(assetIds: [String]) throws -> [String: String?]
} }
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -476,5 +477,20 @@ class NativeSyncApiSetup {
} else { } else {
hashPathsChannel.setMessageHandler(nil) hashPathsChannel.setMessageHandler(nil)
} }
let getCloudIdForAssetIdsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
getCloudIdForAssetIdsChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let assetIdsArg = args[0] as! [String]
do {
let result = try api.getCloudIdForAssetIds(assetIds: assetIdsArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getCloudIdForAssetIdsChannel.setMessageHandler(nil)
}
} }
} }

View File

@@ -286,4 +286,20 @@ class NativeSyncApiImpl: NativeSyncApi {
return FlutterStandardTypedData(bytes: Data(digest)) return FlutterStandardTypedData(bytes: Data(digest))
} }
} }
func getCloudIdForAssetIds(assetIds: [String]) throws -> [String : String?] {
guard #available(iOS 16, *) else {
return Dictionary(
uniqueKeysWithValues: assetIds.map { ($0, nil as String?) }
)
}
var mappings: [String: String?] = [:]
let result = PHPhotoLibrary.shared().cloudIdentifierMappings(forLocalIdentifiers: assetIds)
for (key, value) in result {
let id = try? value.get().stringValue
mappings[key] = id
}
return mappings;
}
} }

View File

@@ -0,0 +1,40 @@
enum RemoteAssetMetadataKey {
mobileApp("mobile-app");
final String key;
const RemoteAssetMetadataKey(this.key);
}
abstract class RemoteAssetMetadataValue {
const RemoteAssetMetadataValue();
Map<String, dynamic> toJson();
}
class RemoteAssetMetadataItem {
final RemoteAssetMetadataKey key;
final RemoteAssetMetadataValue value;
const RemoteAssetMetadataItem({required this.key, required this.value});
Map<String, Object?> toJson() {
return {'key': key.key, 'value': value};
}
}
class RemoteAssetMobileAppMetadata extends RemoteAssetMetadataValue {
final String? cloudId;
const RemoteAssetMobileAppMetadata({this.cloudId});
@override
Map<String, dynamic> toJson() {
final map = <String, Object?>{};
if (cloudId != null) {
map["iCloudId"] = cloudId;
}
return map;
}
}

View File

@@ -3,11 +3,13 @@ part of 'base_asset.model.dart';
class LocalAsset extends BaseAsset { class LocalAsset extends BaseAsset {
final String id; final String id;
final String? remoteId; final String? remoteId;
final String? cloudId;
final int orientation; final int orientation;
const LocalAsset({ const LocalAsset({
required this.id, required this.id,
this.remoteId, this.remoteId,
this.cloudId,
required super.name, required super.name,
super.checksum, super.checksum,
required super.type, required super.type,
@@ -31,6 +33,8 @@ class LocalAsset extends BaseAsset {
String toString() { String toString() {
return '''LocalAsset { return '''LocalAsset {
id: $id, id: $id,
remoteId: ${remoteId ?? "<NA>"},
cloudId: ${cloudId ?? "<NA>"},
name: $name, name: $name,
type: $type, type: $type,
createdAt: $createdAt, createdAt: $createdAt,
@@ -49,7 +53,7 @@ class LocalAsset extends BaseAsset {
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! LocalAsset) return false; if (other is! LocalAsset) return false;
if (identical(this, other)) return true; if (identical(this, other)) return true;
return super == other && id == other.id && orientation == other.orientation; return super == other && id == other.id && orientation == other.orientation && cloudId == other.cloudId;
} }
@override @override
@@ -58,6 +62,7 @@ class LocalAsset extends BaseAsset {
LocalAsset copyWith({ LocalAsset copyWith({
String? id, String? id,
String? remoteId, String? remoteId,
String? cloudId,
String? name, String? name,
String? checksum, String? checksum,
AssetType? type, AssetType? type,
@@ -72,6 +77,7 @@ class LocalAsset extends BaseAsset {
return LocalAsset( return LocalAsset(
id: id ?? this.id, id: id ?? this.id,
remoteId: remoteId ?? this.remoteId, remoteId: remoteId ?? this.remoteId,
cloudId: cloudId ?? this.cloudId,
name: name ?? this.name, name: name ?? this.name,
checksum: checksum ?? this.checksum, checksum: checksum ?? this.checksum,
type: type ?? this.type, type: type ?? this.type,

View File

@@ -76,8 +76,7 @@ enum StoreKey<T> {
betaTimeline<bool>._(1002), betaTimeline<bool>._(1002),
enableBackup<bool>._(1003), enableBackup<bool>._(1003),
useWifiForUploadVideos<bool>._(1004), useWifiForUploadVideos<bool>._(1004),
useWifiForUploadPhotos<bool>._(1005), useWifiForUploadPhotos<bool>._(1005);
needBetaMigration<bool>._(1006);
const StoreKey._(this.id); const StoreKey._(this.id);
final int id; final int id;

View File

@@ -1,4 +1,3 @@
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
@@ -28,14 +27,6 @@ class AssetService {
return asset is LocalAsset ? _localAssetRepository.watch(id) : _remoteAssetRepository.watch(id); return asset is LocalAsset ? _localAssetRepository.watch(id) : _remoteAssetRepository.watch(id);
} }
Future<List<LocalAsset?>> getLocalAssetsByChecksum(String checksum) {
return _localAssetRepository.getByChecksum(checksum);
}
Future<RemoteAsset?> getRemoteAssetByChecksum(String checksum) {
return _remoteAssetRepository.getByChecksum(checksum);
}
Future<RemoteAsset?> getRemoteAsset(String id) { Future<RemoteAsset?> getRemoteAsset(String id) {
return _remoteAssetRepository.get(id); return _remoteAssetRepository.get(id);
} }
@@ -98,8 +89,4 @@ class AssetService {
Future<int> getLocalHashedCount() { Future<int> getLocalHashedCount() {
return _localAssetRepository.getHashedCount(); return _localAssetRepository.getHashedCount();
} }
Future<List<LocalAlbum>> getSourceAlbums(String localAssetId, {BackupSelection? backupSelection}) {
return _localAssetRepository.getSourceAlbums(localAssetId, backupSelection: backupSelection);
}
} }

View File

@@ -5,6 +5,7 @@ import 'package:background_downloader/background_downloader.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/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart'; import 'package:immich_mobile/platform/background_worker_api.g.dart';
@@ -23,7 +24,6 @@ import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:worker_manager/worker_manager.dart';
class BackgroundWorkerFgService { class BackgroundWorkerFgService {
final BackgroundWorkerFgHostApi _foregroundHostApi; final BackgroundWorkerFgHostApi _foregroundHostApi;
@@ -42,7 +42,8 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
final Drift _drift; final Drift _drift;
final DriftLogger _driftLogger; final DriftLogger _driftLogger;
final BackgroundWorkerBgHostApi _backgroundHostApi; final BackgroundWorkerBgHostApi _backgroundHostApi;
final Logger _logger = Logger('BackgroundWorkerBgService'); final Logger _logger = Logger('BackgroundUploadBgService');
late final IsolateLockManager _lockManager;
bool _isCleanedUp = false; bool _isCleanedUp = false;
@@ -58,6 +59,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
driftProvider.overrideWith(driftOverride(drift)), driftProvider.overrideWith(driftOverride(drift)),
], ],
); );
_lockManager = IsolateLockManager(onCloseRequest: _cleanup);
BackgroundWorkerFlutterApi.setUp(this); BackgroundWorkerFlutterApi.setUp(this);
} }
@@ -65,30 +67,41 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
Future<void> init() async { Future<void> init() async {
try { try {
await loadTranslations();
HttpSSLOptions.apply(applyNative: false); HttpSSLOptions.apply(applyNative: false);
await _ref.read(authServiceProvider).setOpenApiServiceEndpoint();
await Future.wait([ // Initialize the file downloader
loadTranslations(), await FileDownloader().configure(
workerManager.init(dynamicSpawning: true), globalConfig: [
_ref.read(authServiceProvider).setOpenApiServiceEndpoint(), // maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
// Initialize the file downloader (Config.holdingQueue, (6, 6, 3)),
FileDownloader().configure( // On Android, if files are larger than 256MB, run in foreground service
globalConfig: [ (Config.runInForegroundIfFileLargerThan, 256),
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3 ],
(Config.holdingQueue, (6, 6, 3)), );
// On Android, if files are larger than 256MB, run in foreground service await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
(Config.runInForegroundIfFileLargerThan, 256), await FileDownloader().trackTasks();
],
),
FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false),
FileDownloader().trackTasks(),
_ref.read(fileMediaRepositoryProvider).enableBackgroundAccess(),
]);
configureFileDownloaderNotifications(); configureFileDownloaderNotifications();
await _ref.read(fileMediaRepositoryProvider).enableBackgroundAccess();
// Notify the host that the background worker service has been initialized and is ready to use // Notify the host that the background upload service has been initialized and is ready to use
_backgroundHostApi.onInitialized(); debugPrint("Acquiring background worker lock");
if (await _lockManager.acquireLock().timeout(
const Duration(seconds: 5),
onTimeout: () {
_lockManager.cancel();
return false;
},
)) {
_logger.info("Acquired background worker lock");
await _backgroundHostApi.onInitialized();
return;
}
_logger.warning("Failed to acquire background worker lock");
await _cleanup();
await _backgroundHostApi.close();
} catch (error, stack) { } catch (error, stack) {
_logger.severe("Failed to initialize background worker", error, stack); _logger.severe("Failed to initialize background worker", error, stack);
_backgroundHostApi.close(); _backgroundHostApi.close();
@@ -157,7 +170,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
_isCleanedUp = true; _isCleanedUp = true;
_logger.info("Cleaning up background worker"); _logger.info("Cleaning up background worker");
final cleanupFutures = [ final cleanupFutures = [
workerManager.dispose(),
_drift.close(), _drift.close(),
_driftLogger.close(), _driftLogger.close(),
_ref.read(backgroundSyncProvider).cancel(), _ref.read(backgroundSyncProvider).cancel(),
@@ -168,6 +180,8 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
cleanupFutures.add(_isar.close()); cleanupFutures.add(_isar.close());
} }
_ref.dispose(); _ref.dispose();
_lockManager.releaseLock();
await Future.wait(cleanupFutures); await Future.wait(cleanupFutures);
_logger.info("Background worker resources cleaned up"); _logger.info("Background worker resources cleaned up");
} catch (error, stack) { } catch (error, stack) {
@@ -177,37 +191,31 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
Future<void> _handleBackup({bool processBulk = true}) async { Future<void> _handleBackup({bool processBulk = true}) async {
if (!_isBackupEnabled) { if (!_isBackupEnabled) {
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
return; return;
} }
_logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service");
final currentUser = _ref.read(currentUserProvider); final currentUser = _ref.read(currentUserProvider);
if (currentUser == null) { if (currentUser == null) {
_logger.warning("[_handleBackup 3] No current user found. Skipping backup from background");
return; return;
} }
if (processBulk) { if (processBulk) {
_logger.info("[_handleBackup 4] Resume backup from background");
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id); return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
} }
final activeTask = await _ref.read(uploadServiceProvider).getActiveTasks(currentUser.id); final activeTask = await _ref.read(uploadServiceProvider).getActiveTasks(currentUser.id);
if (activeTask.isNotEmpty) { if (activeTask.isNotEmpty) {
_logger.info("[_handleBackup 5] Resuming backup for active tasks from background");
await _ref.read(uploadServiceProvider).resumeBackup(); await _ref.read(uploadServiceProvider).resumeBackup();
} else { } else {
_logger.info("[_handleBackup 6] Starting serial backup for new tasks from background");
await _ref.read(uploadServiceProvider).startBackupSerial(currentUser.id); await _ref.read(uploadServiceProvider).startBackupSerial(currentUser.id);
} }
} }
Future<void> _syncAssets({Duration? hashTimeout}) async { Future<void> _syncAssets({Duration? hashTimeout}) async {
final futures = <Future<void>>[]; await (
_ref.read(backgroundSyncProvider).syncLocal(),
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async { _ref.read(backgroundSyncProvider).syncRemote(),
).wait.whenComplete(() async {
if (_isCleanedUp) { if (_isCleanedUp) {
return; return;
} }
@@ -224,11 +232,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
return hashFuture; return hashFuture;
}); });
futures.add(localSyncFuture);
futures.add(_ref.read(backgroundSyncProvider).syncRemote());
await Future.wait(futures);
} }
} }

View File

@@ -38,6 +38,10 @@ class HashService {
Future<void> hashAssets() async { Future<void> hashAssets() async {
_log.info("Starting hashing of assets"); _log.info("Starting hashing of assets");
final Stopwatch stopwatch = Stopwatch()..start(); final Stopwatch stopwatch = Stopwatch()..start();
// Migrate hashes from cloud ID to local ID so we don't have to re-hash them
await _migrateHashes();
// Sorted by backupSelection followed by isCloud // Sorted by backupSelection followed by isCloud
final localAlbums = await _localAlbumRepository.getAll( final localAlbums = await _localAlbumRepository.getAll(
sortBy: {SortLocalAlbumsBy.backupSelection, SortLocalAlbumsBy.isIosSharedAlbum}, sortBy: {SortLocalAlbumsBy.backupSelection, SortLocalAlbumsBy.isIosSharedAlbum},
@@ -59,6 +63,15 @@ class HashService {
_log.info("Hashing took - ${stopwatch.elapsedMilliseconds}ms"); _log.info("Hashing took - ${stopwatch.elapsedMilliseconds}ms");
} }
Future<void> _migrateHashes() async {
final hashMappings = await _localAssetRepository.getHashMappingFromCloudId();
if (hashMappings.isEmpty) {
return;
}
await _localAssetRepository.updateHashes(hashMappings);
}
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB /// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
/// with hash for those that were successfully hashed. Hashes are looked up in a table /// with hash for those that were successfully hashed. Hashes are looked up in a table
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB. /// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
@@ -101,7 +114,7 @@ class HashService {
_log.fine("Hashing ${toHash.length} files"); _log.fine("Hashing ${toHash.length} files");
final hashed = <LocalAsset>[]; final hashed = <LocalAssetHashMapping>[];
final hashes = await _nativeSyncApi.hashPaths(toHash.map((e) => e.path).toList()); final hashes = await _nativeSyncApi.hashPaths(toHash.map((e) => e.path).toList());
assert( assert(
hashes.length == toHash.length, hashes.length == toHash.length,
@@ -117,7 +130,7 @@ class HashService {
final hash = hashes[i]; final hash = hashes[i];
final asset = toHash[i].asset; final asset = toHash[i].asset;
if (hash?.length == 20) { if (hash?.length == 20) {
hashed.add(asset.copyWith(checksum: base64.encode(hash!))); hashed.add((assetId: asset.id, checksum: base64.encode(hash!)));
} else { } else {
_log.warning( _log.warning(
"Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt} from album: ${album.name}", "Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt} from album: ${album.name}",

View File

@@ -44,8 +44,9 @@ class LocalSyncService {
final deviceAlbums = await _nativeSyncApi.getAlbums(); final deviceAlbums = await _nativeSyncApi.getAlbums();
await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums()); await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums());
final newAssets = delta.updates.toLocalAssets();
await _localAlbumRepository.processDelta( await _localAlbumRepository.processDelta(
updates: delta.updates.toLocalAssets(), updates: newAssets,
deletes: delta.deletes, deletes: delta.deletes,
assetAlbums: delta.assetAlbums, assetAlbums: delta.assetAlbums,
); );
@@ -73,6 +74,8 @@ class LocalSyncService {
} }
await updateAlbum(dbAlbum, album); await updateAlbum(dbAlbum, album);
} }
await _mapIosCloudIds(newAssets);
} }
await _nativeSyncApi.checkpointSync(); await _nativeSyncApi.checkpointSync();
@@ -112,9 +115,12 @@ class LocalSyncService {
try { try {
_log.fine("Adding device album ${album.name}"); _log.fine("Adding device album ${album.name}");
final assets = album.assetCount > 0 ? await _nativeSyncApi.getAssetsForAlbum(album.id) : <PlatformAsset>[]; final assets = album.assetCount > 0
? await _nativeSyncApi.getAssetsForAlbum(album.id).then((a) => a.toLocalAssets())
: <LocalAsset>[];
await _localAlbumRepository.upsert(album, toUpsert: assets.toLocalAssets()); await _localAlbumRepository.upsert(album, toUpsert: assets);
await _mapIosCloudIds(assets);
_log.fine("Successfully added device album ${album.name}"); _log.fine("Successfully added device album ${album.name}");
} catch (e, s) { } catch (e, s) {
_log.warning("Error while adding device album", e, s); _log.warning("Error while adding device album", e, s);
@@ -184,13 +190,16 @@ class LocalSyncService {
return false; return false;
} }
final newAssets = await _nativeSyncApi.getAssetsForAlbum(deviceAlbum.id, updatedTimeCond: updatedTime); final newAssets = await _nativeSyncApi
.getAssetsForAlbum(deviceAlbum.id, updatedTimeCond: updatedTime)
.then((a) => a.toLocalAssets());
await _localAlbumRepository.upsert( await _localAlbumRepository.upsert(
deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection), deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
toUpsert: newAssets.toLocalAssets(), toUpsert: newAssets,
); );
await _mapIosCloudIds(newAssets);
return true; return true;
} catch (e, s) { } catch (e, s) {
_log.warning("Error on fast syncing local album: ${dbAlbum.name}", e, s); _log.warning("Error on fast syncing local album: ${dbAlbum.name}", e, s);
@@ -222,6 +231,7 @@ class LocalSyncService {
if (dbAlbum.assetCount == 0) { if (dbAlbum.assetCount == 0) {
_log.fine("Device album ${deviceAlbum.name} is empty. Adding assets to DB."); _log.fine("Device album ${deviceAlbum.name} is empty. Adding assets to DB.");
await _localAlbumRepository.upsert(updatedDeviceAlbum, toUpsert: assetsInDevice); await _localAlbumRepository.upsert(updatedDeviceAlbum, toUpsert: assetsInDevice);
await _mapIosCloudIds(assetsInDevice);
return true; return true;
} }
@@ -259,6 +269,7 @@ class LocalSyncService {
} }
await _localAlbumRepository.upsert(updatedDeviceAlbum, toUpsert: assetsToUpsert, toDelete: assetsToDelete); await _localAlbumRepository.upsert(updatedDeviceAlbum, toUpsert: assetsToUpsert, toDelete: assetsToDelete);
await _mapIosCloudIds(assetsToUpsert);
return true; return true;
} catch (e, s) { } catch (e, s) {
@@ -267,6 +278,16 @@ class LocalSyncService {
return true; return true;
} }
Future<void> _mapIosCloudIds(List<LocalAsset> assets) async {
if (!_platform.isIOS || assets.isEmpty) {
return;
}
final assetIds = assets.map((a) => a.id).toList();
final cloudMapping = await _nativeSyncApi.getCloudIdForAssetIds(assetIds);
await _localAlbumRepository.updateCloudMapping(cloudMapping);
}
bool _assetsEqual(LocalAsset a, LocalAsset b) { bool _assetsEqual(LocalAsset a, LocalAsset b) {
return a.updatedAt.isAtSameMomentAs(b.updatedAt) && return a.updatedAt.isAtSameMomentAs(b.updatedAt) &&
a.createdAt.isAtSameMomentAs(b.createdAt) && a.createdAt.isAtSameMomentAs(b.createdAt) &&

View File

@@ -90,7 +90,7 @@ class StoreService {
_cache.clear(); _cache.clear();
} }
bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? true; bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? false;
} }
class StoreKeyNotFoundException implements Exception { class StoreKeyNotFoundException implements Exception {

View File

@@ -86,6 +86,10 @@ class SyncStreamService {
return _syncStreamRepository.deleteAssetsV1(data.cast()); return _syncStreamRepository.deleteAssetsV1(data.cast());
case SyncEntityType.assetExifV1: case SyncEntityType.assetExifV1:
return _syncStreamRepository.updateAssetsExifV1(data.cast()); return _syncStreamRepository.updateAssetsExifV1(data.cast());
case SyncEntityType.assetMetadataV1:
return _syncStreamRepository.updateAssetsMetadataV1(data.cast());
case SyncEntityType.assetMetadataDeleteV1:
return _syncStreamRepository.deleteAssetsMetadataV1(data.cast());
case SyncEntityType.partnerAssetV1: case SyncEntityType.partnerAssetV1:
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner'); return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner');
case SyncEntityType.partnerAssetBackfillV1: case SyncEntityType.partnerAssetBackfillV1:

View File

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:immich_mobile/domain/utils/migrate_cloud_ids.dart' as m;
import 'package:immich_mobile/domain/utils/sync_linked_album.dart'; import 'package:immich_mobile/domain/utils/sync_linked_album.dart';
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart'; import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
import 'package:immich_mobile/utils/isolate.dart'; import 'package:immich_mobile/utils/isolate.dart';
@@ -21,8 +22,13 @@ class BackgroundSyncManager {
final SyncCallback? onHashingComplete; final SyncCallback? onHashingComplete;
final SyncErrorCallback? onHashingError; final SyncErrorCallback? onHashingError;
final SyncCallback? onCloudIdSyncStart;
final SyncCallback? onCloudIdSyncComplete;
final SyncErrorCallback? onCloudIdSyncError;
Cancelable<void>? _syncTask; Cancelable<void>? _syncTask;
Cancelable<void>? _syncWebsocketTask; Cancelable<void>? _syncWebsocketTask;
Cancelable<void>? _cloudIdSyncTask;
Cancelable<void>? _deviceAlbumSyncTask; Cancelable<void>? _deviceAlbumSyncTask;
Cancelable<void>? _linkedAlbumSyncTask; Cancelable<void>? _linkedAlbumSyncTask;
Cancelable<void>? _hashTask; Cancelable<void>? _hashTask;
@@ -37,6 +43,9 @@ class BackgroundSyncManager {
this.onHashingStart, this.onHashingStart,
this.onHashingComplete, this.onHashingComplete,
this.onHashingError, this.onHashingError,
this.onCloudIdSyncStart,
this.onCloudIdSyncComplete,
this.onCloudIdSyncError,
}); });
Future<void> cancel() async { Future<void> cancel() async {
@@ -54,6 +63,11 @@ class BackgroundSyncManager {
_syncWebsocketTask?.cancel(); _syncWebsocketTask?.cancel();
_syncWebsocketTask = null; _syncWebsocketTask = null;
if (_cloudIdSyncTask != null) {
futures.add(_cloudIdSyncTask!.future);
}
_cloudIdSyncTask?.cancel();
if (_linkedAlbumSyncTask != null) { if (_linkedAlbumSyncTask != null) {
futures.add(_linkedAlbumSyncTask!.future); futures.add(_linkedAlbumSyncTask!.future);
} }
@@ -114,7 +128,6 @@ class BackgroundSyncManager {
}); });
} }
// No need to cancel the task, as it can also be run when the user logs out
Future<void> hashAssets() { Future<void> hashAssets() {
if (_hashTask != null) { if (_hashTask != null) {
return _hashTask!.future; return _hashTask!.future;
@@ -174,6 +187,25 @@ class BackgroundSyncManager {
_linkedAlbumSyncTask = null; _linkedAlbumSyncTask = null;
}); });
} }
Future<void> syncCloudIds() {
if (_cloudIdSyncTask != null) {
return _cloudIdSyncTask!.future;
}
onCloudIdSyncStart?.call();
_cloudIdSyncTask = runInIsolateGentle(computation: m.syncCloudIds);
return _cloudIdSyncTask!
.whenComplete(() {
onCloudIdSyncComplete?.call();
_cloudIdSyncTask = null;
})
.catchError((error) {
onCloudIdSyncError?.call(error.toString());
_cloudIdSyncTask = null;
});
}
} }
Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle( Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(

View File

@@ -0,0 +1,235 @@
import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
const String kIsolateLockManagerPort = "immich://isolate_mutex";
enum _LockStatus { active, released }
class _IsolateRequest {
const _IsolateRequest();
}
class _HeartbeatRequest extends _IsolateRequest {
// Port for the receiver to send replies back
final SendPort sendPort;
const _HeartbeatRequest(this.sendPort);
Map<String, dynamic> toJson() {
return {'type': 'heartbeat', 'sendPort': sendPort};
}
}
class _CloseRequest extends _IsolateRequest {
const _CloseRequest();
Map<String, dynamic> toJson() {
return {'type': 'close'};
}
}
class _IsolateResponse {
const _IsolateResponse();
}
class _HeartbeatResponse extends _IsolateResponse {
final _LockStatus status;
const _HeartbeatResponse(this.status);
Map<String, dynamic> toJson() {
return {'type': 'heartbeat', 'status': status.index};
}
}
typedef OnCloseLockHolderRequest = void Function();
class IsolateLockManager {
final String _portName;
bool _hasLock = false;
ReceivePort? _receivePort;
final OnCloseLockHolderRequest? _onCloseRequest;
final Set<SendPort> _waitingIsolates = {};
// Token object - a new one is created for each acquisition attempt
Object? _currentAcquisitionToken;
IsolateLockManager({String? portName, OnCloseLockHolderRequest? onCloseRequest})
: _portName = portName ?? kIsolateLockManagerPort,
_onCloseRequest = onCloseRequest;
Future<bool> acquireLock() async {
if (_hasLock) {
Logger('BackgroundWorkerLockManager').warning("WARNING: [acquireLock] called more than once");
return true;
}
// Create a new token - this invalidates any previous attempt
final token = _currentAcquisitionToken = Object();
final ReceivePort rp = _receivePort = ReceivePort(_portName);
final SendPort sp = rp.sendPort;
while (!IsolateNameServer.registerPortWithName(sp, _portName)) {
// This attempt was superseded by a newer one in the same isolate
if (_currentAcquisitionToken != token) {
return false;
}
await _lockReleasedByHolder(token);
}
_hasLock = true;
rp.listen(_onRequest);
return true;
}
Future<void> _lockReleasedByHolder(Object token) async {
SendPort? holder = IsolateNameServer.lookupPortByName(_portName);
debugPrint("Found lock holder: $holder");
if (holder == null) {
// No holder, try and acquire lock
return;
}
final ReceivePort tempRp = ReceivePort();
final SendPort tempSp = tempRp.sendPort;
final bs = tempRp.asBroadcastStream();
try {
while (true) {
// Send a heartbeat request with the send port to receive reply from the holder
debugPrint("Sending heartbeat request to lock holder");
holder.send(_HeartbeatRequest(tempSp).toJson());
dynamic answer = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => null);
debugPrint("Received heartbeat response from lock holder: $answer");
// This attempt was superseded by a newer one in the same isolate
if (_currentAcquisitionToken != token) {
break;
}
if (answer == null) {
// Holder failed, most likely killed without calling releaseLock
// Check if a different waiting isolate took the lock
if (holder == IsolateNameServer.lookupPortByName(_portName)) {
// No, remove the stale lock
IsolateNameServer.removePortNameMapping(_portName);
}
break;
}
// Unknown message type received for heartbeat request. Try again
_IsolateResponse? response = _parseResponse(answer);
if (response == null || response is! _HeartbeatResponse) {
break;
}
if (response.status == _LockStatus.released) {
// Holder has released the lock
break;
}
// If the _LockStatus is active, we check again if the task completed
// by sending a released messaged again, if not, send a new heartbeat again
// Check if the holder completed its task after the heartbeat
answer = await bs.first.timeout(
const Duration(seconds: 3),
onTimeout: () => const _HeartbeatResponse(_LockStatus.active).toJson(),
);
response = _parseResponse(answer);
if (response is _HeartbeatResponse && response.status == _LockStatus.released) {
break;
}
}
} catch (e) {
// Timeout or error
} finally {
tempRp.close();
}
return;
}
_IsolateRequest? _parseRequest(dynamic msg) {
if (msg is! Map<String, dynamic>) {
return null;
}
return switch (msg['type']) {
'heartbeat' => _HeartbeatRequest(msg['sendPort']),
'close' => const _CloseRequest(),
_ => null,
};
}
_IsolateResponse? _parseResponse(dynamic msg) {
if (msg is! Map<String, dynamic>) {
return null;
}
return switch (msg['type']) {
'heartbeat' => _HeartbeatResponse(_LockStatus.values[msg['status']]),
_ => null,
};
}
// Executed in the isolate with the lock
void _onRequest(dynamic msg) {
final request = _parseRequest(msg);
if (request == null) {
return;
}
if (request is _HeartbeatRequest) {
// Add the send port to the list of waiting isolates
_waitingIsolates.add(request.sendPort);
request.sendPort.send(const _HeartbeatResponse(_LockStatus.active).toJson());
return;
}
if (request is _CloseRequest) {
_onCloseRequest?.call();
return;
}
}
void releaseLock() {
if (_hasLock) {
IsolateNameServer.removePortNameMapping(_portName);
// Notify waiting isolates
for (final port in _waitingIsolates) {
port.send(const _HeartbeatResponse(_LockStatus.released).toJson());
}
_waitingIsolates.clear();
_hasLock = false;
}
_receivePort?.close();
_receivePort = null;
}
void cancel() {
if (_hasLock) {
return;
}
debugPrint("Cancelling ongoing acquire lock attempts");
// Create a new token to invalidate ongoing acquire lock attempts
_currentAcquisitionToken = Object();
}
void requestHolderToClose() {
if (_hasLock) {
return;
}
IsolateNameServer.lookupPortByName(_portName)?.send(const _CloseRequest().toJson());
}
}

View File

@@ -0,0 +1,82 @@
import 'package:drift/drift.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:logging/logging.dart';
// ignore: import_rule_openapi
import 'package:openapi/api.dart';
Future<void> syncCloudIds(ProviderContainer ref) async {
final db = ref.read(driftProvider);
// Populate cloud IDs for local assets that don't have one yet
await _populateCloudIds(db);
// Wait for remote sync to complete, so we have up-to-date asset metadata entries
await ref.read(syncStreamServiceProvider).sync();
// Fetch the mapping for backed up assets that have a cloud ID locally but do not have a cloud ID on the server
final currentUser = ref.read(currentUserProvider);
if (currentUser == null) {
Logger('migrateCloudIds').warning('Current user is null. Aborting cloudId migration.');
return;
}
final mappingsToUpdate = await _fetchCloudIdMappings(db, currentUser.id);
final assetApi = ref.read(apiServiceProvider).assetsApi;
for (final mapping in mappingsToUpdate) {
final mobileMeta = AssetMetadataUpsertItemDto(
key: AssetMetadataKey.mobileApp,
value: RemoteAssetMobileAppMetadata(cloudId: mapping.cloudId),
);
try {
await assetApi.updateAssetMetadata(mapping.assetId, AssetMetadataUpsertDto(items: [mobileMeta]));
} catch (error, stack) {
Logger('migrateCloudIds').warning('Failed to update metadata for asset ${mapping.assetId}', error, stack);
}
}
}
Future<void> _populateCloudIds(Drift drift) async {
final query = drift.localAssetEntity.selectOnly()
..addColumns([drift.localAssetEntity.id])
..where(drift.localAssetEntity.iCloudId.isNull());
final ids = await query.map((row) => row.read(drift.localAssetEntity.id)!).get();
final cloudMapping = await NativeSyncApi().getCloudIdForAssetIds(ids);
await DriftLocalAlbumRepository(drift).updateCloudMapping(cloudMapping);
}
typedef _CloudIdMapping = ({String assetId, String cloudId});
Future<List<_CloudIdMapping>> _fetchCloudIdMappings(Drift drift, String userId) async {
final query =
drift.remoteAssetEntity.selectOnly().join([
leftOuterJoin(
drift.localAssetEntity,
drift.localAssetEntity.checksum.equalsExp(drift.remoteAssetEntity.checksum),
useColumns: false,
),
leftOuterJoin(
drift.remoteAssetCloudIdEntity,
drift.localAssetEntity.iCloudId.equalsExp(drift.remoteAssetCloudIdEntity.cloudId),
useColumns: false,
),
])
..addColumns([drift.remoteAssetEntity.id, drift.localAssetEntity.iCloudId])
..where(
drift.localAssetEntity.id.isNotNull() &
drift.localAssetEntity.iCloudId.isNotNull() &
drift.remoteAssetEntity.ownerId.equals(userId) &
drift.remoteAssetCloudIdEntity.cloudId.isNull(),
);
return query
.map(
(row) => (assetId: row.read(drift.remoteAssetEntity.id)!, cloudId: row.read(drift.localAssetEntity.iCloudId)!),
)
.get();
}

View File

@@ -5,6 +5,7 @@ import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)') @TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)')
class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
const LocalAssetEntity(); const LocalAssetEntity();
@@ -16,6 +17,8 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
IntColumn get orientation => integer().withDefault(const Constant(0))(); IntColumn get orientation => integer().withDefault(const Constant(0))();
TextColumn get iCloudId => text().nullable()();
@override @override
Set<Column> get primaryKey => {id}; Set<Column> get primaryKey => {id};
} }
@@ -34,5 +37,6 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
width: width, width: width,
remoteId: null, remoteId: null,
orientation: orientation, orientation: orientation,
cloudId: iCloudId,
); );
} }

View File

@@ -21,6 +21,7 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder =
i0.Value<String?> checksum, i0.Value<String?> checksum,
i0.Value<bool> isFavorite, i0.Value<bool> isFavorite,
i0.Value<int> orientation, i0.Value<int> orientation,
i0.Value<String?> iCloudId,
}); });
typedef $$LocalAssetEntityTableUpdateCompanionBuilder = typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
i1.LocalAssetEntityCompanion Function({ i1.LocalAssetEntityCompanion Function({
@@ -35,6 +36,7 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
i0.Value<String?> checksum, i0.Value<String?> checksum,
i0.Value<bool> isFavorite, i0.Value<bool> isFavorite,
i0.Value<int> orientation, i0.Value<int> orientation,
i0.Value<String?> iCloudId,
}); });
class $$LocalAssetEntityTableFilterComposer class $$LocalAssetEntityTableFilterComposer
@@ -101,6 +103,11 @@ class $$LocalAssetEntityTableFilterComposer
column: $table.orientation, column: $table.orientation,
builder: (column) => i0.ColumnFilters(column), builder: (column) => i0.ColumnFilters(column),
); );
i0.ColumnFilters<String> get iCloudId => $composableBuilder(
column: $table.iCloudId,
builder: (column) => i0.ColumnFilters(column),
);
} }
class $$LocalAssetEntityTableOrderingComposer class $$LocalAssetEntityTableOrderingComposer
@@ -166,6 +173,11 @@ class $$LocalAssetEntityTableOrderingComposer
column: $table.orientation, column: $table.orientation,
builder: (column) => i0.ColumnOrderings(column), builder: (column) => i0.ColumnOrderings(column),
); );
i0.ColumnOrderings<String> get iCloudId => $composableBuilder(
column: $table.iCloudId,
builder: (column) => i0.ColumnOrderings(column),
);
} }
class $$LocalAssetEntityTableAnnotationComposer class $$LocalAssetEntityTableAnnotationComposer
@@ -215,6 +227,9 @@ class $$LocalAssetEntityTableAnnotationComposer
column: $table.orientation, column: $table.orientation,
builder: (column) => column, builder: (column) => column,
); );
i0.GeneratedColumn<String> get iCloudId =>
$composableBuilder(column: $table.iCloudId, builder: (column) => column);
} }
class $$LocalAssetEntityTableTableManager class $$LocalAssetEntityTableTableManager
@@ -268,6 +283,7 @@ class $$LocalAssetEntityTableTableManager
i0.Value<String?> checksum = const i0.Value.absent(), i0.Value<String?> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(), i0.Value<bool> isFavorite = const i0.Value.absent(),
i0.Value<int> orientation = const i0.Value.absent(), i0.Value<int> orientation = const i0.Value.absent(),
i0.Value<String?> iCloudId = const i0.Value.absent(),
}) => i1.LocalAssetEntityCompanion( }) => i1.LocalAssetEntityCompanion(
name: name, name: name,
type: type, type: type,
@@ -280,6 +296,7 @@ class $$LocalAssetEntityTableTableManager
checksum: checksum, checksum: checksum,
isFavorite: isFavorite, isFavorite: isFavorite,
orientation: orientation, orientation: orientation,
iCloudId: iCloudId,
), ),
createCompanionCallback: createCompanionCallback:
({ ({
@@ -294,6 +311,7 @@ class $$LocalAssetEntityTableTableManager
i0.Value<String?> checksum = const i0.Value.absent(), i0.Value<String?> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(), i0.Value<bool> isFavorite = const i0.Value.absent(),
i0.Value<int> orientation = const i0.Value.absent(), i0.Value<int> orientation = const i0.Value.absent(),
i0.Value<String?> iCloudId = const i0.Value.absent(),
}) => i1.LocalAssetEntityCompanion.insert( }) => i1.LocalAssetEntityCompanion.insert(
name: name, name: name,
type: type, type: type,
@@ -306,6 +324,7 @@ class $$LocalAssetEntityTableTableManager
checksum: checksum, checksum: checksum,
isFavorite: isFavorite, isFavorite: isFavorite,
orientation: orientation, orientation: orientation,
iCloudId: iCloudId,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
@@ -473,6 +492,17 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: const i4.Constant(0), defaultValue: const i4.Constant(0),
); );
static const i0.VerificationMeta _iCloudIdMeta = const i0.VerificationMeta(
'iCloudId',
);
@override
late final i0.GeneratedColumn<String> iCloudId = i0.GeneratedColumn<String>(
'i_cloud_id',
aliasedName,
true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
);
@override @override
List<i0.GeneratedColumn> get $columns => [ List<i0.GeneratedColumn> get $columns => [
name, name,
@@ -486,6 +516,7 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
checksum, checksum,
isFavorite, isFavorite,
orientation, orientation,
iCloudId,
]; ];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@@ -566,6 +597,12 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
), ),
); );
} }
if (data.containsKey('i_cloud_id')) {
context.handle(
_iCloudIdMeta,
iCloudId.isAcceptableOrUnknown(data['i_cloud_id']!, _iCloudIdMeta),
);
}
return context; return context;
} }
@@ -624,6 +661,10 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
i0.DriftSqlType.int, i0.DriftSqlType.int,
data['${effectivePrefix}orientation'], data['${effectivePrefix}orientation'],
)!, )!,
iCloudId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}i_cloud_id'],
),
); );
} }
@@ -653,6 +694,7 @@ class LocalAssetEntityData extends i0.DataClass
final String? checksum; final String? checksum;
final bool isFavorite; final bool isFavorite;
final int orientation; final int orientation;
final String? iCloudId;
const LocalAssetEntityData({ const LocalAssetEntityData({
required this.name, required this.name,
required this.type, required this.type,
@@ -665,6 +707,7 @@ class LocalAssetEntityData extends i0.DataClass
this.checksum, this.checksum,
required this.isFavorite, required this.isFavorite,
required this.orientation, required this.orientation,
this.iCloudId,
}); });
@override @override
Map<String, i0.Expression> toColumns(bool nullToAbsent) { Map<String, i0.Expression> toColumns(bool nullToAbsent) {
@@ -692,6 +735,9 @@ class LocalAssetEntityData extends i0.DataClass
} }
map['is_favorite'] = i0.Variable<bool>(isFavorite); map['is_favorite'] = i0.Variable<bool>(isFavorite);
map['orientation'] = i0.Variable<int>(orientation); map['orientation'] = i0.Variable<int>(orientation);
if (!nullToAbsent || iCloudId != null) {
map['i_cloud_id'] = i0.Variable<String>(iCloudId);
}
return map; return map;
} }
@@ -714,6 +760,7 @@ class LocalAssetEntityData extends i0.DataClass
checksum: serializer.fromJson<String?>(json['checksum']), checksum: serializer.fromJson<String?>(json['checksum']),
isFavorite: serializer.fromJson<bool>(json['isFavorite']), isFavorite: serializer.fromJson<bool>(json['isFavorite']),
orientation: serializer.fromJson<int>(json['orientation']), orientation: serializer.fromJson<int>(json['orientation']),
iCloudId: serializer.fromJson<String?>(json['iCloudId']),
); );
} }
@override @override
@@ -733,6 +780,7 @@ class LocalAssetEntityData extends i0.DataClass
'checksum': serializer.toJson<String?>(checksum), 'checksum': serializer.toJson<String?>(checksum),
'isFavorite': serializer.toJson<bool>(isFavorite), 'isFavorite': serializer.toJson<bool>(isFavorite),
'orientation': serializer.toJson<int>(orientation), 'orientation': serializer.toJson<int>(orientation),
'iCloudId': serializer.toJson<String?>(iCloudId),
}; };
} }
@@ -748,6 +796,7 @@ class LocalAssetEntityData extends i0.DataClass
i0.Value<String?> checksum = const i0.Value.absent(), i0.Value<String?> checksum = const i0.Value.absent(),
bool? isFavorite, bool? isFavorite,
int? orientation, int? orientation,
i0.Value<String?> iCloudId = const i0.Value.absent(),
}) => i1.LocalAssetEntityData( }) => i1.LocalAssetEntityData(
name: name ?? this.name, name: name ?? this.name,
type: type ?? this.type, type: type ?? this.type,
@@ -762,6 +811,7 @@ class LocalAssetEntityData extends i0.DataClass
checksum: checksum.present ? checksum.value : this.checksum, checksum: checksum.present ? checksum.value : this.checksum,
isFavorite: isFavorite ?? this.isFavorite, isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation, orientation: orientation ?? this.orientation,
iCloudId: iCloudId.present ? iCloudId.value : this.iCloudId,
); );
LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) { LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) {
return LocalAssetEntityData( return LocalAssetEntityData(
@@ -782,6 +832,7 @@ class LocalAssetEntityData extends i0.DataClass
orientation: data.orientation.present orientation: data.orientation.present
? data.orientation.value ? data.orientation.value
: this.orientation, : this.orientation,
iCloudId: data.iCloudId.present ? data.iCloudId.value : this.iCloudId,
); );
} }
@@ -798,7 +849,8 @@ class LocalAssetEntityData extends i0.DataClass
..write('id: $id, ') ..write('id: $id, ')
..write('checksum: $checksum, ') ..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ') ..write('isFavorite: $isFavorite, ')
..write('orientation: $orientation') ..write('orientation: $orientation, ')
..write('iCloudId: $iCloudId')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@@ -816,6 +868,7 @@ class LocalAssetEntityData extends i0.DataClass
checksum, checksum,
isFavorite, isFavorite,
orientation, orientation,
iCloudId,
); );
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@@ -831,7 +884,8 @@ class LocalAssetEntityData extends i0.DataClass
other.id == this.id && other.id == this.id &&
other.checksum == this.checksum && other.checksum == this.checksum &&
other.isFavorite == this.isFavorite && other.isFavorite == this.isFavorite &&
other.orientation == this.orientation); other.orientation == this.orientation &&
other.iCloudId == this.iCloudId);
} }
class LocalAssetEntityCompanion class LocalAssetEntityCompanion
@@ -847,6 +901,7 @@ class LocalAssetEntityCompanion
final i0.Value<String?> checksum; final i0.Value<String?> checksum;
final i0.Value<bool> isFavorite; final i0.Value<bool> isFavorite;
final i0.Value<int> orientation; final i0.Value<int> orientation;
final i0.Value<String?> iCloudId;
const LocalAssetEntityCompanion({ const LocalAssetEntityCompanion({
this.name = const i0.Value.absent(), this.name = const i0.Value.absent(),
this.type = const i0.Value.absent(), this.type = const i0.Value.absent(),
@@ -859,6 +914,7 @@ class LocalAssetEntityCompanion
this.checksum = const i0.Value.absent(), this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(),
this.orientation = const i0.Value.absent(), this.orientation = const i0.Value.absent(),
this.iCloudId = const i0.Value.absent(),
}); });
LocalAssetEntityCompanion.insert({ LocalAssetEntityCompanion.insert({
required String name, required String name,
@@ -872,6 +928,7 @@ class LocalAssetEntityCompanion
this.checksum = const i0.Value.absent(), this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(),
this.orientation = const i0.Value.absent(), this.orientation = const i0.Value.absent(),
this.iCloudId = const i0.Value.absent(),
}) : name = i0.Value(name), }) : name = i0.Value(name),
type = i0.Value(type), type = i0.Value(type),
id = i0.Value(id); id = i0.Value(id);
@@ -887,6 +944,7 @@ class LocalAssetEntityCompanion
i0.Expression<String>? checksum, i0.Expression<String>? checksum,
i0.Expression<bool>? isFavorite, i0.Expression<bool>? isFavorite,
i0.Expression<int>? orientation, i0.Expression<int>? orientation,
i0.Expression<String>? iCloudId,
}) { }) {
return i0.RawValuesInsertable({ return i0.RawValuesInsertable({
if (name != null) 'name': name, if (name != null) 'name': name,
@@ -900,6 +958,7 @@ class LocalAssetEntityCompanion
if (checksum != null) 'checksum': checksum, if (checksum != null) 'checksum': checksum,
if (isFavorite != null) 'is_favorite': isFavorite, if (isFavorite != null) 'is_favorite': isFavorite,
if (orientation != null) 'orientation': orientation, if (orientation != null) 'orientation': orientation,
if (iCloudId != null) 'i_cloud_id': iCloudId,
}); });
} }
@@ -915,6 +974,7 @@ class LocalAssetEntityCompanion
i0.Value<String?>? checksum, i0.Value<String?>? checksum,
i0.Value<bool>? isFavorite, i0.Value<bool>? isFavorite,
i0.Value<int>? orientation, i0.Value<int>? orientation,
i0.Value<String?>? iCloudId,
}) { }) {
return i1.LocalAssetEntityCompanion( return i1.LocalAssetEntityCompanion(
name: name ?? this.name, name: name ?? this.name,
@@ -928,6 +988,7 @@ class LocalAssetEntityCompanion
checksum: checksum ?? this.checksum, checksum: checksum ?? this.checksum,
isFavorite: isFavorite ?? this.isFavorite, isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation, orientation: orientation ?? this.orientation,
iCloudId: iCloudId ?? this.iCloudId,
); );
} }
@@ -969,6 +1030,9 @@ class LocalAssetEntityCompanion
if (orientation.present) { if (orientation.present) {
map['orientation'] = i0.Variable<int>(orientation.value); map['orientation'] = i0.Variable<int>(orientation.value);
} }
if (iCloudId.present) {
map['i_cloud_id'] = i0.Variable<String>(iCloudId.value);
}
return map; return map;
} }
@@ -985,8 +1049,14 @@ class LocalAssetEntityCompanion
..write('id: $id, ') ..write('id: $id, ')
..write('checksum: $checksum, ') ..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ') ..write('isFavorite: $isFavorite, ')
..write('orientation: $orientation') ..write('orientation: $orientation, ')
..write('iCloudId: $iCloudId')
..write(')')) ..write(')'))
.toString(); .toString();
} }
} }
i0.Index get idxLocalAssetCloudId => i0.Index(
'idx_local_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
);

View File

@@ -21,7 +21,8 @@ SELECT
rae.owner_id, rae.owner_id,
rae.live_photo_video_id, rae.live_photo_video_id,
0 as orientation, 0 as orientation,
rae.stack_id rae.stack_id,
NULL as i_cloud_id
FROM FROM
remote_asset_entity rae remote_asset_entity rae
LEFT JOIN LEFT JOIN
@@ -53,7 +54,8 @@ SELECT
NULL as owner_id, NULL as owner_id,
NULL as live_photo_video_id, NULL as live_photo_video_id,
lae.orientation, lae.orientation,
NULL as stack_id NULL as stack_id,
lae.i_cloud_id
FROM FROM
local_asset_entity lae local_asset_entity lae
WHERE NOT EXISTS ( WHERE NOT EXISTS (

View File

@@ -29,7 +29,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
); );
$arrayStartIndex += generatedlimit.amountOfVariables; $arrayStartIndex += generatedlimit.amountOfVariables;
return customSelect( return customSelect(
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}', 'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
variables: [ variables: [
for (var $ in userIds) i0.Variable<String>($), for (var $ in userIds) i0.Variable<String>($),
...generatedlimit.introducedVariables, ...generatedlimit.introducedVariables,
@@ -62,6 +62,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
livePhotoVideoId: row.readNullable<String>('live_photo_video_id'), livePhotoVideoId: row.readNullable<String>('live_photo_video_id'),
orientation: row.read<int>('orientation'), orientation: row.read<int>('orientation'),
stackId: row.readNullable<String>('stack_id'), stackId: row.readNullable<String>('stack_id'),
iCloudId: row.readNullable<String>('i_cloud_id'),
), ),
); );
} }
@@ -129,6 +130,7 @@ class MergedAssetResult {
final String? livePhotoVideoId; final String? livePhotoVideoId;
final int orientation; final int orientation;
final String? stackId; final String? stackId;
final String? iCloudId;
MergedAssetResult({ MergedAssetResult({
this.remoteId, this.remoteId,
this.localId, this.localId,
@@ -146,6 +148,7 @@ class MergedAssetResult {
this.livePhotoVideoId, this.livePhotoVideoId,
required this.orientation, required this.orientation,
this.stackId, this.stackId,
this.iCloudId,
}); });
} }

View File

@@ -0,0 +1,12 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class RemoteAssetCloudIdEntity extends Table with DriftDefaultsMixin {
TextColumn get assetId => text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get cloudId => text().unique().nullable()();
@override
Set<Column> get primaryKey => {assetId};
}

View File

@@ -0,0 +1,548 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.dart'
as i2;
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
as i3;
import 'package:drift/internal/modular.dart' as i4;
typedef $$RemoteAssetCloudIdEntityTableCreateCompanionBuilder =
i1.RemoteAssetCloudIdEntityCompanion Function({
required String assetId,
i0.Value<String?> cloudId,
});
typedef $$RemoteAssetCloudIdEntityTableUpdateCompanionBuilder =
i1.RemoteAssetCloudIdEntityCompanion Function({
i0.Value<String> assetId,
i0.Value<String?> cloudId,
});
final class $$RemoteAssetCloudIdEntityTableReferences
extends
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$RemoteAssetCloudIdEntityTable,
i1.RemoteAssetCloudIdEntityData
> {
$$RemoteAssetCloudIdEntityTableReferences(
super.$_db,
super.$_table,
super.$_typedResult,
);
static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
i4.ReadDatabaseContainer(db)
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
.createAlias(
i0.$_aliasNameGenerator(
i4.ReadDatabaseContainer(db)
.resultSet<i1.$RemoteAssetCloudIdEntityTable>(
'remote_asset_cloud_id_entity',
)
.assetId,
i4.ReadDatabaseContainer(
db,
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity').id,
),
);
i3.$$RemoteAssetEntityTableProcessedTableManager get assetId {
final $_column = $_itemColumn<String>('asset_id')!;
final manager = i3
.$$RemoteAssetEntityTableTableManager(
$_db,
i4.ReadDatabaseContainer(
$_db,
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
)
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]),
);
}
}
class $$RemoteAssetCloudIdEntityTableFilterComposer
extends
i0.Composer<i0.GeneratedDatabase, i1.$RemoteAssetCloudIdEntityTable> {
$$RemoteAssetCloudIdEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get cloudId => $composableBuilder(
column: $table.cloudId,
builder: (column) => i0.ColumnFilters(column),
);
i3.$$RemoteAssetEntityTableFilterComposer get assetId {
final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i4.ReadDatabaseContainer(
$db,
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i3.$$RemoteAssetEntityTableFilterComposer(
$db: $db,
$table: i4.ReadDatabaseContainer(
$db,
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$RemoteAssetCloudIdEntityTableOrderingComposer
extends
i0.Composer<i0.GeneratedDatabase, i1.$RemoteAssetCloudIdEntityTable> {
$$RemoteAssetCloudIdEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get cloudId => $composableBuilder(
column: $table.cloudId,
builder: (column) => i0.ColumnOrderings(column),
);
i3.$$RemoteAssetEntityTableOrderingComposer get assetId {
final i3.$$RemoteAssetEntityTableOrderingComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i4.ReadDatabaseContainer(
$db,
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i3.$$RemoteAssetEntityTableOrderingComposer(
$db: $db,
$table: i4.ReadDatabaseContainer(
$db,
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$RemoteAssetCloudIdEntityTableAnnotationComposer
extends
i0.Composer<i0.GeneratedDatabase, i1.$RemoteAssetCloudIdEntityTable> {
$$RemoteAssetCloudIdEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get cloudId =>
$composableBuilder(column: $table.cloudId, builder: (column) => column);
i3.$$RemoteAssetEntityTableAnnotationComposer get assetId {
final i3.$$RemoteAssetEntityTableAnnotationComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i4.ReadDatabaseContainer(
$db,
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i3.$$RemoteAssetEntityTableAnnotationComposer(
$db: $db,
$table: i4.ReadDatabaseContainer(
$db,
).resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$RemoteAssetCloudIdEntityTableTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.$RemoteAssetCloudIdEntityTable,
i1.RemoteAssetCloudIdEntityData,
i1.$$RemoteAssetCloudIdEntityTableFilterComposer,
i1.$$RemoteAssetCloudIdEntityTableOrderingComposer,
i1.$$RemoteAssetCloudIdEntityTableAnnotationComposer,
$$RemoteAssetCloudIdEntityTableCreateCompanionBuilder,
$$RemoteAssetCloudIdEntityTableUpdateCompanionBuilder,
(
i1.RemoteAssetCloudIdEntityData,
i1.$$RemoteAssetCloudIdEntityTableReferences,
),
i1.RemoteAssetCloudIdEntityData,
i0.PrefetchHooks Function({bool assetId})
> {
$$RemoteAssetCloudIdEntityTableTableManager(
i0.GeneratedDatabase db,
i1.$RemoteAssetCloudIdEntityTable table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$RemoteAssetCloudIdEntityTableFilterComposer(
$db: db,
$table: table,
),
createOrderingComposer: () =>
i1.$$RemoteAssetCloudIdEntityTableOrderingComposer(
$db: db,
$table: table,
),
createComputedFieldComposer: () =>
i1.$$RemoteAssetCloudIdEntityTableAnnotationComposer(
$db: db,
$table: table,
),
updateCompanionCallback:
({
i0.Value<String> assetId = const i0.Value.absent(),
i0.Value<String?> cloudId = const i0.Value.absent(),
}) => i1.RemoteAssetCloudIdEntityCompanion(
assetId: assetId,
cloudId: cloudId,
),
createCompanionCallback:
({
required String assetId,
i0.Value<String?> cloudId = const i0.Value.absent(),
}) => i1.RemoteAssetCloudIdEntityCompanion.insert(
assetId: assetId,
cloudId: cloudId,
),
withReferenceMapper: (p0) => p0
.map(
(e) => (
e.readTable(table),
i1.$$RemoteAssetCloudIdEntityTableReferences(db, table, e),
),
)
.toList(),
prefetchHooksCallback: ({assetId = false}) {
return i0.PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
addJoins:
<
T extends i0.TableManagerState<
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic
>
>(state) {
if (assetId) {
state =
state.withJoin(
currentTable: table,
currentColumn: table.assetId,
referencedTable: i1
.$$RemoteAssetCloudIdEntityTableReferences
._assetIdTable(db),
referencedColumn: i1
.$$RemoteAssetCloudIdEntityTableReferences
._assetIdTable(db)
.id,
)
as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
),
);
}
typedef $$RemoteAssetCloudIdEntityTableProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$RemoteAssetCloudIdEntityTable,
i1.RemoteAssetCloudIdEntityData,
i1.$$RemoteAssetCloudIdEntityTableFilterComposer,
i1.$$RemoteAssetCloudIdEntityTableOrderingComposer,
i1.$$RemoteAssetCloudIdEntityTableAnnotationComposer,
$$RemoteAssetCloudIdEntityTableCreateCompanionBuilder,
$$RemoteAssetCloudIdEntityTableUpdateCompanionBuilder,
(
i1.RemoteAssetCloudIdEntityData,
i1.$$RemoteAssetCloudIdEntityTableReferences,
),
i1.RemoteAssetCloudIdEntityData,
i0.PrefetchHooks Function({bool assetId})
>;
class $RemoteAssetCloudIdEntityTable extends i2.RemoteAssetCloudIdEntity
with
i0.TableInfo<
$RemoteAssetCloudIdEntityTable,
i1.RemoteAssetCloudIdEntityData
> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$RemoteAssetCloudIdEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _assetIdMeta = const i0.VerificationMeta(
'assetId',
);
@override
late final i0.GeneratedColumn<String> assetId = i0.GeneratedColumn<String>(
'asset_id',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES remote_asset_entity (id) ON DELETE CASCADE',
),
);
static const i0.VerificationMeta _cloudIdMeta = const i0.VerificationMeta(
'cloudId',
);
@override
late final i0.GeneratedColumn<String> cloudId = i0.GeneratedColumn<String>(
'cloud_id',
aliasedName,
true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways('UNIQUE'),
);
@override
List<i0.GeneratedColumn> get $columns => [assetId, cloudId];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'remote_asset_cloud_id_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.RemoteAssetCloudIdEntityData> instance, {
bool isInserting = false,
}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('asset_id')) {
context.handle(
_assetIdMeta,
assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta),
);
} else if (isInserting) {
context.missing(_assetIdMeta);
}
if (data.containsKey('cloud_id')) {
context.handle(
_cloudIdMeta,
cloudId.isAcceptableOrUnknown(data['cloud_id']!, _cloudIdMeta),
);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {assetId};
@override
i1.RemoteAssetCloudIdEntityData map(
Map<String, dynamic> data, {
String? tablePrefix,
}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.RemoteAssetCloudIdEntityData(
assetId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}asset_id'],
)!,
cloudId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}cloud_id'],
),
);
}
@override
$RemoteAssetCloudIdEntityTable createAlias(String alias) {
return $RemoteAssetCloudIdEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class RemoteAssetCloudIdEntityData extends i0.DataClass
implements i0.Insertable<i1.RemoteAssetCloudIdEntityData> {
final String assetId;
final String? cloudId;
const RemoteAssetCloudIdEntityData({required this.assetId, this.cloudId});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['asset_id'] = i0.Variable<String>(assetId);
if (!nullToAbsent || cloudId != null) {
map['cloud_id'] = i0.Variable<String>(cloudId);
}
return map;
}
factory RemoteAssetCloudIdEntityData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return RemoteAssetCloudIdEntityData(
assetId: serializer.fromJson<String>(json['assetId']),
cloudId: serializer.fromJson<String?>(json['cloudId']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'assetId': serializer.toJson<String>(assetId),
'cloudId': serializer.toJson<String?>(cloudId),
};
}
i1.RemoteAssetCloudIdEntityData copyWith({
String? assetId,
i0.Value<String?> cloudId = const i0.Value.absent(),
}) => i1.RemoteAssetCloudIdEntityData(
assetId: assetId ?? this.assetId,
cloudId: cloudId.present ? cloudId.value : this.cloudId,
);
RemoteAssetCloudIdEntityData copyWithCompanion(
i1.RemoteAssetCloudIdEntityCompanion data,
) {
return RemoteAssetCloudIdEntityData(
assetId: data.assetId.present ? data.assetId.value : this.assetId,
cloudId: data.cloudId.present ? data.cloudId.value : this.cloudId,
);
}
@override
String toString() {
return (StringBuffer('RemoteAssetCloudIdEntityData(')
..write('assetId: $assetId, ')
..write('cloudId: $cloudId')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(assetId, cloudId);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.RemoteAssetCloudIdEntityData &&
other.assetId == this.assetId &&
other.cloudId == this.cloudId);
}
class RemoteAssetCloudIdEntityCompanion
extends i0.UpdateCompanion<i1.RemoteAssetCloudIdEntityData> {
final i0.Value<String> assetId;
final i0.Value<String?> cloudId;
const RemoteAssetCloudIdEntityCompanion({
this.assetId = const i0.Value.absent(),
this.cloudId = const i0.Value.absent(),
});
RemoteAssetCloudIdEntityCompanion.insert({
required String assetId,
this.cloudId = const i0.Value.absent(),
}) : assetId = i0.Value(assetId);
static i0.Insertable<i1.RemoteAssetCloudIdEntityData> custom({
i0.Expression<String>? assetId,
i0.Expression<String>? cloudId,
}) {
return i0.RawValuesInsertable({
if (assetId != null) 'asset_id': assetId,
if (cloudId != null) 'cloud_id': cloudId,
});
}
i1.RemoteAssetCloudIdEntityCompanion copyWith({
i0.Value<String>? assetId,
i0.Value<String?>? cloudId,
}) {
return i1.RemoteAssetCloudIdEntityCompanion(
assetId: assetId ?? this.assetId,
cloudId: cloudId ?? this.cloudId,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (assetId.present) {
map['asset_id'] = i0.Variable<String>(assetId.value);
}
if (cloudId.present) {
map['cloud_id'] = i0.Variable<String>(cloudId.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('RemoteAssetCloudIdEntityCompanion(')
..write('assetId: $assetId, ')
..write('cloudId: $cloudId')
..write(')'))
.toString();
}
}

View File

@@ -4,6 +4,7 @@ import 'package:drift/drift.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
@@ -137,4 +138,22 @@ class DriftBackupRepository extends DriftDatabaseRepository {
return query.map((localAsset) => localAsset.toDto()).get(); return query.map((localAsset) => localAsset.toDto()).get();
} }
FutureOr<List<LocalAlbum>> getSourceAlbums(String localAssetId) {
final query = _db.localAlbumEntity.select()
..where(
(lae) =>
existsQuery(
_db.localAlbumAssetEntity.selectOnly()
..addColumns([_db.localAlbumAssetEntity.albumId])
..where(
_db.localAlbumAssetEntity.albumId.equalsExp(lae.id) &
_db.localAlbumAssetEntity.assetId.equals(localAssetId),
),
) &
lae.backupSelection.equalsValue(BackupSelection.selected),
)
..orderBy([(lae) => OrderingTerm.asc(lae.name)]);
return query.map((localAlbum) => localAlbum.toDto()).get();
}
} }

View File

@@ -18,6 +18,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart'; import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
@@ -56,6 +57,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
RemoteAlbumEntity, RemoteAlbumEntity,
RemoteAlbumAssetEntity, RemoteAlbumAssetEntity,
RemoteAlbumUserEntity, RemoteAlbumUserEntity,
RemoteAssetCloudIdEntity,
MemoryEntity, MemoryEntity,
MemoryAssetEntity, MemoryAssetEntity,
StackEntity, StackEntity,
@@ -70,7 +72,7 @@ class Drift extends $Drift implements IDatabaseRepository {
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true))); : super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
@override @override
int get schemaVersion => 10; int get schemaVersion => 11;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@@ -133,6 +135,12 @@ class Drift extends $Drift implements IDatabaseRepository {
await m.addColumn(v10.userEntity, v10.userEntity.avatarColor); await m.addColumn(v10.userEntity, v10.userEntity.avatarColor);
await m.alterTable(TableMigration(v10.userEntity)); await m.alterTable(TableMigration(v10.userEntity));
}, },
from10To11: (m, v11) async {
// Add i_cloud_id to local and remote asset tables
await m.addColumn(v11.localAssetEntity, v11.localAssetEntity.iCloudId);
await m.createIndex(v11.idxLocalAssetCloudId);
await m.createTable(v11.remoteAssetCloudIdEntity);
},
), ),
); );

View File

@@ -27,19 +27,21 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.
as i12; as i12;
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
as i13; as i13;
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart'
as i14; as i14;
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
as i15; as i15;
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
as i16; as i16;
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
as i17; as i17;
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
as i18; as i18;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
as i19; as i19;
import 'package:drift/internal/modular.dart' as i20; import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i20;
import 'package:drift/internal/modular.dart' as i21;
abstract class $Drift extends i0.GeneratedDatabase { abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e); $Drift(i0.QueryExecutor e) : super(e);
@@ -70,16 +72,18 @@ abstract class $Drift extends i0.GeneratedDatabase {
.$RemoteAlbumAssetEntityTable(this); .$RemoteAlbumAssetEntityTable(this);
late final i13.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i13 late final i13.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i13
.$RemoteAlbumUserEntityTable(this); .$RemoteAlbumUserEntityTable(this);
late final i14.$MemoryEntityTable memoryEntity = i14.$MemoryEntityTable(this); late final i14.$RemoteAssetCloudIdEntityTable remoteAssetCloudIdEntity = i14
late final i15.$MemoryAssetEntityTable memoryAssetEntity = i15 .$RemoteAssetCloudIdEntityTable(this);
late final i15.$MemoryEntityTable memoryEntity = i15.$MemoryEntityTable(this);
late final i16.$MemoryAssetEntityTable memoryAssetEntity = i16
.$MemoryAssetEntityTable(this); .$MemoryAssetEntityTable(this);
late final i16.$PersonEntityTable personEntity = i16.$PersonEntityTable(this); late final i17.$PersonEntityTable personEntity = i17.$PersonEntityTable(this);
late final i17.$AssetFaceEntityTable assetFaceEntity = i17 late final i18.$AssetFaceEntityTable assetFaceEntity = i18
.$AssetFaceEntityTable(this); .$AssetFaceEntityTable(this);
late final i18.$StoreEntityTable storeEntity = i18.$StoreEntityTable(this); late final i19.$StoreEntityTable storeEntity = i19.$StoreEntityTable(this);
i19.MergedAssetDrift get mergedAssetDrift => i20.ReadDatabaseContainer( i20.MergedAssetDrift get mergedAssetDrift => i21.ReadDatabaseContainer(
this, this,
).accessor<i19.MergedAssetDrift>(i19.MergedAssetDrift.new); ).accessor<i20.MergedAssetDrift>(i20.MergedAssetDrift.new);
@override @override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables => Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>(); allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -93,6 +97,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
localAlbumEntity, localAlbumEntity,
localAlbumAssetEntity, localAlbumAssetEntity,
i4.idxLocalAssetChecksum, i4.idxLocalAssetChecksum,
i4.idxLocalAssetCloudId,
i2.idxRemoteAssetOwnerChecksum, i2.idxRemoteAssetOwnerChecksum,
i2.uQRemoteAssetsOwnerChecksum, i2.uQRemoteAssetsOwnerChecksum,
i2.uQRemoteAssetsOwnerLibraryChecksum, i2.uQRemoteAssetsOwnerLibraryChecksum,
@@ -103,6 +108,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
remoteExifEntity, remoteExifEntity,
remoteAlbumAssetEntity, remoteAlbumAssetEntity,
remoteAlbumUserEntity, remoteAlbumUserEntity,
remoteAssetCloudIdEntity,
memoryEntity, memoryEntity,
memoryAssetEntity, memoryAssetEntity,
personEntity, personEntity,
@@ -242,6 +248,18 @@ abstract class $Drift extends i0.GeneratedDatabase {
i0.TableUpdate('remote_album_user_entity', kind: i0.UpdateKind.delete), i0.TableUpdate('remote_album_user_entity', kind: i0.UpdateKind.delete),
], ],
), ),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'remote_asset_entity',
limitUpdateKind: i0.UpdateKind.delete,
),
result: [
i0.TableUpdate(
'remote_asset_cloud_id_entity',
kind: i0.UpdateKind.delete,
),
],
),
i0.WritePropagation( i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName( on: i0.TableUpdateQuery.onTableName(
'user_entity', 'user_entity',
@@ -326,14 +344,20 @@ class $DriftManager {
); );
i13.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i13 i13.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i13
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity); .$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
i14.$$MemoryEntityTableTableManager get memoryEntity => i14.$$RemoteAssetCloudIdEntityTableTableManager
i14.$$MemoryEntityTableTableManager(_db, _db.memoryEntity); get remoteAssetCloudIdEntity =>
i15.$$MemoryAssetEntityTableTableManager get memoryAssetEntity => i14.$$RemoteAssetCloudIdEntityTableTableManager(
i15.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity); _db,
i16.$$PersonEntityTableTableManager get personEntity => _db.remoteAssetCloudIdEntity,
i16.$$PersonEntityTableTableManager(_db, _db.personEntity); );
i17.$$AssetFaceEntityTableTableManager get assetFaceEntity => i15.$$MemoryEntityTableTableManager get memoryEntity =>
i17.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity); i15.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
i18.$$StoreEntityTableTableManager get storeEntity => i16.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
i18.$$StoreEntityTableTableManager(_db, _db.storeEntity); i16.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
i17.$$PersonEntityTableTableManager get personEntity =>
i17.$$PersonEntityTableTableManager(_db, _db.personEntity);
i18.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
i18.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
i19.$$StoreEntityTableTableManager get storeEntity =>
i19.$$StoreEntityTableTableManager(_db, _db.storeEntity);
} }

View File

@@ -4270,6 +4270,455 @@ i1.GeneratedColumn<String> _column_94(String aliasedName) =>
true, true,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
); );
final class Schema11 extends i0.VersionedSchema {
Schema11({required super.database}) : super(version: 11);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAssetChecksum,
idxLocalAssetCloudId,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
remoteAssetCloudIdEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
idxLatLng,
];
late final Shape20 userEntity = Shape20(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_84,
_column_85,
_column_91,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape17 remoteAssetEntity = Shape17(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_86,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape3 stackEntity = Shape3(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
attachedDatabase: database,
),
alias: null,
);
late final Shape22 localAssetEntity = Shape22(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_22,
_column_14,
_column_23,
_column_95,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape9 remoteAlbumEntity = Shape9(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_56,
_column_9,
_column_5,
_column_15,
_column_57,
_column_58,
_column_59,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape19 localAlbumEntity = Shape19(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_5,
_column_31,
_column_32,
_column_90,
_column_33,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 localAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_34, _column_35],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxLocalAssetCloudId = i1.Index(
'idx_local_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
);
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
late final Shape21 authUserEntity = Shape21(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_2,
_column_84,
_column_85,
_column_92,
_column_93,
_column_7,
_column_94,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_25, _column_26, _column_27],
attachedDatabase: database,
),
alias: null,
);
late final Shape5 partnerEntity = Shape5(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_28, _column_29, _column_30],
attachedDatabase: database,
),
alias: null,
);
late final Shape8 remoteExifEntity = Shape8(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_11,
_column_10,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_50,
_column_51,
_column_52,
_column_53,
_column_54,
_column_55,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_36, _column_60],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_60, _column_25, _column_61],
attachedDatabase: database,
),
alias: null,
);
late final Shape23 remoteAssetCloudIdEntity = Shape23(
source: i0.VersionedTable(
entityName: 'remote_asset_cloud_id_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [_column_36, _column_96],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 memoryEntity = Shape11(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_18,
_column_15,
_column_8,
_column_62,
_column_63,
_column_64,
_column_65,
_column_66,
_column_67,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_36, _column_68],
attachedDatabase: database,
),
alias: null,
);
late final Shape14 personEntity = Shape14(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_15,
_column_1,
_column_69,
_column_71,
_column_72,
_column_73,
_column_74,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape15 assetFaceEntity = Shape15(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_36,
_column_76,
_column_77,
_column_78,
_column_79,
_column_80,
_column_81,
_column_82,
_column_83,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_87, _column_88, _column_89],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
}
class Shape22 extends i0.VersionedTable {
Shape22({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get type =>
columnsByName['type']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get width =>
columnsByName['width']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get height =>
columnsByName['height']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get durationInSeconds =>
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get checksum =>
columnsByName['checksum']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isFavorite =>
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<int> get orientation =>
columnsByName['orientation']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get iCloudId =>
columnsByName['i_cloud_id']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_95(String aliasedName) =>
i1.GeneratedColumn<String>(
'i_cloud_id',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
class Shape23 extends i0.VersionedTable {
Shape23({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get assetId =>
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get cloudId =>
columnsByName['cloud_id']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_96(String aliasedName) =>
i1.GeneratedColumn<String>(
'cloud_id',
aliasedName,
true,
type: i1.DriftSqlType.string,
defaultConstraints: i1.GeneratedColumn.constraintIsAlways('UNIQUE'),
);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -4280,6 +4729,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9, required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10, required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@@ -4328,6 +4778,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from9To10(migrator, schema); await from9To10(migrator, schema);
return 10; return 10;
case 10:
final schema = Schema11(database: database);
final migrator = i1.Migrator(database, schema);
await from10To11(migrator, schema);
return 11;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@@ -4344,6 +4799,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9, required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10, required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
@@ -4355,5 +4811,6 @@ i1.OnUpgrade stepByStep({
from7To8: from7To8, from7To8: from7To8,
from8To9: from8To9, from8To9: from8To9,
from9To10: from9To10, from9To10: from9To10,
from10To11: from10To11,
), ),
); );

View File

@@ -231,6 +231,25 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get(); return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get();
} }
Future<void> updateCloudMapping(Map<String, String?> cloudMapping) {
if (cloudMapping.isEmpty) {
return Future.value();
}
return _db.batch((batch) {
for (final entry in cloudMapping.entries) {
final assetId = entry.key;
final cloudId = entry.value;
batch.update(
_db.localAssetEntity,
LocalAssetEntityCompanion(iCloudId: Value(cloudId)),
where: (f) => f.id.equals(assetId),
);
}
});
}
Future<void> _upsertAssets(Iterable<LocalAsset> localAssets) { Future<void> _upsertAssets(Iterable<LocalAsset> localAssets) {
if (localAssets.isEmpty) { if (localAssets.isEmpty) {
return Future.value(); return Future.value();

View File

@@ -1,12 +1,12 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
typedef LocalAssetHashMapping = ({String assetId, String checksum});
class DriftLocalAssetRepository extends DriftDatabaseRepository { class DriftLocalAssetRepository extends DriftDatabaseRepository {
final Drift _db; final Drift _db;
const DriftLocalAssetRepository(this._db) : super(_db); const DriftLocalAssetRepository(this._db) : super(_db);
@@ -28,25 +28,19 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
Future<LocalAsset?> get(String id) => _assetSelectable(id).getSingleOrNull(); Future<LocalAsset?> get(String id) => _assetSelectable(id).getSingleOrNull();
Future<List<LocalAsset?>> getByChecksum(String checksum) {
final query = _db.localAssetEntity.select()..where((lae) => lae.checksum.equals(checksum));
return query.map((row) => row.toDto()).get();
}
Stream<LocalAsset?> watch(String id) => _assetSelectable(id).watchSingleOrNull(); Stream<LocalAsset?> watch(String id) => _assetSelectable(id).watchSingleOrNull();
Future<void> updateHashes(Iterable<LocalAsset> hashes) { Future<void> updateHashes(Iterable<LocalAssetHashMapping> hashes) {
if (hashes.isEmpty) { if (hashes.isEmpty) {
return Future.value(); return Future.value();
} }
return _db.batch((batch) async { return _db.batch((batch) async {
for (final asset in hashes) { for (final mapping in hashes) {
batch.update( batch.update(
_db.localAssetEntity, _db.localAssetEntity,
LocalAssetEntityCompanion(checksum: Value(asset.checksum)), LocalAssetEntityCompanion(checksum: Value(mapping.checksum)),
where: (e) => e.id.equals(asset.id), where: (e) => e.id.equals(mapping.assetId),
); );
} }
}); });
@@ -78,22 +72,26 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
return _db.managers.localAssetEntity.filter((e) => e.checksum.isNull().not()).count(); return _db.managers.localAssetEntity.filter((e) => e.checksum.isNull().not()).count();
} }
Future<List<LocalAlbum>> getSourceAlbums(String localAssetId, {BackupSelection? backupSelection}) { Future<List<LocalAssetHashMapping>> getHashMappingFromCloudId() async {
final query = _db.localAlbumEntity.select() final query =
..where( _db.localAssetEntity.selectOnly().join([
(lae) => existsQuery( leftOuterJoin(
_db.localAlbumAssetEntity.selectOnly() _db.remoteAssetCloudIdEntity,
..addColumns([_db.localAlbumAssetEntity.albumId]) _db.localAssetEntity.iCloudId.equalsExp(_db.remoteAssetCloudIdEntity.cloudId),
..where( useColumns: false,
_db.localAlbumAssetEntity.albumId.equalsExp(lae.id) &
_db.localAlbumAssetEntity.assetId.equals(localAssetId),
), ),
), leftOuterJoin(
) _db.remoteAssetEntity,
..orderBy([(lae) => OrderingTerm.asc(lae.name)]); _db.remoteAssetCloudIdEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
if (backupSelection != null) { useColumns: false,
query.where((lae) => lae.backupSelection.equalsValue(backupSelection)); ),
} ])
return query.map((localAlbum) => localAlbum.toDto()).get(); ..addColumns([_db.localAssetEntity.id, _db.remoteAssetEntity.checksum])
..where(_db.remoteAssetCloudIdEntity.cloudId.isNotNull() & _db.localAssetEntity.checksum.isNull());
return query
.map(
(row) => (assetId: row.read(_db.localAssetEntity.id)!, checksum: row.read(_db.remoteAssetEntity.checksum)!),
)
.get();
} }
} }

View File

@@ -55,12 +55,6 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
return _assetSelectable(id).getSingleOrNull(); return _assetSelectable(id).getSingleOrNull();
} }
Future<RemoteAsset?> getByChecksum(String checksum) {
final query = _db.remoteAssetEntity.select()..where((row) => row.checksum.equals(checksum));
return query.map((row) => row.toDto()).getSingleOrNull();
}
Future<List<RemoteAsset>> getStackChildren(RemoteAsset asset) { Future<List<RemoteAsset>> getStackChildren(RemoteAsset asset) {
if (asset.stackId == null) { if (asset.stackId == null) {
return Future.value([]); return Future.value([]);

View File

@@ -42,6 +42,7 @@ class SyncApiRepository {
SyncRequestType.usersV1, SyncRequestType.usersV1,
SyncRequestType.assetsV1, SyncRequestType.assetsV1,
SyncRequestType.assetExifsV1, SyncRequestType.assetExifsV1,
SyncRequestType.assetMetadataV1,
SyncRequestType.partnersV1, SyncRequestType.partnersV1,
SyncRequestType.partnerAssetsV1, SyncRequestType.partnerAssetsV1,
SyncRequestType.partnerAssetExifsV1, SyncRequestType.partnerAssetExifsV1,
@@ -142,6 +143,8 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.assetV1: SyncAssetV1.fromJson, SyncEntityType.assetV1: SyncAssetV1.fromJson,
SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson, SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson,
SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson, SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson,
SyncEntityType.assetMetadataV1: SyncAssetMetadataV1.fromJson,
SyncEntityType.assetMetadataDeleteV1: SyncAssetMetadataDeleteV1.fromJson,
SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson, SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson,
SyncEntityType.partnerAssetBackfillV1: SyncAssetV1.fromJson, SyncEntityType.partnerAssetBackfillV1: SyncAssetV1.fromJson,
SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson, SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson,

View File

@@ -18,6 +18,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart';
@@ -241,6 +242,42 @@ class SyncStreamRepository extends DriftDatabaseRepository {
} }
} }
Future<void> deleteAssetsMetadataV1(Iterable<SyncAssetMetadataDeleteV1> data) async {
try {
await _db.batch((batch) {
for (final metadata in data) {
if (metadata.key == AssetMetadataKey.mobileApp) {
batch.deleteWhere(_db.remoteAssetCloudIdEntity, (row) => row.assetId.equals(metadata.assetId));
}
}
});
} catch (error, stack) {
_logger.severe('Error: deleteAssetsMetadataV1', error, stack);
rethrow;
}
}
Future<void> updateAssetsMetadataV1(Iterable<SyncAssetMetadataV1> data) async {
try {
await _db.batch((batch) {
for (final metadata in data) {
if (metadata.key == AssetMetadataKey.mobileApp) {
final map = metadata.value as Map<String, Object?>;
final companion = RemoteAssetCloudIdEntityCompanion(cloudId: Value(map['iCloudId']?.toString()));
batch.insert(
_db.remoteAssetCloudIdEntity,
companion.copyWith(assetId: Value(metadata.assetId)),
onConflict: DoUpdate((_) => companion),
);
}
}
});
} catch (error, stack) {
_logger.severe('Error: updateAssetsMetadataV1', error, stack);
rethrow;
}
}
Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async { Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async {
try { try {
await _db.remoteAlbumEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.albumId))); await _db.remoteAlbumEntity.deleteWhere((row) => row.id.isIn(data.map((e) => e.albumId)));

View File

@@ -87,6 +87,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
isFavorite: row.isFavorite, isFavorite: row.isFavorite,
durationInSeconds: row.durationInSeconds, durationInSeconds: row.durationInSeconds,
orientation: row.orientation, orientation: row.orientation,
cloudId: row.iCloudId,
), ),
) )
.get(); .get();

View File

@@ -17,9 +17,9 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart';
@@ -205,9 +205,9 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
// needs to be delayed so that EasyLocalization is working // needs to be delayed so that EasyLocalization is working
if (Store.isBetaTimelineEnabled) { if (Store.isBetaTimelineEnabled) {
ref.read(backgroundServiceProvider).disableService(); ref.read(backgroundServiceProvider).disableService();
ref.read(backgroundWorkerFgServiceProvider).enable(); ref.read(driftBackgroundUploadFgService).enable();
} else { } else {
ref.read(backgroundWorkerFgServiceProvider).disable(); ref.read(driftBackgroundUploadFgService).disable();
ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
} }
}); });

View File

@@ -13,7 +13,6 @@ import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/background.service.dart';
@@ -80,7 +79,7 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false); ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider)); await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
await ref.read(backgroundWorkerFgServiceProvider).disable(); await ref.read(driftBackgroundUploadFgService).disable();
} }
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);

View File

@@ -11,7 +11,7 @@ import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_se
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/beta_sync_settings.dart';
import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart'; import 'package:immich_mobile/widgets/settings/beta_timeline_list_tile.dart';
import 'package:immich_mobile/widgets/settings/language_settings.dart'; import 'package:immich_mobile/widgets/settings/language_settings.dart';
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
@@ -20,7 +20,7 @@ import 'package:immich_mobile/widgets/settings/preference_settings/preference_se
import 'package:immich_mobile/widgets/settings/settings_card.dart'; import 'package:immich_mobile/widgets/settings/settings_card.dart';
enum SettingSection { enum SettingSection {
beta('sync_status', Icons.sync_outlined, "sync_status_subtitle"), beta('beta_sync', Icons.sync_outlined, "beta_sync_subtitle"),
advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"), advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"),
assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"), assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"),
backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"), backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"),
@@ -76,9 +76,9 @@ class _MobileLayout extends StatelessWidget {
if (Store.isBetaTimelineEnabled) if (Store.isBetaTimelineEnabled)
SettingsCard( SettingsCard(
icon: Icons.sync_outlined, icon: Icons.sync_outlined,
title: 'sync_status'.tr(), title: 'beta_sync'.tr(),
subtitle: 'sync_status_subtitle'.tr(), subtitle: 'beta_sync_subtitle'.tr(),
settingRoute: const SyncStatusRoute(), settingRoute: const BetaSyncSettingsRoute(),
), ),
] ]
: [ : [
@@ -143,7 +143,7 @@ class _BetaLandscapeToggle extends HookWidget {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
const SizedBox(height: 100, child: BetaTimelineListTile()), const SizedBox(height: 100, child: BetaTimelineListTile()),
if (Store.isBetaTimelineEnabled) const Expanded(child: SyncStatusAndActions()), if (Store.isBetaTimelineEnabled) const Expanded(child: BetaSyncSettings()),
], ],
); );
} }

View File

@@ -2,8 +2,10 @@ import 'package:auto_route/auto_route.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/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -21,14 +23,23 @@ class SplashScreenPage extends StatefulHookConsumerWidget {
class SplashScreenPageState extends ConsumerState<SplashScreenPage> { class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
final log = Logger("SplashScreenPage"); final log = Logger("SplashScreenPage");
@override @override
void initState() { void initState() {
super.initState(); super.initState();
ref final lockManager = ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
.read(authProvider.notifier)
.setOpenApiServiceEndpoint() lockManager.requestHolderToClose();
.then(logConnectionInfo) lockManager
.whenComplete(() => resumeSession()); .acquireLock()
.timeout(const Duration(seconds: 5))
.whenComplete(
() => ref
.read(authProvider.notifier)
.setOpenApiServiceEndpoint()
.then(logConnectionInfo)
.whenComplete(() => resumeSession()),
);
} }
void logConnectionInfo(String? endpoint) { void logConnectionInfo(String? endpoint) {
@@ -69,16 +80,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
return; return;
} }
// clean install - change the default of the flag
// current install not using beta timeline
if (context.router.current.name == SplashScreenRoute.name) { if (context.router.current.name == SplashScreenRoute.name) {
final needBetaMigration = Store.get(StoreKey.needBetaMigration, false);
if (needBetaMigration) {
await Store.put(StoreKey.needBetaMigration, false);
context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: true)]);
return;
}
context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute()); context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute());
} }

View File

@@ -1,25 +1,25 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/widgets/settings/beta_sync_settings/sync_status_and_actions.dart'; import 'package:immich_mobile/widgets/settings/beta_sync_settings/beta_sync_settings.dart';
@RoutePage() @RoutePage()
class SyncStatusPage extends StatelessWidget { class BetaSyncSettingsPage extends StatelessWidget {
const SyncStatusPage({super.key}); const BetaSyncSettingsPage({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
elevation: 0, elevation: 0,
title: const Text("sync_status").t(context: context), title: const Text("beta_sync").t(context: context),
leading: IconButton( leading: IconButton(
onPressed: () => context.maybePop(true), onPressed: () => context.maybePop(true),
splashRadius: 24, splashRadius: 24,
icon: const Icon(Icons.arrow_back_ios_rounded), icon: const Icon(Icons.arrow_back_ios_rounded),
), ),
), ),
body: const SyncStatusAndActions(), body: const BetaSyncSettings(),
); );
} }
} }

View File

@@ -495,4 +495,32 @@ class NativeSyncApi {
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<Uint8List?>(); return (pigeonVar_replyList[0] as List<Object?>?)!.cast<Uint8List?>();
} }
} }
Future<Map<String, String?>> getCloudIdForAssetIds(List<String> assetIds) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[assetIds]);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as Map<Object?, Object?>?)!.cast<String, String?>();
}
}
} }

View File

@@ -1,345 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
@RoutePage()
class AssetTroubleshootPage extends ConsumerWidget {
final BaseAsset asset;
const AssetTroubleshootPage({super.key, required this.asset});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: const Text("Asset Troubleshoot")),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: _AssetDetailsView(asset: asset),
),
),
);
}
}
class _AssetDetailsView extends ConsumerWidget {
final BaseAsset asset;
const _AssetDetailsView({required this.asset});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_AssetPropertiesSection(asset: asset),
const SizedBox(height: 16),
Text('Matching Assets', style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
if (asset.checksum != null) ...[
_LocalAssetsSection(asset: asset),
const SizedBox(height: 16),
_RemoteAssetSection(asset: asset),
] else ...[
const _PropertySectionCard(
title: 'Local Assets',
properties: [_PropertyItem(label: 'Status', value: 'No checksum available - cannot fetch local assets')],
),
const SizedBox(height: 16),
const _PropertySectionCard(
title: 'Remote Assets',
properties: [_PropertyItem(label: 'Status', value: 'No checksum available - cannot fetch remote asset')],
),
],
],
);
}
}
class _AssetPropertiesSection extends ConsumerStatefulWidget {
final BaseAsset asset;
const _AssetPropertiesSection({required this.asset});
@override
ConsumerState createState() => _AssetPropertiesSectionState();
}
class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection> {
List<_PropertyItem> properties = [];
@override
void initState() {
super.initState();
_buildAssetProperties(widget.asset).whenComplete(() {
if (mounted) {
setState(() {});
}
});
}
@override
Widget build(BuildContext context) {
final title = _getAssetTypeTitle(widget.asset);
return _PropertySectionCard(title: title, properties: properties);
}
Future<void> _buildAssetProperties(BaseAsset asset) async {
_addCommonProperties();
if (asset is LocalAsset) {
await _addLocalAssetProperties(asset);
} else if (asset is RemoteAsset) {
await _addRemoteAssetProperties(asset);
}
}
void _addCommonProperties() {
final asset = widget.asset;
properties.addAll([
_PropertyItem(label: 'Name', value: asset.name),
_PropertyItem(label: 'Checksum', value: asset.checksum),
_PropertyItem(label: 'Type', value: asset.type.toString()),
_PropertyItem(label: 'Created At', value: asset.createdAt.toString()),
_PropertyItem(label: 'Updated At', value: asset.updatedAt.toString()),
_PropertyItem(label: 'Width', value: asset.width?.toString()),
_PropertyItem(label: 'Height', value: asset.height?.toString()),
_PropertyItem(
label: 'Duration',
value: asset.durationInSeconds != null ? '${asset.durationInSeconds} seconds' : null,
),
_PropertyItem(label: 'Is Favorite', value: asset.isFavorite.toString()),
_PropertyItem(label: 'Live Photo Video ID', value: asset.livePhotoVideoId),
]);
}
Future<void> _addLocalAssetProperties(LocalAsset asset) async {
properties.insertAll(0, [
_PropertyItem(label: 'Local ID', value: asset.id),
_PropertyItem(label: 'Remote ID', value: asset.remoteId),
]);
properties.insert(4, _PropertyItem(label: 'Orientation', value: asset.orientation.toString()));
final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id);
properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', ')));
}
Future<void> _addRemoteAssetProperties(RemoteAsset asset) async {
properties.insertAll(0, [
_PropertyItem(label: 'Remote ID', value: asset.id),
_PropertyItem(label: 'Local ID', value: asset.localId),
_PropertyItem(label: 'Owner ID', value: asset.ownerId),
]);
final additionalProps = <_PropertyItem>[
_PropertyItem(label: 'Thumb Hash', value: asset.thumbHash),
_PropertyItem(label: 'Visibility', value: asset.visibility.toString()),
_PropertyItem(label: 'Stack ID', value: asset.stackId),
];
properties.insertAll(4, additionalProps);
final exif = await ref.read(assetServiceProvider).getExif(asset);
if (exif != null) {
_addExifProperties(exif);
} else {
properties.add(const _PropertyItem(label: 'EXIF', value: null));
}
}
void _addExifProperties(ExifInfo exif) {
properties.addAll([
_PropertyItem(
label: 'File Size',
value: exif.fileSize != null ? '${(exif.fileSize! / 1024 / 1024).toStringAsFixed(2)} MB' : null,
),
_PropertyItem(label: 'Description', value: exif.description),
_PropertyItem(label: 'EXIF Width', value: exif.width?.toString()),
_PropertyItem(label: 'EXIF Height', value: exif.height?.toString()),
_PropertyItem(label: 'Date Taken', value: exif.dateTimeOriginal?.toString()),
_PropertyItem(label: 'Time Zone', value: exif.timeZone),
_PropertyItem(label: 'Camera Make', value: exif.make),
_PropertyItem(label: 'Camera Model', value: exif.model),
_PropertyItem(label: 'Lens', value: exif.lens),
_PropertyItem(label: 'F-Number', value: exif.f != null ? 'f/${exif.fNumber}' : null),
_PropertyItem(label: 'Focal Length', value: exif.mm != null ? '${exif.focalLength}mm' : null),
_PropertyItem(label: 'ISO', value: exif.iso?.toString()),
_PropertyItem(label: 'Exposure Time', value: exif.exposureTime.isNotEmpty ? exif.exposureTime : null),
_PropertyItem(
label: 'GPS Coordinates',
value: exif.hasCoordinates ? '${exif.latitude}, ${exif.longitude}' : null,
),
_PropertyItem(
label: 'Location',
value: [exif.city, exif.state, exif.country].where((e) => e != null && e.isNotEmpty).join(', '),
),
]);
}
String _getAssetTypeTitle(BaseAsset asset) {
if (asset is LocalAsset) return 'Local Asset';
if (asset is RemoteAsset) return 'Remote Asset';
return 'Base Asset';
}
}
class _LocalAssetsSection extends ConsumerWidget {
final BaseAsset asset;
const _LocalAssetsSection({required this.asset});
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetService = ref.watch(assetServiceProvider);
return FutureBuilder<List<LocalAsset?>>(
future: assetService.getLocalAssetsByChecksum(asset.checksum!),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const _PropertySectionCard(
title: 'Local Assets',
properties: [_PropertyItem(label: 'Status', value: 'Loading...')],
);
}
if (snapshot.hasError) {
return _PropertySectionCard(
title: 'Local Assets',
properties: [_PropertyItem(label: 'Error', value: snapshot.error.toString())],
);
}
final localAssets = snapshot.data?.cast<LocalAsset>() ?? [];
if (asset is LocalAsset) {
localAssets.removeWhere((a) => a.id == (asset as LocalAsset).id);
if (localAssets.isEmpty) {
return const SizedBox.shrink();
}
}
if (localAssets.isEmpty) {
return const _PropertySectionCard(
title: 'Local Assets',
properties: [_PropertyItem(label: 'Status', value: 'No local assets found with this checksum')],
);
}
return Column(
children: [
if (localAssets.length > 1)
_PropertySectionCard(
title: 'Local Assets Summary',
properties: [_PropertyItem(label: 'Total Count', value: localAssets.length.toString())],
),
...localAssets.map((localAsset) {
return Padding(
padding: const EdgeInsets.only(top: 16),
child: _AssetPropertiesSection(asset: localAsset),
);
}),
],
);
},
);
}
}
class _RemoteAssetSection extends ConsumerWidget {
final BaseAsset asset;
const _RemoteAssetSection({required this.asset});
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetService = ref.watch(assetServiceProvider);
if (asset is RemoteAsset) {
return const SizedBox.shrink();
}
return FutureBuilder<RemoteAsset?>(
future: assetService.getRemoteAssetByChecksum(asset.checksum!),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const _PropertySectionCard(
title: 'Remote Assets',
properties: [_PropertyItem(label: 'Status', value: 'Loading...')],
);
}
if (snapshot.hasError) {
return _PropertySectionCard(
title: 'Remote Assets',
properties: [_PropertyItem(label: 'Error', value: snapshot.error.toString())],
);
}
final remoteAsset = snapshot.data;
if (remoteAsset == null) {
return const _PropertySectionCard(
title: 'Remote Assets',
properties: [_PropertyItem(label: 'Status', value: 'No remote asset found with this checksum')],
);
}
return _AssetPropertiesSection(asset: remoteAsset);
},
);
}
}
class _PropertySectionCard extends StatelessWidget {
final String title;
final List<_PropertyItem> properties;
const _PropertySectionCard({required this.title, required this.properties});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
...properties,
],
),
),
);
}
}
class _PropertyItem extends StatelessWidget {
final String label;
final String? value;
const _PropertyItem({required this.label, this.value});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text('$label:', style: const TextStyle(fontWeight: FontWeight.w500)),
),
Expanded(
child: Text(value ?? 'N/A', style: TextStyle(color: Theme.of(context).colorScheme.secondary)),
),
],
),
);
}
}

View File

@@ -1,30 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
class AdvancedInfoActionButton extends ConsumerWidget {
final ActionSource source;
const AdvancedInfoActionButton({super.key, required this.source});
void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) {
return;
}
ref.read(actionProvider.notifier).troubleshoot(source, context);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton(
maxWidth: 115.0,
iconData: Icons.help_outline_rounded,
label: "troubleshoot".t(context: context),
onPressed: () => _onTap(context, ref),
);
}
}

View File

@@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart';
@@ -15,7 +14,6 @@ import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
@@ -43,7 +41,6 @@ class AssetDetailBottomSheet extends ConsumerWidget {
final isInLockedView = ref.watch(inLockedViewProvider); final isInLockedView = ref.watch(inLockedViewProvider);
final currentAlbum = ref.watch(currentRemoteAlbumProvider); final currentAlbum = ref.watch(currentRemoteAlbumProvider);
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive; final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
final buttonContext = ActionButtonContext( final buttonContext = ActionButtonContext(
asset: asset, asset: asset,
@@ -52,7 +49,6 @@ class AssetDetailBottomSheet extends ConsumerWidget {
isTrashEnabled: isTrashEnable, isTrashEnabled: isTrashEnable,
isInLockedView: isInLockedView, isInLockedView: isInLockedView,
currentAlbum: currentAlbum, currentAlbum: currentAlbum,
advancedTroubleshooting: advancedTroubleshooting,
source: ActionSource.viewer, source: ActionSource.viewer,
); );
@@ -126,10 +122,6 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
return [fNumber, exposureTime, focalLength, iso].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator); return [fNumber, exposureTime, focalLength, iso].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator);
} }
Future<void> _editDateTime(BuildContext context, WidgetRef ref) async {
await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context);
}
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetNotifier); final asset = ref.watch(currentAssetNotifier);
@@ -140,6 +132,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull; final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
final cameraTitle = _getCameraInfoTitle(exifInfo); final cameraTitle = _getCameraInfoTitle(exifInfo);
Future<void> editDateTime() async {
await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context);
}
return SliverList.list( return SliverList.list(
children: [ children: [
// Asset Date and Time // Asset Date and Time
@@ -147,7 +143,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
title: _getDateTime(context, asset), title: _getDateTime(context, asset),
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
trailing: asset.hasRemote ? const Icon(Icons.edit, size: 18) : null, trailing: asset.hasRemote ? const Icon(Icons.edit, size: 18) : null,
onTap: asset.hasRemote ? () async => await _editDateTime(context, ref) : null, onTap: asset.hasRemote ? () async => await editDateTime() : null,
), ),
if (exifInfo != null) _SheetAssetDescription(exif: exifInfo), if (exifInfo != null) _SheetAssetDescription(exif: exifInfo),
const SheetPeopleDetails(), const SheetPeopleDetails(),

View File

@@ -1,8 +1,8 @@
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/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';

View File

@@ -4,8 +4,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
@@ -23,7 +21,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -54,7 +51,6 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final multiselect = ref.watch(multiSelectProvider); final multiselect = ref.watch(multiSelectProvider);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
Future<void> addAssetsToAlbum(RemoteAlbum album) async { Future<void> addAssetsToAlbum(RemoteAlbum album) async {
final selectedAssets = multiselect.selectedAssets; final selectedAssets = multiselect.selectedAssets;
@@ -92,9 +88,6 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
maxChildSize: 0.85, maxChildSize: 0.85,
shouldCloseOnMinExtent: false, shouldCloseOnMinExtent: false,
actions: [ actions: [
if (multiselect.selectedAssets.length == 1 && advancedTroubleshooting) ...[
const AdvancedInfoActionButton(source: ActionSource.timeline),
],
const ShareActionButton(source: ActionSource.timeline), const ShareActionButton(source: ActionSource.timeline),
if (multiselect.hasRemote) ...[ if (multiselect.hasRemote) ...[
const ShareLinkActionButton(source: ActionSource.timeline), const ShareLinkActionButton(source: ActionSource.timeline),

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album.provider.dart';
@@ -124,43 +125,82 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
} }
} }
Future<void> _safeRun(Future<void> action, String debugName) async {
if (!_shouldContinueOperation()) {
return;
}
try {
await action;
} catch (e, stackTrace) {
_log.warning("Error during $debugName operation", e, stackTrace);
}
}
Future<void> _handleBetaTimelineResume() async { Future<void> _handleBetaTimelineResume() async {
_ref.read(backupProvider.notifier).cancelBackup(); _ref.read(backupProvider.notifier).cancelBackup();
final lockManager = _ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
// Give isolates time to complete any ongoing database transactions // Give isolates time to complete any ongoing database transactions
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
lockManager.requestHolderToClose();
// Add timeout to prevent deadlock on lock acquisition
try {
await lockManager.acquireLock().timeout(
const Duration(seconds: 10),
onTimeout: () {
_log.warning("Lock acquisition timed out, proceeding without lock");
throw TimeoutException("Lock acquisition timed out", const Duration(seconds: 10));
},
);
} catch (e) {
_log.warning("Failed to acquire lock: $e");
return;
}
final backgroundManager = _ref.read(backgroundSyncProvider); final backgroundManager = _ref.read(backgroundSyncProvider);
final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
try { try {
// Run operations sequentially with state checks and error handling for each // Run operations sequentially with state checks and error handling for each
_safeRun(backgroundManager.syncLocal(), "syncLocal"); if (_shouldContinueOperation()) {
_safeRun(backgroundManager.hashAssets(), "hashAssets"); try {
_safeRun(backgroundManager.syncRemote(), "syncRemote").then((_) { await backgroundManager.syncLocal();
if (isAlbumLinkedSyncEnable) { } catch (e, stackTrace) {
_safeRun(backgroundManager.syncLinkedAlbum(), "syncLinkedAlbum"); _log.warning("Failed syncLocal: $e", e, stackTrace);
} }
}); }
// Check if app is still active before remote sync
if (_shouldContinueOperation()) {
try {
await backgroundManager.syncRemote();
} catch (e, stackTrace) {
_log.warning("Failed syncRemote: $e", e, stackTrace);
}
if (isAlbumLinkedSyncEnable && _shouldContinueOperation()) {
try {
await backgroundManager.syncLinkedAlbum();
} catch (e, stackTrace) {
_log.warning("Failed syncLinkedAlbum: $e", e, stackTrace);
}
}
}
// Check if app is still active before hashing
if (_shouldContinueOperation()) {
try {
await backgroundManager.hashAssets();
} catch (e, stackTrace) {
_log.warning("Failed hashAssets: $e", e, stackTrace);
}
}
// Handle backup resume only if still active // Handle backup resume only if still active
if (isEnableBackup) { if (_shouldContinueOperation()) {
final currentUser = _ref.read(currentUserProvider); final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
if (currentUser != null) {
_safeRun(_ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id), "handleBackupResume"); if (isEnableBackup) {
final currentUser = _ref.read(currentUserProvider);
if (currentUser != null) {
try {
await _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
_log.fine("Completed backup resume");
} catch (e, stackTrace) {
_log.warning("Failed backup resume: $e", e, stackTrace);
}
}
} }
} }
} catch (e, stackTrace) { } catch (e, stackTrace) {
@@ -168,7 +208,8 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
} finally { } finally {
// Ensure lock is released even if operations fail // Ensure lock is released even if operations fail
try { try {
_log.info("Lock released after background sync operations"); lockManager.releaseLock();
_log.fine("Lock released after background sync operations");
} catch (lockError) { } catch (lockError) {
_log.warning("Failed to release lock after error: $lockError"); _log.warning("Failed to release lock after error: $lockError");
} }
@@ -222,6 +263,28 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) { if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) {
_ref.read(backupProvider.notifier).cancelBackup(); _ref.read(backupProvider.notifier).cancelBackup();
} }
} else {
final backgroundManager = _ref.read(backgroundSyncProvider);
// Cancel operations with extended timeout to allow database transactions to complete
try {
await Future.wait([
backgroundManager.cancel().timeout(const Duration(seconds: 10)),
backgroundManager.cancelLocal().timeout(const Duration(seconds: 10)),
]).timeout(const Duration(seconds: 15));
// Give additional time for isolates to clean up database connections
await Future.delayed(const Duration(milliseconds: 1000));
} catch (e) {
_log.warning("Timeout during background cancellation: $e");
}
// Always release the lock, even if cancellation failed
try {
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
} catch (e) {
_log.warning("Failed to release lock on pause: $e");
}
} }
_ref.read(websocketProvider.notifier).disconnect(); _ref.read(websocketProvider.notifier).disconnect();
@@ -249,6 +312,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
} catch (_) {} } catch (_) {}
if (Store.isBetaTimelineEnabled) { if (Store.isBetaTimelineEnabled) {
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
return; return;
} }

View File

@@ -1,5 +1,6 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart';
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) { final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
@@ -14,7 +15,14 @@ final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
onHashingStart: syncStatusNotifier.startHashJob, onHashingStart: syncStatusNotifier.startHashJob,
onHashingComplete: syncStatusNotifier.completeHashJob, onHashingComplete: syncStatusNotifier.completeHashJob,
onHashingError: syncStatusNotifier.errorHashJob, onHashingError: syncStatusNotifier.errorHashJob,
onCloudIdSyncStart: syncStatusNotifier.startCloudIdSync,
onCloudIdSyncComplete: syncStatusNotifier.completeCloudIdSync,
onCloudIdSyncError: syncStatusNotifier.errorCloudIdSync,
); );
ref.onDispose(manager.cancel); ref.onDispose(manager.cancel);
return manager; return manager;
}); });
final isolateLockManagerProvider = Provider.family<IsolateLockManager, String>((ref, name) {
return IsolateLockManager(portName: name);
});

View File

@@ -6,6 +6,7 @@ import 'package:flutter/foundation.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/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/background_worker.service.dart';
import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/album.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/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
@@ -17,6 +18,7 @@ import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; 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/models/server_info/server_disk_info.model.dart'; import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart';
import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
@@ -34,6 +36,8 @@ 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;
final driftBackgroundUploadFgService = Provider((ref) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) { final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
return BackupNotifier( return BackupNotifier(
ref.watch(backupServiceProvider), ref.watch(backupServiceProvider),

View File

@@ -6,11 +6,11 @@ import 'package:background_downloader/background_downloader.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.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/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/upload.service.dart'; import 'package:immich_mobile/services/upload.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@@ -380,5 +380,5 @@ final driftCandidateBackupAlbumInfoProvider = FutureProvider.autoDispose.family<
ref, ref,
assetId, assetId,
) { ) {
return ref.read(localAssetRepository).getSourceAlbums(assetId, backupSelection: BackupSelection.selected); return ref.read(backupRepositoryProvider).getSourceAlbums(assetId);
}); });

View File

@@ -1,4 +1,3 @@
import 'package:auto_route/auto_route.dart';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
@@ -7,7 +6,6 @@ import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/action.service.dart'; import 'package:immich_mobile/services/action.service.dart';
import 'package:immich_mobile/services/download.service.dart'; import 'package:immich_mobile/services/download.service.dart';
import 'package:immich_mobile/services/timeline.service.dart'; import 'package:immich_mobile/services/timeline.service.dart';
@@ -117,16 +115,6 @@ class ActionNotifier extends Notifier<void> {
}; };
} }
Future<ActionResult> troubleshoot(ActionSource source, BuildContext context) async {
final assets = _getAssets(source);
if (assets.length > 1) {
return ActionResult(count: assets.length, success: false, error: 'Cannot troubleshoot multiple assets');
}
context.pushRoute(AssetTroubleshootRoute(asset: assets.first));
return ActionResult(count: assets.length, success: true);
}
Future<ActionResult> shareLink(ActionSource source, BuildContext context) async { Future<ActionResult> shareLink(ActionSource source, BuildContext context) async {
final ids = _getRemoteIdsForSource(source); final ids = _getRemoteIdsForSource(source);
try { try {

View File

@@ -1,11 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/background_worker.service.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/platform/thumbnail_api.g.dart'; import 'package:immich_mobile/platform/thumbnail_api.g.dart';
final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi()); final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
final thumbnailApi = ThumbnailApi(); final thumbnailApi = ThumbnailApi();

View File

@@ -21,6 +21,7 @@ class SyncStatusState {
final SyncStatus remoteSyncStatus; final SyncStatus remoteSyncStatus;
final SyncStatus localSyncStatus; final SyncStatus localSyncStatus;
final SyncStatus hashJobStatus; final SyncStatus hashJobStatus;
final SyncStatus cloudIdSyncStatus;
final String? errorMessage; final String? errorMessage;
@@ -28,6 +29,7 @@ class SyncStatusState {
this.remoteSyncStatus = SyncStatus.idle, this.remoteSyncStatus = SyncStatus.idle,
this.localSyncStatus = SyncStatus.idle, this.localSyncStatus = SyncStatus.idle,
this.hashJobStatus = SyncStatus.idle, this.hashJobStatus = SyncStatus.idle,
this.cloudIdSyncStatus = SyncStatus.idle,
this.errorMessage, this.errorMessage,
}); });
@@ -35,12 +37,14 @@ class SyncStatusState {
SyncStatus? remoteSyncStatus, SyncStatus? remoteSyncStatus,
SyncStatus? localSyncStatus, SyncStatus? localSyncStatus,
SyncStatus? hashJobStatus, SyncStatus? hashJobStatus,
SyncStatus? cloudIdSyncStatus,
String? errorMessage, String? errorMessage,
}) { }) {
return SyncStatusState( return SyncStatusState(
remoteSyncStatus: remoteSyncStatus ?? this.remoteSyncStatus, remoteSyncStatus: remoteSyncStatus ?? this.remoteSyncStatus,
localSyncStatus: localSyncStatus ?? this.localSyncStatus, localSyncStatus: localSyncStatus ?? this.localSyncStatus,
hashJobStatus: hashJobStatus ?? this.hashJobStatus, hashJobStatus: hashJobStatus ?? this.hashJobStatus,
cloudIdSyncStatus: cloudIdSyncStatus ?? this.cloudIdSyncStatus,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: errorMessage ?? this.errorMessage,
); );
} }
@@ -48,6 +52,7 @@ class SyncStatusState {
bool get isRemoteSyncing => remoteSyncStatus == SyncStatus.syncing; bool get isRemoteSyncing => remoteSyncStatus == SyncStatus.syncing;
bool get isLocalSyncing => localSyncStatus == SyncStatus.syncing; bool get isLocalSyncing => localSyncStatus == SyncStatus.syncing;
bool get isHashing => hashJobStatus == SyncStatus.syncing; bool get isHashing => hashJobStatus == SyncStatus.syncing;
bool get isCloudIdSyncing => cloudIdSyncStatus == SyncStatus.syncing;
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
@@ -56,11 +61,12 @@ class SyncStatusState {
other.remoteSyncStatus == remoteSyncStatus && other.remoteSyncStatus == remoteSyncStatus &&
other.localSyncStatus == localSyncStatus && other.localSyncStatus == localSyncStatus &&
other.hashJobStatus == hashJobStatus && other.hashJobStatus == hashJobStatus &&
other.cloudIdSyncStatus == cloudIdSyncStatus &&
other.errorMessage == errorMessage; other.errorMessage == errorMessage;
} }
@override @override
int get hashCode => Object.hash(remoteSyncStatus, localSyncStatus, hashJobStatus, errorMessage); int get hashCode => Object.hash(remoteSyncStatus, localSyncStatus, hashJobStatus, cloudIdSyncStatus, errorMessage);
} }
class SyncStatusNotifier extends Notifier<SyncStatusState> { class SyncStatusNotifier extends Notifier<SyncStatusState> {
@@ -71,6 +77,7 @@ class SyncStatusNotifier extends Notifier<SyncStatusState> {
remoteSyncStatus: SyncStatus.idle, remoteSyncStatus: SyncStatus.idle,
localSyncStatus: SyncStatus.idle, localSyncStatus: SyncStatus.idle,
hashJobStatus: SyncStatus.idle, hashJobStatus: SyncStatus.idle,
cloudIdSyncStatus: SyncStatus.idle,
); );
} }
@@ -109,6 +116,18 @@ class SyncStatusNotifier extends Notifier<SyncStatusState> {
void startHashJob() => setHashJobStatus(SyncStatus.syncing); void startHashJob() => setHashJobStatus(SyncStatus.syncing);
void completeHashJob() => setHashJobStatus(SyncStatus.success); void completeHashJob() => setHashJobStatus(SyncStatus.success);
void errorHashJob(String error) => setHashJobStatus(SyncStatus.error, error); void errorHashJob(String error) => setHashJobStatus(SyncStatus.error, error);
///
/// Cloud ID Sync Job
///
void setCloudIdSyncStatus(SyncStatus status, [String? errorMessage]) {
state = state.copyWith(cloudIdSyncStatus: status, errorMessage: status == SyncStatus.error ? errorMessage : null);
}
void startCloudIdSync() => setCloudIdSyncStatus(SyncStatus.syncing);
void completeCloudIdSync() => setCloudIdSyncStatus(SyncStatus.success);
void errorCloudIdSync(String error) => setCloudIdSyncStatus(SyncStatus.error, error);
} }
final syncStatusProvider = NotifierProvider<SyncStatusNotifier, SyncStatusState>(SyncStatusNotifier.new); final syncStatusProvider = NotifierProvider<SyncStatusNotifier, SyncStatusState>(SyncStatusNotifier.new);

View File

@@ -76,7 +76,7 @@ import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart';
import 'package:immich_mobile/pages/search/person_result.page.dart'; import 'package:immich_mobile/pages/search/person_result.page.dart';
import 'package:immich_mobile/pages/search/recently_taken.page.dart'; import 'package:immich_mobile/pages/search/recently_taken.page.dart';
import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart';
import 'package:immich_mobile/pages/settings/sync_status.page.dart'; import 'package:immich_mobile/pages/settings/beta_sync_settings.page.dart';
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart'; import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
@@ -86,7 +86,6 @@ import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_album_options.page.dart'; import 'package:immich_mobile/presentation/pages/drift_album_options.page.dart';
import 'package:immich_mobile/presentation/pages/drift_archive.page.dart'; import 'package:immich_mobile/presentation/pages/drift_archive.page.dart';
import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/drift_asset_selection_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/drift_asset_troubleshoot.page.dart';
import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_create_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_favorite.page.dart'; import 'package:immich_mobile/presentation/pages/drift_favorite.page.dart';
import 'package:immich_mobile/presentation/pages/drift_library.page.dart'; import 'package:immich_mobile/presentation/pages/drift_library.page.dart';
@@ -333,7 +332,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: ChangeExperienceRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: ChangeExperienceRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPartnerRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPartnerRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftUploadDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftUploadDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: SyncStatusRoute.page, guards: [_duplicateGuard]), AutoRoute(page: BetaSyncSettingsRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftPeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPersonRoute.page, guards: [_authGuard]), AutoRoute(page: DriftPersonRoute.page, guards: [_authGuard]),
AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]),
@@ -344,7 +343,6 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DriftFilterImageRoute.page), AutoRoute(page: DriftFilterImageRoute.page),
AutoRoute(page: DriftActivitiesRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftActivitiesRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]),
// required to handle all deeplinks in deep_link.service.dart // required to handle all deeplinks in deep_link.service.dart
// auto_route_library#1722 // auto_route_library#1722
RedirectRoute(path: '*', redirectTo: '/'), RedirectRoute(path: '*', redirectTo: '/'),

View File

@@ -403,43 +403,6 @@ class ArchiveRoute extends PageRouteInfo<void> {
); );
} }
/// generated route for
/// [AssetTroubleshootPage]
class AssetTroubleshootRoute extends PageRouteInfo<AssetTroubleshootRouteArgs> {
AssetTroubleshootRoute({
Key? key,
required BaseAsset asset,
List<PageRouteInfo>? children,
}) : super(
AssetTroubleshootRoute.name,
args: AssetTroubleshootRouteArgs(key: key, asset: asset),
initialChildren: children,
);
static const String name = 'AssetTroubleshootRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
final args = data.argsAs<AssetTroubleshootRouteArgs>();
return AssetTroubleshootPage(key: args.key, asset: args.asset);
},
);
}
class AssetTroubleshootRouteArgs {
const AssetTroubleshootRouteArgs({this.key, required this.asset});
final Key? key;
final BaseAsset asset;
@override
String toString() {
return 'AssetTroubleshootRouteArgs{key: $key, asset: $asset}';
}
}
/// generated route for /// generated route for
/// [AssetViewerPage] /// [AssetViewerPage]
class AssetViewerRoute extends PageRouteInfo<AssetViewerRouteArgs> { class AssetViewerRoute extends PageRouteInfo<AssetViewerRouteArgs> {
@@ -546,6 +509,22 @@ class BackupOptionsRoute extends PageRouteInfo<void> {
); );
} }
/// generated route for
/// [BetaSyncSettingsPage]
class BetaSyncSettingsRoute extends PageRouteInfo<void> {
const BetaSyncSettingsRoute({List<PageRouteInfo>? children})
: super(BetaSyncSettingsRoute.name, initialChildren: children);
static const String name = 'BetaSyncSettingsRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const BetaSyncSettingsPage();
},
);
}
/// generated route for /// generated route for
/// [ChangeExperiencePage] /// [ChangeExperiencePage]
class ChangeExperienceRoute extends PageRouteInfo<ChangeExperienceRouteArgs> { class ChangeExperienceRoute extends PageRouteInfo<ChangeExperienceRouteArgs> {
@@ -2650,22 +2629,6 @@ class SplashScreenRoute extends PageRouteInfo<void> {
); );
} }
/// generated route for
/// [SyncStatusPage]
class SyncStatusRoute extends PageRouteInfo<void> {
const SyncStatusRoute({List<PageRouteInfo>? children})
: super(SyncStatusRoute.name, initialChildren: children);
static const String name = 'SyncStatusRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const SyncStatusPage();
},
);
}
/// generated route for /// generated route for
/// [TabControllerPage] /// [TabControllerPage]
class TabControllerRoute extends PageRouteInfo<void> { class TabControllerRoute extends PageRouteInfo<void> {

View File

@@ -46,7 +46,7 @@ enum AppSettingsEnum<T> {
syncAlbums<bool>(StoreKey.syncAlbums, null, false), syncAlbums<bool>(StoreKey.syncAlbums, null, false),
autoEndpointSwitching<bool>(StoreKey.autoEndpointSwitching, null, false), autoEndpointSwitching<bool>(StoreKey.autoEndpointSwitching, null, false),
photoManagerCustomFilter<bool>(StoreKey.photoManagerCustomFilter, null, true), photoManagerCustomFilter<bool>(StoreKey.photoManagerCustomFilter, null, true),
betaTimeline<bool>(StoreKey.betaTimeline, null, true), betaTimeline<bool>(StoreKey.betaTimeline, null, false),
enableBackup<bool>(StoreKey.enableBackup, null, false), enableBackup<bool>(StoreKey.enableBackup, null, false),
useCellularForUploadVideos<bool>(StoreKey.useWifiForUploadVideos, null, false), useCellularForUploadVideos<bool>(StoreKey.useWifiForUploadVideos, null, false),
useCellularForUploadPhotos<bool>(StoreKey.useWifiForUploadPhotos, null, false), useCellularForUploadPhotos<bool>(StoreKey.useWifiForUploadPhotos, null, false),

View File

@@ -6,6 +6,7 @@ import 'package:background_downloader/background_downloader.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/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
@@ -291,6 +292,7 @@ class UploadService {
priority: priority, priority: priority,
isFavorite: asset.isFavorite, isFavorite: asset.isFavorite,
requiresWiFi: requiresWiFi, requiresWiFi: requiresWiFi,
cloudId: asset.cloudId,
); );
} }
@@ -320,6 +322,7 @@ class UploadService {
priority: 0, // Highest priority to get upload immediately priority: 0, // Highest priority to get upload immediately
isFavorite: asset.isFavorite, isFavorite: asset.isFavorite,
requiresWiFi: requiresWiFi, requiresWiFi: requiresWiFi,
cloudId: asset.cloudId,
); );
} }
@@ -346,6 +349,7 @@ class UploadService {
String? metadata, String? metadata,
int? priority, int? priority,
bool? isFavorite, bool? isFavorite,
String? cloudId,
bool requiresWiFi = true, bool requiresWiFi = true,
}) async { }) async {
final serverEndpoint = Store.get(StoreKey.serverEndpoint); final serverEndpoint = Store.get(StoreKey.serverEndpoint);
@@ -361,6 +365,12 @@ class UploadService {
'fileModifiedAt': modifiedAt.toUtc().toIso8601String(), 'fileModifiedAt': modifiedAt.toUtc().toIso8601String(),
'isFavorite': isFavorite?.toString() ?? 'false', 'isFavorite': isFavorite?.toString() ?? 'false',
'duration': '0', 'duration': '0',
'metadata': jsonEncode([
RemoteAssetMetadataItem(
key: RemoteAssetMetadataKey.mobileApp,
value: RemoteAssetMobileAppMetadata(cloudId: cloudId),
),
]),
if (fields != null) ...fields, if (fields != null) ...fields,
}; };

View File

@@ -1,8 +1,6 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
@@ -17,6 +15,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
class ActionButtonContext { class ActionButtonContext {
final BaseAsset asset; final BaseAsset asset;
@@ -25,7 +24,6 @@ class ActionButtonContext {
final bool isTrashEnabled; final bool isTrashEnabled;
final bool isInLockedView; final bool isInLockedView;
final RemoteAlbum? currentAlbum; final RemoteAlbum? currentAlbum;
final bool advancedTroubleshooting;
final ActionSource source; final ActionSource source;
const ActionButtonContext({ const ActionButtonContext({
@@ -35,13 +33,11 @@ class ActionButtonContext {
required this.isTrashEnabled, required this.isTrashEnabled,
required this.isInLockedView, required this.isInLockedView,
required this.currentAlbum, required this.currentAlbum,
required this.advancedTroubleshooting,
required this.source, required this.source,
}); });
} }
enum ActionButtonType { enum ActionButtonType {
advancedInfo,
share, share,
shareLink, shareLink,
archive, archive,
@@ -59,7 +55,6 @@ enum ActionButtonType {
bool shouldShow(ActionButtonContext context) { bool shouldShow(ActionButtonContext context) {
return switch (this) { return switch (this) {
ActionButtonType.advancedInfo => context.advancedTroubleshooting,
ActionButtonType.share => true, ActionButtonType.share => true,
ActionButtonType.shareLink => ActionButtonType.shareLink =>
!context.isInLockedView && // !context.isInLockedView && //
@@ -120,7 +115,6 @@ enum ActionButtonType {
Widget buildButton(ActionButtonContext context) { Widget buildButton(ActionButtonContext context) {
return switch (this) { return switch (this) {
ActionButtonType.advancedInfo => AdvancedInfoActionButton(source: context.source),
ActionButtonType.share => ShareActionButton(source: context.source), ActionButtonType.share => ShareActionButton(source: context.source),
ActionButtonType.shareLink => ShareLinkActionButton(source: context.source), ActionButtonType.shareLink => ShareLinkActionButton(source: context.source),
ActionButtonType.archive => ArchiveActionButton(source: context.source), ActionButtonType.archive => ArchiveActionButton(source: context.source),
@@ -144,7 +138,6 @@ enum ActionButtonType {
class ActionButtonBuilder { class ActionButtonBuilder {
static const List<ActionButtonType> _actionTypes = [ static const List<ActionButtonType> _actionTypes = [
ActionButtonType.advancedInfo,
ActionButtonType.share, ActionButtonType.share,
ActionButtonType.shareLink, ActionButtonType.shareLink,
ActionButtonType.likeActivity, ActionButtonType.likeActivity,

View File

@@ -90,7 +90,7 @@ abstract final class Bootstrap {
} }
static Future<void> initDomain(Isar db, Drift drift, DriftLogger logDb, {bool shouldBufferLogs = true}) async { static Future<void> initDomain(Isar db, Drift drift, DriftLogger logDb, {bool shouldBufferLogs = true}) async {
final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? true; final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? false;
final IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db); final IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db);
await StoreService.init(storeRepository: storeRepo); await StoreService.init(storeRepository: storeRepo);

View File

@@ -29,15 +29,15 @@ import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
// ignore: import_rule_photo_manager // ignore: import_rule_photo_manager
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
const int targetVersion = 15; const int targetVersion = 14;
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async { Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
final hasVersion = Store.tryGet(StoreKey.version) != null; final hasVersion = Store.tryGet(StoreKey.version) != null;
final int version = Store.get(StoreKey.version, targetVersion); final int version = Store.get(StoreKey.version, targetVersion);
if (version < 9) { if (version < 9) {
await Store.put(StoreKey.version, targetVersion); await Store.put(StoreKey.version, targetVersion);
final value = await db.storeValues.get(StoreKey.currentUser.id); final value = await db.storeValues.get(StoreKey.currentUser.id);
@@ -67,22 +67,6 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
await Store.populateCache(); await Store.populateCache();
} }
// Handle migration only for this version
// TODO: remove when old timeline is removed
if (version == 15) {
final isBeta = Store.tryGet(StoreKey.betaTimeline);
final isNewInstallation = await _isNewInstallation(db, drift);
// For new installations, no migration needed
// For existing installations, only migrate if beta timeline is not enabled (null or false)
if (isNewInstallation || isBeta == true) {
await Store.put(StoreKey.needBetaMigration, false);
} else {
await resetDriftDatabase(drift);
await Store.put(StoreKey.needBetaMigration, true);
}
}
if (targetVersion >= 12) { if (targetVersion >= 12) {
await Store.put(StoreKey.version, targetVersion); await Store.put(StoreKey.version, targetVersion);
return; return;
@@ -95,35 +79,6 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
} }
} }
Future<bool> _isNewInstallation(Isar db, Drift drift) async {
try {
final isarUserCount = await db.users.count();
if (isarUserCount > 0) {
return false;
}
final isarAssetCount = await db.assets.count();
if (isarAssetCount > 0) {
return false;
}
final driftStoreCount = await drift.storeEntity.select().get().then((list) => list.length);
if (driftStoreCount > 0) {
return false;
}
final driftAssetCount = await drift.localAssetEntity.select().get().then((list) => list.length);
if (driftAssetCount > 0) {
return false;
}
return true;
} catch (error) {
debugPrint("[MIGRATION] Error checking if new installation: $error");
return false;
}
}
Future<void> _migrateTo(Isar db, int version) async { Future<void> _migrateTo(Isar db, int version) async {
await Store.delete(StoreKey.assetETag); await Store.delete(StoreKey.assetETag);
await db.writeTxn(() async { await db.writeTxn(() async {
@@ -310,45 +265,16 @@ class _DeviceAsset {
const _DeviceAsset({required this.assetId, this.hash, this.dateTime}); const _DeviceAsset({required this.assetId, this.hash, this.dateTime});
} }
Future<List<void>> runNewSync(WidgetRef ref, {bool full = false}) { Future<void> runNewSync(WidgetRef ref, {bool full = false}) {
ref.read(backupProvider.notifier).cancelBackup(); ref.read(backupProvider.notifier).cancelBackup();
final backgroundManager = ref.read(backgroundSyncProvider); final backgroundManager = ref.read(backgroundSyncProvider);
final isAlbumLinkedSyncEnable = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); final isAlbumLinkedSyncEnable = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
return Future.wait([ return (backgroundManager.syncLocal(full: full), backgroundManager.syncRemote()).wait.whenComplete(() async {
backgroundManager.syncLocal(full: full).then((_) { await backgroundManager.hashAssets();
Logger("runNewSync").fine("Hashing assets after syncLocal"); if (isAlbumLinkedSyncEnable) {
return backgroundManager.hashAssets(); await backgroundManager.syncLinkedAlbum();
}), }
backgroundManager.syncRemote().then((_) {
if (isAlbumLinkedSyncEnable) {
return backgroundManager.syncLinkedAlbum();
}
}),
]);
}
Future<void> resetDriftDatabase(Drift drift) async {
// https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94
final database = drift.attachedDatabase;
await database.exclusively(() async {
// https://stackoverflow.com/a/65743498/25690041
await database.customStatement('PRAGMA writable_schema = 1;');
await database.customStatement('DELETE FROM sqlite_master;');
await database.customStatement('VACUUM;');
await database.customStatement('PRAGMA writable_schema = 0;');
await database.customStatement('PRAGMA integrity_check');
await database.customStatement('PRAGMA user_version = 0');
await database.beforeOpen(
// ignore: invalid_use_of_internal_member
database.resolvedEngine.executor,
OpeningDetails(null, database.schemaVersion),
);
await database.customStatement('PRAGMA user_version = ${database.schemaVersion}');
// Refresh all stream queries
database.notifyUpdates({for (final table in database.allTables) TableUpdate.onTable(table)});
}); });
} }

View File

@@ -90,11 +90,11 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
minLeadingWidth: 50, minLeadingWidth: 50,
leading: GestureDetector( leading: GestureDetector(
onTap: pickUserProfileImage, onTap: pickUserProfileImage,
onLongPress: toggleReadonlyMode, onDoubleTap: toggleReadonlyMode,
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
AbsorbPointer(child: buildUserProfileImage()), buildUserProfileImage(),
if (!isReadonlyModeEnabled) if (!isReadonlyModeEnabled)
Positioned( Positioned(
bottom: -5, bottom: -5,

View File

@@ -157,7 +157,7 @@ class _ProfileIndicator extends ConsumerWidget {
return InkWell( return InkWell(
onTap: () => showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()), onTap: () => showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()),
onLongPress: () => toggleReadonlyMode(), onDoubleTap: () => toggleReadonlyMode(),
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
child: Badge( child: Badge(
label: Container( label: Container(
@@ -173,7 +173,7 @@ class _ProfileIndicator extends ConsumerWidget {
? const Icon(Icons.face_outlined, size: widgetSize) ? const Icon(Icons.face_outlined, size: widgetSize)
: Semantics( : Semantics(
label: "logged_in_as".tr(namedArgs: {"user": user.name}), label: "logged_in_as".tr(namedArgs: {"user": user.name}),
child: AbsorbPointer(child: UserCircleAvatar(radius: 17, size: 31, user: user)), child: UserCircleAvatar(radius: 17, size: 31, user: user),
), ),
), ),
); );

View File

@@ -0,0 +1,387 @@
import 'dart:io';
import 'package:drift/drift.dart' as drift_db;
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/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart';
import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
class BetaSyncSettings extends HookConsumerWidget {
const BetaSyncSettings({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetService = ref.watch(assetServiceProvider);
final localAlbumService = ref.watch(localAlbumServiceProvider);
final remoteAlbumService = ref.watch(remoteAlbumServiceProvider);
final memoryService = ref.watch(driftMemoryServiceProvider);
Future<List<dynamic>> loadCounts() async {
final assetCounts = assetService.getAssetCounts();
final localAlbumCounts = localAlbumService.getCount();
final remoteAlbumCounts = remoteAlbumService.getCount();
final memoryCount = memoryService.getCount();
final getLocalHashedCount = assetService.getLocalHashedCount();
return await Future.wait([assetCounts, localAlbumCounts, remoteAlbumCounts, memoryCount, getLocalHashedCount]);
}
Future<void> resetDatabase() async {
// https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94
final drift = ref.read(driftProvider);
final database = drift.attachedDatabase;
await database.exclusively(() async {
// https://stackoverflow.com/a/65743498/25690041
await database.customStatement('PRAGMA writable_schema = 1;');
await database.customStatement('DELETE FROM sqlite_master;');
await database.customStatement('VACUUM;');
await database.customStatement('PRAGMA writable_schema = 0;');
await database.customStatement('PRAGMA integrity_check');
await database.customStatement('PRAGMA user_version = 0');
await database.beforeOpen(
// ignore: invalid_use_of_internal_member
database.resolvedEngine.executor,
drift_db.OpeningDetails(null, database.schemaVersion),
);
await database.customStatement('PRAGMA user_version = ${database.schemaVersion}');
// Refresh all stream queries
database.notifyUpdates({for (final table in database.allTables) drift_db.TableUpdate.onTable(table)});
});
}
Future<void> exportDatabase() async {
try {
// WAL Checkpoint to ensure all changes are written to the database
await ref.read(driftProvider).customStatement("pragma wal_checkpoint(truncate)");
final documentsDir = await getApplicationDocumentsDirectory();
final dbFile = File(path.join(documentsDir.path, 'immich.sqlite'));
if (!await dbFile.exists()) {
if (context.mounted) {
context.scaffoldMessenger.showSnackBar(
SnackBar(content: Text("Database file not found".t(context: context))),
);
}
return;
}
final timestamp = DateTime.now().millisecondsSinceEpoch;
final exportFile = File(path.join(documentsDir.path, 'immich_export_$timestamp.sqlite'));
await dbFile.copy(exportFile.path);
await Share.shareXFiles([XFile(exportFile.path)], text: 'Immich Database Export');
Future.delayed(const Duration(seconds: 30), () async {
if (await exportFile.exists()) {
await exportFile.delete();
}
});
if (context.mounted) {
context.scaffoldMessenger.showSnackBar(
SnackBar(content: Text("Database exported successfully".t(context: context))),
);
}
} catch (e) {
if (context.mounted) {
context.scaffoldMessenger.showSnackBar(
SnackBar(content: Text("Failed to export database: $e".t(context: context))),
);
}
}
}
Future<void> clearFileCache() async {
await ref.read(storageRepositoryProvider).clearCache();
}
Future<void> resetSqliteDb(BuildContext context, Future<void> Function() resetDatabase) {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("reset_sqlite".t(context: context)),
content: Text("reset_sqlite_confirmation".t(context: context)),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text("cancel".t(context: context)),
),
TextButton(
onPressed: () async {
await resetDatabase();
context.pop();
context.scaffoldMessenger.showSnackBar(
SnackBar(content: Text("reset_sqlite_success".t(context: context))),
);
},
child: Text(
"confirm".t(context: context),
style: TextStyle(color: context.colorScheme.error),
),
),
],
);
},
);
}
return FutureBuilder<List<dynamic>>(
future: loadCounts(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return ListView(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Text(
"Error occur, reset the local database by tapping the button below",
style: context.textTheme.bodyLarge,
),
),
),
ListTile(
title: Text(
"reset_sqlite".t(context: context),
style: TextStyle(color: context.colorScheme.error, fontWeight: FontWeight.w500),
),
leading: Icon(Icons.settings_backup_restore_rounded, color: context.colorScheme.error),
onTap: () async {
await resetSqliteDb(context, resetDatabase);
},
),
],
);
}
final assetCounts = snapshot.data![0]! as (int, int);
final localAssetCount = assetCounts.$1;
final remoteAssetCount = assetCounts.$2;
final localAlbumCount = snapshot.data![1]! as int;
final remoteAlbumCount = snapshot.data![2]! as int;
final memoryCount = snapshot.data![3]! as int;
final localHashedCount = snapshot.data![4]! as int;
return Padding(
padding: const EdgeInsets.only(top: 16, bottom: 32),
child: ListView(
children: [
_SectionHeaderText(text: "assets".t(context: context)),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex(
direction: Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 8.0,
children: [
Expanded(
child: EntitiyCountTile(
label: "local".t(context: context),
count: localAssetCount,
icon: Icons.smartphone,
),
),
Expanded(
child: EntitiyCountTile(
label: "remote".t(context: context),
count: remoteAssetCount,
icon: Icons.cloud,
),
),
],
),
),
_SectionHeaderText(text: "albums".t(context: context)),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex(
direction: Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 8.0,
children: [
Expanded(
child: EntitiyCountTile(
label: "local".t(context: context),
count: localAlbumCount,
icon: Icons.smartphone,
),
),
Expanded(
child: EntitiyCountTile(
label: "remote".t(context: context),
count: remoteAlbumCount,
icon: Icons.cloud,
),
),
],
),
),
_SectionHeaderText(text: "other".t(context: context)),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex(
direction: Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 8.0,
children: [
Expanded(
child: EntitiyCountTile(
label: "memories".t(context: context),
count: memoryCount,
icon: Icons.calendar_today,
),
),
Expanded(
child: EntitiyCountTile(
label: "hashed_assets".t(context: context),
count: localHashedCount,
icon: Icons.tag,
),
),
],
),
),
const Divider(height: 1, indent: 16, endIndent: 16),
const SizedBox(height: 24),
_SectionHeaderText(text: "jobs".t(context: context)),
ListTile(
title: Text(
"sync_local".t(context: context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text("tap_to_run_job".t(context: context)),
leading: const Icon(Icons.sync),
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).localSyncStatus),
onTap: () {
ref.read(backgroundSyncProvider).syncLocal(full: true);
},
),
ListTile(
title: Text(
"sync_remote".t(context: context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text("tap_to_run_job".t(context: context)),
leading: const Icon(Icons.cloud_sync),
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).remoteSyncStatus),
onTap: () {
ref.read(backgroundSyncProvider).syncRemote();
},
),
ListTile(
title: Text(
"hash_asset".t(context: context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
leading: const Icon(Icons.tag),
subtitle: Text("tap_to_run_job".t(context: context)),
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).hashJobStatus),
onTap: () {
ref.read(backgroundSyncProvider).hashAssets();
},
),
if (Platform.isIOS)
ListTile(
title: Text(
"sync_cloud_ids".t(context: context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
leading: const Icon(Icons.keyboard_command_key_rounded),
subtitle: Text("tap_to_run_job".t(context: context)),
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).cloudIdSyncStatus),
onTap: ref.read(backgroundSyncProvider).syncCloudIds,
),
const Divider(height: 1, indent: 16, endIndent: 16),
const SizedBox(height: 24),
_SectionHeaderText(text: "actions".t(context: context)),
ListTile(
title: Text(
"clear_file_cache".t(context: context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
leading: const Icon(Icons.playlist_remove_rounded),
onTap: clearFileCache,
),
ListTile(
title: Text(
"export_database".t(context: context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text("export_database_description".t(context: context)),
leading: const Icon(Icons.download),
onTap: exportDatabase,
),
ListTile(
title: Text(
"reset_sqlite".t(context: context),
style: TextStyle(color: context.colorScheme.error, fontWeight: FontWeight.w500),
),
leading: Icon(Icons.settings_backup_restore_rounded, color: context.colorScheme.error),
onTap: () async {
await resetSqliteDb(context, resetDatabase);
},
),
],
),
);
},
);
}
}
class _SyncStatusIcon extends StatelessWidget {
final SyncStatus status;
const _SyncStatusIcon({required this.status});
@override
Widget build(BuildContext context) {
return switch (status) {
SyncStatus.idle => const SizedBox.shrink(),
SyncStatus.syncing => const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 2)),
SyncStatus.success => const Icon(Icons.check_circle_outline, color: Colors.green),
SyncStatus.error => Icon(Icons.error_outline, color: context.colorScheme.error),
};
}
}
class _SectionHeaderText extends StatelessWidget {
final String text;
const _SectionHeaderText({required this.text});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Text(
text.toUpperCase(),
style: context.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w500,
color: context.colorScheme.onSurface.withAlpha(200),
),
),
);
}
}

View File

@@ -1,353 +0,0 @@
import 'dart:io';
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/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart';
import 'package:immich_mobile/utils/migration.dart';
import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
class SyncStatusAndActions extends HookConsumerWidget {
const SyncStatusAndActions({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
Future<void> exportDatabase() async {
try {
// WAL Checkpoint to ensure all changes are written to the database
await ref.read(driftProvider).customStatement("pragma wal_checkpoint(truncate)");
final documentsDir = await getApplicationDocumentsDirectory();
final dbFile = File(path.join(documentsDir.path, 'immich.sqlite'));
if (!await dbFile.exists()) {
if (context.mounted) {
context.scaffoldMessenger.showSnackBar(
SnackBar(content: Text("Database file not found".t(context: context))),
);
}
return;
}
final timestamp = DateTime.now().millisecondsSinceEpoch;
final exportFile = File(path.join(documentsDir.path, 'immich_export_$timestamp.sqlite'));
await dbFile.copy(exportFile.path);
await Share.shareXFiles([XFile(exportFile.path)], text: 'Immich Database Export');
Future.delayed(const Duration(seconds: 30), () async {
if (await exportFile.exists()) {
await exportFile.delete();
}
});
if (context.mounted) {
context.scaffoldMessenger.showSnackBar(
SnackBar(content: Text("Database exported successfully".t(context: context))),
);
}
} catch (e) {
if (context.mounted) {
context.scaffoldMessenger.showSnackBar(
SnackBar(content: Text("Failed to export database: $e".t(context: context))),
);
}
}
}
Future<void> clearFileCache() async {
await ref.read(storageRepositoryProvider).clearCache();
}
Future<void> resetSqliteDb(BuildContext context) {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("reset_sqlite".t(context: context)),
content: Text("reset_sqlite_confirmation".t(context: context)),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text("cancel".t(context: context)),
),
TextButton(
onPressed: () async {
await resetDriftDatabase(ref.read(driftProvider));
context.pop();
context.scaffoldMessenger.showSnackBar(
SnackBar(content: Text("reset_sqlite_success".t(context: context))),
);
},
child: Text(
"confirm".t(context: context),
style: TextStyle(color: context.colorScheme.error),
),
),
],
);
},
);
}
return Padding(
padding: const EdgeInsets.only(top: 16, bottom: 32),
child: ListView(
children: [
_SectionHeaderText(text: "assets".t(context: context)),
const _SyncStatsCounts(),
const Divider(height: 1, indent: 16, endIndent: 16),
const SizedBox(height: 24),
_SectionHeaderText(text: "jobs".t(context: context)),
ListTile(
title: Text(
"sync_local".t(context: context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text("tap_to_run_job".t(context: context)),
leading: const Icon(Icons.sync),
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).localSyncStatus),
onTap: () {
ref.read(backgroundSyncProvider).syncLocal(full: true);
},
),
ListTile(
title: Text(
"sync_remote".t(context: context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text("tap_to_run_job".t(context: context)),
leading: const Icon(Icons.cloud_sync),
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).remoteSyncStatus),
onTap: () {
ref.read(backgroundSyncProvider).syncRemote();
},
),
ListTile(
title: Text(
"hash_asset".t(context: context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
leading: const Icon(Icons.tag),
subtitle: Text("tap_to_run_job".t(context: context)),
trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).hashJobStatus),
onTap: () {
ref.read(backgroundSyncProvider).hashAssets();
},
),
const Divider(height: 1, indent: 16, endIndent: 16),
const SizedBox(height: 24),
_SectionHeaderText(text: "actions".t(context: context)),
ListTile(
title: Text(
"clear_file_cache".t(context: context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
leading: const Icon(Icons.playlist_remove_rounded),
onTap: clearFileCache,
),
ListTile(
title: Text(
"export_database".t(context: context),
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text("export_database_description".t(context: context)),
leading: const Icon(Icons.download),
onTap: exportDatabase,
),
ListTile(
title: Text(
"reset_sqlite".t(context: context),
style: TextStyle(color: context.colorScheme.error, fontWeight: FontWeight.w500),
),
leading: Icon(Icons.settings_backup_restore_rounded, color: context.colorScheme.error),
onTap: () async {
await resetSqliteDb(context);
},
),
],
),
);
}
}
class _SyncStatusIcon extends StatelessWidget {
final SyncStatus status;
const _SyncStatusIcon({required this.status});
@override
Widget build(BuildContext context) {
return switch (status) {
SyncStatus.idle => const Icon(Icons.pause_circle_outline_rounded),
SyncStatus.syncing => const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 2)),
SyncStatus.success => const Icon(Icons.check_circle_outline, color: Colors.green),
SyncStatus.error => Icon(Icons.error_outline, color: context.colorScheme.error),
};
}
}
class _SectionHeaderText extends StatelessWidget {
final String text;
const _SectionHeaderText({required this.text});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Text(
text.toUpperCase(),
style: context.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w500,
color: context.colorScheme.onSurface.withAlpha(200),
),
),
);
}
}
class _SyncStatsCounts extends ConsumerWidget {
const _SyncStatsCounts();
@override
Widget build(BuildContext context, WidgetRef ref) {
final assetService = ref.watch(assetServiceProvider);
final localAlbumService = ref.watch(localAlbumServiceProvider);
final remoteAlbumService = ref.watch(remoteAlbumServiceProvider);
final memoryService = ref.watch(driftMemoryServiceProvider);
Future<List<dynamic>> loadCounts() async {
final assetCounts = assetService.getAssetCounts();
final localAlbumCounts = localAlbumService.getCount();
final remoteAlbumCounts = remoteAlbumService.getCount();
final memoryCount = memoryService.getCount();
final getLocalHashedCount = assetService.getLocalHashedCount();
return await Future.wait([assetCounts, localAlbumCounts, remoteAlbumCounts, memoryCount, getLocalHashedCount]);
}
return FutureBuilder(
future: loadCounts(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(child: SizedBox(height: 48, width: 48, child: CircularProgressIndicator()));
}
if (snapshot.hasError) {
return ListView(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Text(
"Error occur, reset the local database by tapping the button below",
style: context.textTheme.bodyLarge,
),
),
),
],
);
}
final assetCounts = snapshot.data![0]! as (int, int);
final localAssetCount = assetCounts.$1;
final remoteAssetCount = assetCounts.$2;
final localAlbumCount = snapshot.data![1]! as int;
final remoteAlbumCount = snapshot.data![2]! as int;
final memoryCount = snapshot.data![3]! as int;
final localHashedCount = snapshot.data![4]! as int;
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex(
direction: Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 8.0,
children: [
Expanded(
child: EntitiyCountTile(
label: "local".t(context: context),
count: localAssetCount,
icon: Icons.smartphone,
),
),
Expanded(
child: EntitiyCountTile(
label: "remote".t(context: context),
count: remoteAssetCount,
icon: Icons.cloud,
),
),
],
),
),
_SectionHeaderText(text: "albums".t(context: context)),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex(
direction: Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 8.0,
children: [
Expanded(
child: EntitiyCountTile(
label: "local".t(context: context),
count: localAlbumCount,
icon: Icons.smartphone,
),
),
Expanded(
child: EntitiyCountTile(
label: "remote".t(context: context),
count: remoteAlbumCount,
icon: Icons.cloud,
),
),
],
),
),
_SectionHeaderText(text: "other".t(context: context)),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex(
direction: Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 8.0,
children: [
Expanded(
child: EntitiyCountTile(
label: "memories".t(context: context),
count: memoryCount,
icon: Icons.calendar_today,
),
),
Expanded(
child: EntitiyCountTile(
label: "hashed_assets".t(context: context),
count: localHashedCount,
icon: Icons.tag,
),
),
],
),
),
],
);
},
);
}
}

View File

@@ -53,15 +53,12 @@ class TimelineApi {
/// * [AssetVisibility] visibility: /// * [AssetVisibility] visibility:
/// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED) /// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)
/// ///
/// * [bool] withCoordinates:
/// Include location data in the response
///
/// * [bool] withPartners: /// * [bool] withPartners:
/// Include assets shared by partners /// Include assets shared by partners
/// ///
/// * [bool] withStacked: /// * [bool] withStacked:
/// Include stacked assets in the response. When true, only primary assets from stacks are returned. /// Include stacked assets in the response. When true, only primary assets from stacks are returned.
Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/timeline/bucket'; final apiPath = r'/timeline/bucket';
@@ -103,9 +100,6 @@ class TimelineApi {
if (visibility != null) { if (visibility != null) {
queryParams.addAll(_queryParams('', 'visibility', visibility)); queryParams.addAll(_queryParams('', 'visibility', visibility));
} }
if (withCoordinates != null) {
queryParams.addAll(_queryParams('', 'withCoordinates', withCoordinates));
}
if (withPartners != null) { if (withPartners != null) {
queryParams.addAll(_queryParams('', 'withPartners', withPartners)); queryParams.addAll(_queryParams('', 'withPartners', withPartners));
} }
@@ -162,16 +156,13 @@ class TimelineApi {
/// * [AssetVisibility] visibility: /// * [AssetVisibility] visibility:
/// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED) /// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)
/// ///
/// * [bool] withCoordinates:
/// Include location data in the response
///
/// * [bool] withPartners: /// * [bool] withPartners:
/// Include assets shared by partners /// Include assets shared by partners
/// ///
/// * [bool] withStacked: /// * [bool] withStacked:
/// Include stacked assets in the response. When true, only primary assets from stacks are returned. /// Include stacked assets in the response. When true, only primary assets from stacks are returned.
Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, ); final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
@@ -219,15 +210,12 @@ class TimelineApi {
/// * [AssetVisibility] visibility: /// * [AssetVisibility] visibility:
/// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED) /// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)
/// ///
/// * [bool] withCoordinates:
/// Include location data in the response
///
/// * [bool] withPartners: /// * [bool] withPartners:
/// Include assets shared by partners /// Include assets shared by partners
/// ///
/// * [bool] withStacked: /// * [bool] withStacked:
/// Include stacked assets in the response. When true, only primary assets from stacks are returned. /// Include stacked assets in the response. When true, only primary assets from stacks are returned.
Future<Response> getTimeBucketsWithHttpInfo({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { Future<Response> getTimeBucketsWithHttpInfo({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/timeline/buckets'; final apiPath = r'/timeline/buckets';
@@ -268,9 +256,6 @@ class TimelineApi {
if (visibility != null) { if (visibility != null) {
queryParams.addAll(_queryParams('', 'visibility', visibility)); queryParams.addAll(_queryParams('', 'visibility', visibility));
} }
if (withCoordinates != null) {
queryParams.addAll(_queryParams('', 'withCoordinates', withCoordinates));
}
if (withPartners != null) { if (withPartners != null) {
queryParams.addAll(_queryParams('', 'withPartners', withPartners)); queryParams.addAll(_queryParams('', 'withPartners', withPartners));
} }
@@ -324,16 +309,13 @@ class TimelineApi {
/// * [AssetVisibility] visibility: /// * [AssetVisibility] visibility:
/// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED) /// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)
/// ///
/// * [bool] withCoordinates:
/// Include location data in the response
///
/// * [bool] withPartners: /// * [bool] withPartners:
/// Include assets shared by partners /// Include assets shared by partners
/// ///
/// * [bool] withStacked: /// * [bool] withStacked:
/// Include stacked assets in the response. When true, only primary assets from stacks are returned. /// Include stacked assets in the response. When true, only primary assets from stacks are returned.
Future<List<TimeBucketsResponseDto>?> getTimeBuckets({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async { Future<List<TimeBucketsResponseDto>?> getTimeBuckets({ String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketsWithHttpInfo( albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, ); final response = await getTimeBucketsWithHttpInfo( albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }

View File

@@ -21,10 +21,8 @@ class TimeBucketAssetResponseDto {
this.isFavorite = const [], this.isFavorite = const [],
this.isImage = const [], this.isImage = const [],
this.isTrashed = const [], this.isTrashed = const [],
this.latitude = const [],
this.livePhotoVideoId = const [], this.livePhotoVideoId = const [],
this.localOffsetHours = const [], this.localOffsetHours = const [],
this.longitude = const [],
this.ownerId = const [], this.ownerId = const [],
this.projectionType = const [], this.projectionType = const [],
this.ratio = const [], this.ratio = const [],
@@ -57,18 +55,12 @@ class TimeBucketAssetResponseDto {
/// Array indicating whether each asset is in the trash /// Array indicating whether each asset is in the trash
List<bool> isTrashed; List<bool> isTrashed;
/// Array of latitude coordinates extracted from EXIF GPS data
List<num?> latitude;
/// Array of live photo video asset IDs (null for non-live photos) /// Array of live photo video asset IDs (null for non-live photos)
List<String?> livePhotoVideoId; List<String?> livePhotoVideoId;
/// Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective. /// Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.
List<num> localOffsetHours; List<num> localOffsetHours;
/// Array of longitude coordinates extracted from EXIF GPS data
List<num?> longitude;
/// Array of owner IDs for each asset /// Array of owner IDs for each asset
List<String> ownerId; List<String> ownerId;
@@ -97,10 +89,8 @@ class TimeBucketAssetResponseDto {
_deepEquality.equals(other.isFavorite, isFavorite) && _deepEquality.equals(other.isFavorite, isFavorite) &&
_deepEquality.equals(other.isImage, isImage) && _deepEquality.equals(other.isImage, isImage) &&
_deepEquality.equals(other.isTrashed, isTrashed) && _deepEquality.equals(other.isTrashed, isTrashed) &&
_deepEquality.equals(other.latitude, latitude) &&
_deepEquality.equals(other.livePhotoVideoId, livePhotoVideoId) && _deepEquality.equals(other.livePhotoVideoId, livePhotoVideoId) &&
_deepEquality.equals(other.localOffsetHours, localOffsetHours) && _deepEquality.equals(other.localOffsetHours, localOffsetHours) &&
_deepEquality.equals(other.longitude, longitude) &&
_deepEquality.equals(other.ownerId, ownerId) && _deepEquality.equals(other.ownerId, ownerId) &&
_deepEquality.equals(other.projectionType, projectionType) && _deepEquality.equals(other.projectionType, projectionType) &&
_deepEquality.equals(other.ratio, ratio) && _deepEquality.equals(other.ratio, ratio) &&
@@ -119,10 +109,8 @@ class TimeBucketAssetResponseDto {
(isFavorite.hashCode) + (isFavorite.hashCode) +
(isImage.hashCode) + (isImage.hashCode) +
(isTrashed.hashCode) + (isTrashed.hashCode) +
(latitude.hashCode) +
(livePhotoVideoId.hashCode) + (livePhotoVideoId.hashCode) +
(localOffsetHours.hashCode) + (localOffsetHours.hashCode) +
(longitude.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
(projectionType.hashCode) + (projectionType.hashCode) +
(ratio.hashCode) + (ratio.hashCode) +
@@ -131,7 +119,7 @@ class TimeBucketAssetResponseDto {
(visibility.hashCode); (visibility.hashCode);
@override @override
String toString() => 'TimeBucketAssetResponseDto[city=$city, country=$country, duration=$duration, fileCreatedAt=$fileCreatedAt, id=$id, isFavorite=$isFavorite, isImage=$isImage, isTrashed=$isTrashed, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, localOffsetHours=$localOffsetHours, longitude=$longitude, ownerId=$ownerId, projectionType=$projectionType, ratio=$ratio, stack=$stack, thumbhash=$thumbhash, visibility=$visibility]'; String toString() => 'TimeBucketAssetResponseDto[city=$city, country=$country, duration=$duration, fileCreatedAt=$fileCreatedAt, id=$id, isFavorite=$isFavorite, isImage=$isImage, isTrashed=$isTrashed, livePhotoVideoId=$livePhotoVideoId, localOffsetHours=$localOffsetHours, ownerId=$ownerId, projectionType=$projectionType, ratio=$ratio, stack=$stack, thumbhash=$thumbhash, visibility=$visibility]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -143,10 +131,8 @@ class TimeBucketAssetResponseDto {
json[r'isFavorite'] = this.isFavorite; json[r'isFavorite'] = this.isFavorite;
json[r'isImage'] = this.isImage; json[r'isImage'] = this.isImage;
json[r'isTrashed'] = this.isTrashed; json[r'isTrashed'] = this.isTrashed;
json[r'latitude'] = this.latitude;
json[r'livePhotoVideoId'] = this.livePhotoVideoId; json[r'livePhotoVideoId'] = this.livePhotoVideoId;
json[r'localOffsetHours'] = this.localOffsetHours; json[r'localOffsetHours'] = this.localOffsetHours;
json[r'longitude'] = this.longitude;
json[r'ownerId'] = this.ownerId; json[r'ownerId'] = this.ownerId;
json[r'projectionType'] = this.projectionType; json[r'projectionType'] = this.projectionType;
json[r'ratio'] = this.ratio; json[r'ratio'] = this.ratio;
@@ -189,18 +175,12 @@ class TimeBucketAssetResponseDto {
isTrashed: json[r'isTrashed'] is Iterable isTrashed: json[r'isTrashed'] is Iterable
? (json[r'isTrashed'] as Iterable).cast<bool>().toList(growable: false) ? (json[r'isTrashed'] as Iterable).cast<bool>().toList(growable: false)
: const [], : const [],
latitude: json[r'latitude'] is Iterable
? (json[r'latitude'] as Iterable).cast<num>().toList(growable: false)
: const [],
livePhotoVideoId: json[r'livePhotoVideoId'] is Iterable livePhotoVideoId: json[r'livePhotoVideoId'] is Iterable
? (json[r'livePhotoVideoId'] as Iterable).cast<String>().toList(growable: false) ? (json[r'livePhotoVideoId'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],
localOffsetHours: json[r'localOffsetHours'] is Iterable localOffsetHours: json[r'localOffsetHours'] is Iterable
? (json[r'localOffsetHours'] as Iterable).cast<num>().toList(growable: false) ? (json[r'localOffsetHours'] as Iterable).cast<num>().toList(growable: false)
: const [], : const [],
longitude: json[r'longitude'] is Iterable
? (json[r'longitude'] as Iterable).cast<num>().toList(growable: false)
: const [],
ownerId: json[r'ownerId'] is Iterable ownerId: json[r'ownerId'] is Iterable
? (json[r'ownerId'] as Iterable).cast<String>().toList(growable: false) ? (json[r'ownerId'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],

View File

@@ -96,4 +96,6 @@ abstract class NativeSyncApi {
@TaskQueue(type: TaskQueueType.serialBackgroundThread) @TaskQueue(type: TaskQueueType.serialBackgroundThread)
List<Uint8List?> hashPaths(List<String> paths); List<Uint8List?> hashPaths(List<String> paths);
Map<String, String?> getCloudIdForAssetIds(List<String> assetIds);
} }

View File

@@ -3,9 +3,9 @@ import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/services/hash.service.dart'; import 'package:immich_mobile/domain/services/hash.service.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import '../../fixtures/album.stub.dart'; import '../../fixtures/album.stub.dart';
@@ -39,6 +39,7 @@ void main() {
registerFallbackValue(LocalAlbumStub.recent); registerFallbackValue(LocalAlbumStub.recent);
registerFallbackValue(LocalAssetStub.image1); registerFallbackValue(LocalAssetStub.image1);
when(() => mockAssetRepo.getHashMappingFromCloudId()).thenAnswer((_) async => []);
when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {}); when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {});
when(() => mockStorageRepo.clearCache()).thenAnswer((_) async => {}); when(() => mockStorageRepo.clearCache()).thenAnswer((_) async => {});
}); });
@@ -87,7 +88,8 @@ void main() {
await sut.hashAssets(); await sut.hashAssets();
verify(() => mockNativeApi.hashPaths(['image-path'])).called(1); verify(() => mockNativeApi.hashPaths(['image-path'])).called(1);
final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List<LocalAsset>; final captured =
verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List<LocalAssetHashMapping>;
expect(captured.length, 1); expect(captured.length, 1);
expect(captured[0].checksum, base64.encode(hash)); expect(captured[0].checksum, base64.encode(hash));
}); });
@@ -107,7 +109,8 @@ void main() {
await sut.hashAssets(); await sut.hashAssets();
final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List<LocalAsset>; final captured =
verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List<LocalAssetHashMapping>;
expect(captured.length, 0); expect(captured.length, 0);
}); });
@@ -128,7 +131,8 @@ void main() {
await sut.hashAssets(); await sut.hashAssets();
final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List<LocalAsset>; final captured =
verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List<LocalAssetHashMapping>;
expect(captured.length, 0); expect(captured.length, 0);
}); });
@@ -224,9 +228,10 @@ void main() {
await sut.hashAssets(); await sut.hashAssets();
final captured = verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List<LocalAsset>; final captured =
verify(() => mockAssetRepo.updateHashes(captureAny())).captured.first as List<LocalAssetHashMapping>;
expect(captured.length, 1); expect(captured.length, 1);
expect(captured.first.id, asset1.id); expect(captured.first.assetId, asset1.id);
}); });
}); });
} }

View File

@@ -13,6 +13,7 @@ import 'schema_v7.dart' as v7;
import 'schema_v8.dart' as v8; import 'schema_v8.dart' as v8;
import 'schema_v9.dart' as v9; import 'schema_v9.dart' as v9;
import 'schema_v10.dart' as v10; import 'schema_v10.dart' as v10;
import 'schema_v11.dart' as v11;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@@ -38,10 +39,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v9.DatabaseAtV9(db); return v9.DatabaseAtV9(db);
case 10: case 10:
return v10.DatabaseAtV10(db); return v10.DatabaseAtV10(db);
case 11:
return v11.DatabaseAtV11(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
} }
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
} }

File diff suppressed because it is too large Load Diff

View File

@@ -81,7 +81,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -111,7 +110,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -126,7 +124,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: true, isInLockedView: true,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -144,7 +141,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -160,7 +156,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: true, isInLockedView: true,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -176,7 +171,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -194,7 +188,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -210,7 +203,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -226,7 +218,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: true, isInLockedView: true,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -242,7 +233,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -258,7 +248,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -276,7 +265,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -292,7 +280,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -308,7 +295,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -326,7 +312,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -342,7 +327,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -358,7 +342,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: true, isInLockedView: true,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -376,7 +359,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -392,7 +374,6 @@ void main() {
isTrashEnabled: false, isTrashEnabled: false,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -410,7 +391,6 @@ void main() {
isTrashEnabled: false, isTrashEnabled: false,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -426,7 +406,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -444,7 +423,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -462,7 +440,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -480,7 +457,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -496,7 +472,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -514,7 +489,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -532,7 +506,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -547,7 +520,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -565,7 +537,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -581,7 +552,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -597,7 +567,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -612,45 +581,12 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
expect(ActionButtonType.likeActivity.shouldShow(context), isFalse); expect(ActionButtonType.likeActivity.shouldShow(context), isFalse);
}); });
}); });
group('advancedTroubleshooting button', () {
test('should show when in advanced troubleshooting mode', () {
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: true,
source: ActionSource.timeline,
);
expect(ActionButtonType.advancedInfo.shouldShow(context), isTrue);
});
test('should not show when not in advanced troubleshooting mode', () {
final context = ActionButtonContext(
asset: mergedAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline,
);
expect(ActionButtonType.advancedInfo.shouldShow(context), isFalse);
});
});
}); });
group('ActionButtonType.buildButton', () { group('ActionButtonType.buildButton', () {
@@ -666,7 +602,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
}); });
@@ -682,7 +617,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
final widget = buttonType.buildButton(contextWithAlbum); final widget = buttonType.buildButton(contextWithAlbum);
@@ -705,7 +639,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -725,7 +658,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -743,7 +675,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -762,7 +693,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -775,7 +705,6 @@ void main() {
isTrashEnabled: true, isTrashEnabled: true,
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );

View File

@@ -8895,15 +8895,6 @@
"$ref": "#/components/schemas/AssetVisibility" "$ref": "#/components/schemas/AssetVisibility"
} }
}, },
{
"name": "withCoordinates",
"required": false,
"in": "query",
"description": "Include location data in the response",
"schema": {
"type": "boolean"
}
},
{ {
"name": "withPartners", "name": "withPartners",
"required": false, "required": false,
@@ -9049,15 +9040,6 @@
"$ref": "#/components/schemas/AssetVisibility" "$ref": "#/components/schemas/AssetVisibility"
} }
}, },
{
"name": "withCoordinates",
"required": false,
"in": "query",
"description": "Include location data in the response",
"schema": {
"type": "boolean"
}
},
{ {
"name": "withPartners", "name": "withPartners",
"required": false, "required": false,
@@ -17084,14 +17066,6 @@
}, },
"type": "array" "type": "array"
}, },
"latitude": {
"description": "Array of latitude coordinates extracted from EXIF GPS data",
"items": {
"nullable": true,
"type": "number"
},
"type": "array"
},
"livePhotoVideoId": { "livePhotoVideoId": {
"description": "Array of live photo video asset IDs (null for non-live photos)", "description": "Array of live photo video asset IDs (null for non-live photos)",
"items": { "items": {
@@ -17107,14 +17081,6 @@
}, },
"type": "array" "type": "array"
}, },
"longitude": {
"description": "Array of longitude coordinates extracted from EXIF GPS data",
"items": {
"nullable": true,
"type": "number"
},
"type": "array"
},
"ownerId": { "ownerId": {
"description": "Array of owner IDs for each asset", "description": "Array of owner IDs for each asset",
"items": { "items": {

View File

@@ -1561,14 +1561,10 @@ export type TimeBucketAssetResponseDto = {
isImage: boolean[]; isImage: boolean[];
/** Array indicating whether each asset is in the trash */ /** Array indicating whether each asset is in the trash */
isTrashed: boolean[]; isTrashed: boolean[];
/** Array of latitude coordinates extracted from EXIF GPS data */
latitude?: (number | null)[];
/** Array of live photo video asset IDs (null for non-live photos) */ /** Array of live photo video asset IDs (null for non-live photos) */
livePhotoVideoId: (string | null)[]; livePhotoVideoId: (string | null)[];
/** Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective. */ /** Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective. */
localOffsetHours: number[]; localOffsetHours: number[];
/** Array of longitude coordinates extracted from EXIF GPS data */
longitude?: (number | null)[];
/** Array of owner IDs for each asset */ /** Array of owner IDs for each asset */
ownerId: string[]; ownerId: string[];
/** Array of projection types for 360° content (e.g., "EQUIRECTANGULAR", "CUBEFACE", "CYLINDRICAL") */ /** Array of projection types for 360° content (e.g., "EQUIRECTANGULAR", "CUBEFACE", "CYLINDRICAL") */
@@ -4297,7 +4293,7 @@ export function tagAssets({ id, bulkIdsDto }: {
/** /**
* This endpoint requires the `asset.read` permission. * This endpoint requires the `asset.read` permission.
*/ */
export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, timeBucket, userId, visibility, withCoordinates, withPartners, withStacked }: { export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, timeBucket, userId, visibility, withPartners, withStacked }: {
albumId?: string; albumId?: string;
isFavorite?: boolean; isFavorite?: boolean;
isTrashed?: boolean; isTrashed?: boolean;
@@ -4309,7 +4305,6 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers
timeBucket: string; timeBucket: string;
userId?: string; userId?: string;
visibility?: AssetVisibility; visibility?: AssetVisibility;
withCoordinates?: boolean;
withPartners?: boolean; withPartners?: boolean;
withStacked?: boolean; withStacked?: boolean;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
@@ -4328,7 +4323,6 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers
timeBucket, timeBucket,
userId, userId,
visibility, visibility,
withCoordinates,
withPartners, withPartners,
withStacked withStacked
}))}`, { }))}`, {
@@ -4338,7 +4332,7 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers
/** /**
* This endpoint requires the `asset.read` permission. * This endpoint requires the `asset.read` permission.
*/ */
export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, userId, visibility, withCoordinates, withPartners, withStacked }: { export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, userId, visibility, withPartners, withStacked }: {
albumId?: string; albumId?: string;
isFavorite?: boolean; isFavorite?: boolean;
isTrashed?: boolean; isTrashed?: boolean;
@@ -4349,7 +4343,6 @@ export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, per
tagId?: string; tagId?: string;
userId?: string; userId?: string;
visibility?: AssetVisibility; visibility?: AssetVisibility;
withCoordinates?: boolean;
withPartners?: boolean; withPartners?: boolean;
withStacked?: boolean; withStacked?: boolean;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
@@ -4367,7 +4360,6 @@ export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, per
tagId, tagId,
userId, userId,
visibility, visibility,
withCoordinates,
withPartners, withPartners,
withStacked withStacked
}))}`, { }))}`, {

1104
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -42,11 +42,11 @@
- ⚠️ Цей проєкт перебуває **в дуже активній** розробці. - ⚠️ Цей проєкт перебуває **в дуже активній** розробці.
- ⚠️ Очікуйте безліч помилок і глобальних змін. - ⚠️ Очікуйте безліч помилок і глобальних змін.
- ⚠️ **Не використовуйте цей застосунок як єдине сховище своїх фото та відео.** - ⚠️ **Не використовуйте цей додаток як єдине сховище своїх фото та відео.**
- ⚠️ Завжди дотримуйтесь [плану резервного копіювання 3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) для ваших дорогоцінних фотографій та відео! - ⚠️ Завжди дотримуйтесь [плану резервного копіювання 3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) для ваших дорогоцінних фотографій та відео!
> [!NOTE] > [!NOTE]
> Основну документацію, зокрема посібники зі встановлення, можна знайти за адресою https://immich.app/. > Основну документацію, зокрема посібники з встановлення, можна знайти за адресою https://immich.app/.
## Посилання ## Посилання
@@ -61,7 +61,7 @@
## Демо ## Демо
Доступ до демо-версії [тут](https://demo.immich.app). Для мобільного застосунку ви можете використовувати `https://demo.immich.app` в якості `Server Endpoint URL`. Доступ до демо-версії [тут](https://demo.immich.app). Для мобільного додатку ви можете використовувати `https://demo.immich.app` в якості `Server Endpoint URL`.
### Облікові дані для входу ### Облікові дані для входу
@@ -74,7 +74,7 @@
| Функції | Додаток | Веб | | Функції | Додаток | Веб |
| :------------------------------------------------------- | ------- | --- | | :------------------------------------------------------- | ------- | --- |
| Завантаження та перегляд відео й фото | Так | Так | | Завантаження та перегляд відео й фото | Так | Так |
| Автоматичне резервне копіювання при відкритті застосунку | Так | Н/Д | | Автоматичне резервне копіювання при відкритті додатка | Так | Н/Д |
| Запобігання дублюванню файлів | Так | Так | | Запобігання дублюванню файлів | Так | Так |
| Вибір альбомів для резервного копіювання | Так | Н/Д | | Вибір альбомів для резервного копіювання | Так | Н/Д |
| Завантаження фото та відео на локальний пристрій | Так | Так | | Завантаження фото та відео на локальний пристрій | Так | Так |
@@ -112,7 +112,7 @@
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Статус перекладів" /> <img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Статус перекладів" />
</a> </a>
## Активність репозиторію ## Активність репозитарію
![Діяльність](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Зображення аналітики Repobeats") ![Діяльність](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Зображення аналітики Repobeats")

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