Compare commits
24 Commits
fix/19543
...
feat/sync-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
438fa46cbc | ||
|
|
89bb0ca0c9 | ||
|
|
84fcbd4df8 | ||
|
|
de1c6d7182 | ||
|
|
53fc603d91 | ||
|
|
67fb9b7a2a | ||
|
|
03cd491197 | ||
|
|
5236a72fb3 | ||
|
|
b11ea52704 | ||
|
|
e2c87c2042 | ||
|
|
9cf5d83707 | ||
|
|
254ca4a13d | ||
|
|
82c93cf325 | ||
|
|
d02d3b5472 | ||
|
|
867f4fc53a | ||
|
|
d81ee18238 | ||
|
|
b93b07f461 | ||
|
|
36680e4279 | ||
|
|
816fb1746a | ||
|
|
4b7d61ce97 | ||
|
|
bfadf68e15 | ||
|
|
61e079a63e | ||
|
|
ecbaca3cee | ||
|
|
ada4265cf9 |
11
.github/workflows/close-duplicates.yml
vendored
11
.github/workflows/close-duplicates.yml
vendored
@@ -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
|
||||||
|
|||||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -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}}'
|
||||||
|
|||||||
8
.github/workflows/docs-deploy.yml
vendored
8
.github/workflows/docs-deploy.yml
vendored
@@ -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:
|
||||||
|
|||||||
2
.github/workflows/fix-format.yml
vendored
2
.github/workflows/fix-format.yml
vendored
@@ -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: |
|
||||||
|
|||||||
15
.github/workflows/merge-translations.yml
vendored
15
.github/workflows/merge-translations.yml
vendored
@@ -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
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/preview-label.yaml
vendored
2
.github/workflows/preview-label.yaml
vendored
@@ -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({
|
||||||
|
|||||||
2
.github/workflows/static_analysis.yml
vendored
2
.github/workflows/static_analysis.yml
vendored
@@ -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
|
||||||
|
|||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -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*
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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).
|
||||||
:::
|
:::
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
15
i18n/en.json
15
i18n/en.json
@@ -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",
|
||||||
|
|||||||
@@ -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 && \
|
||||||
|
|||||||
93
machine-learning/uv.lock
generated
93
machine-learning/uv.lock
generated
@@ -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]]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
mobile/drift_schemas/main/drift_schema_v11.json
generated
Normal file
1
mobile/drift_schemas/main/drift_schema_v11.json
generated
Normal file
File diff suppressed because one or more lines are too long
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
mobile/lib/domain/models/asset/asset_metadata.model.dart
Normal file
40
mobile/lib/domain/models/asset/asset_metadata.model.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -176,56 +190,48 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleBackup({bool processBulk = true}) async {
|
Future<void> _handleBackup({bool processBulk = true}) async {
|
||||||
if (!_isBackupEnabled || _isCleanedUp) {
|
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 {
|
||||||
await _ref.read(backgroundSyncProvider).syncLocal();
|
await (
|
||||||
if (_isCleanedUp) {
|
_ref.read(backgroundSyncProvider).syncLocal(),
|
||||||
return;
|
_ref.read(backgroundSyncProvider).syncRemote(),
|
||||||
}
|
).wait.whenComplete(() async {
|
||||||
|
if (_isCleanedUp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await _ref.read(backgroundSyncProvider).syncRemote();
|
var hashFuture = _ref.read(backgroundSyncProvider).hashAssets();
|
||||||
if (_isCleanedUp) {
|
if (hashTimeout != null) {
|
||||||
return;
|
hashFuture = hashFuture.timeout(
|
||||||
}
|
hashTimeout,
|
||||||
|
onTimeout: () {
|
||||||
|
// Consume cancellation errors as we want to continue processing
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
var hashFuture = _ref.read(backgroundSyncProvider).hashAssets();
|
return hashFuture;
|
||||||
if (hashTimeout != null) {
|
});
|
||||||
hashFuture = hashFuture.timeout(
|
|
||||||
hashTimeout,
|
|
||||||
onTimeout: () {
|
|
||||||
// Consume cancellation errors as we want to continue processing
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await hashFuture;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}",
|
||||||
|
|||||||
@@ -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) &&
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
final syncLinkedAlbumServiceProvider = Provider(
|
final syncLinkedAlbumServiceProvider = Provider(
|
||||||
(ref) => SyncLinkedAlbumService(
|
(ref) => SyncLinkedAlbumService(
|
||||||
@@ -20,9 +19,7 @@ class SyncLinkedAlbumService {
|
|||||||
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||||
final DriftAlbumApiRepository _albumApiRepository;
|
final DriftAlbumApiRepository _albumApiRepository;
|
||||||
|
|
||||||
SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
|
const SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
|
||||||
|
|
||||||
final _log = Logger("SyncLinkedAlbumService");
|
|
||||||
|
|
||||||
Future<void> syncLinkedAlbums(String userId) async {
|
Future<void> syncLinkedAlbums(String userId) async {
|
||||||
final selectedAlbums = await _localAlbumRepository.getBackupAlbums();
|
final selectedAlbums = await _localAlbumRepository.getBackupAlbums();
|
||||||
@@ -51,12 +48,8 @@ class SyncLinkedAlbumService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> manageLinkedAlbums(List<LocalAlbum> localAlbums, String ownerId) async {
|
Future<void> manageLinkedAlbums(List<LocalAlbum> localAlbums, String ownerId) async {
|
||||||
try {
|
for (final album in localAlbums) {
|
||||||
for (final album in localAlbums) {
|
await _processLocalAlbum(album, ownerId);
|
||||||
await _processLocalAlbum(album, ownerId);
|
|
||||||
}
|
|
||||||
} catch (error, stackTrace) {
|
|
||||||
_log.severe("Error managing linked albums", error, stackTrace);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
235
mobile/lib/domain/utils/isolate_lock_manager.dart
Normal file
235
mobile/lib/domain/utils/isolate_lock_manager.dart
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
82
mobile/lib/domain/utils/migrate_cloud_ids.dart
Normal file
82
mobile/lib/domain/utils/migrate_cloud_ids.dart
Normal 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();
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)',
|
||||||
|
);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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};
|
||||||
|
}
|
||||||
548
mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart
generated
Normal file
548
mobile/lib/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart
generated
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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([]);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)));
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
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:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@@ -15,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';
|
||||||
@@ -44,13 +41,49 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
|||||||
|
|
||||||
Future<void> _handleMigration() async {
|
Future<void> _handleMigration() async {
|
||||||
try {
|
try {
|
||||||
await _performMigrationLogic().timeout(
|
if (widget.switchingToBeta) {
|
||||||
const Duration(minutes: 3),
|
final assetNotifier = ref.read(assetProvider.notifier);
|
||||||
onTimeout: () async {
|
if (assetNotifier.mounted) {
|
||||||
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
assetNotifier.dispose();
|
||||||
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
}
|
||||||
},
|
final albumNotifier = ref.read(albumProvider.notifier);
|
||||||
);
|
if (albumNotifier.mounted) {
|
||||||
|
albumNotifier.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel uploads
|
||||||
|
await Store.put(StoreKey.backgroundBackup, false);
|
||||||
|
ref
|
||||||
|
.read(backupProvider.notifier)
|
||||||
|
.configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {});
|
||||||
|
ref.read(backupProvider.notifier).setAutoBackup(false);
|
||||||
|
ref.read(backupProvider.notifier).cancelBackup();
|
||||||
|
ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||||
|
// Start listening to new websocket events
|
||||||
|
ref.read(websocketProvider.notifier).stopListenToOldEvents();
|
||||||
|
ref.read(websocketProvider.notifier).startListeningToBetaEvents();
|
||||||
|
|
||||||
|
final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||||
|
|
||||||
|
if (permission.isGranted) {
|
||||||
|
await ref.read(backgroundSyncProvider).syncLocal(full: true);
|
||||||
|
await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||||
|
await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||||
|
await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||||
|
await ref.read(backgroundServiceProvider).disableService();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await ref.read(backgroundSyncProvider).cancel();
|
||||||
|
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
|
||||||
|
ref.read(websocketProvider.notifier).startListeningToOldEvents();
|
||||||
|
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
|
||||||
|
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
||||||
|
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||||
|
await ref.read(driftBackgroundUploadFgService).disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||||
|
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -68,52 +101,6 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _performMigrationLogic() async {
|
|
||||||
if (widget.switchingToBeta) {
|
|
||||||
final assetNotifier = ref.read(assetProvider.notifier);
|
|
||||||
if (assetNotifier.mounted) {
|
|
||||||
assetNotifier.dispose();
|
|
||||||
}
|
|
||||||
final albumNotifier = ref.read(albumProvider.notifier);
|
|
||||||
if (albumNotifier.mounted) {
|
|
||||||
albumNotifier.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel uploads
|
|
||||||
await Store.put(StoreKey.backgroundBackup, false);
|
|
||||||
ref
|
|
||||||
.read(backupProvider.notifier)
|
|
||||||
.configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {});
|
|
||||||
ref.read(backupProvider.notifier).setAutoBackup(false);
|
|
||||||
ref.read(backupProvider.notifier).cancelBackup();
|
|
||||||
ref.read(manualUploadProvider.notifier).cancelBackup();
|
|
||||||
// Start listening to new websocket events
|
|
||||||
ref.read(websocketProvider.notifier).stopListenToOldEvents();
|
|
||||||
ref.read(websocketProvider.notifier).startListeningToBetaEvents();
|
|
||||||
|
|
||||||
final permission = await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
|
||||||
|
|
||||||
if (permission.isGranted) {
|
|
||||||
await ref.read(backgroundSyncProvider).syncLocal(full: true);
|
|
||||||
await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
|
||||||
await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
|
||||||
await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
|
||||||
await ref.read(backgroundServiceProvider).disableService();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await ref.read(backgroundSyncProvider).cancel();
|
|
||||||
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
|
|
||||||
ref.read(websocketProvider.notifier).startListeningToOldEvents();
|
|
||||||
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
|
|
||||||
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
|
||||||
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
|
||||||
await ref.read(backgroundWorkerFgServiceProvider).disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
|
||||||
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -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()),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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/background_sync.provider.dart';
|
||||||
@@ -22,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) {
|
||||||
@@ -48,23 +58,11 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
|||||||
if (accessToken != null && serverUrl != null && endpoint != null) {
|
if (accessToken != null && serverUrl != null && endpoint != null) {
|
||||||
final infoProvider = ref.read(serverInfoProvider.notifier);
|
final infoProvider = ref.read(serverInfoProvider.notifier);
|
||||||
final wsProvider = ref.read(websocketProvider.notifier);
|
final wsProvider = ref.read(websocketProvider.notifier);
|
||||||
final backgroundManager = ref.read(backgroundSyncProvider);
|
|
||||||
|
|
||||||
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
|
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
|
||||||
(_) async {
|
(a) {
|
||||||
try {
|
try {
|
||||||
wsProvider.connect();
|
wsProvider.connect();
|
||||||
infoProvider.getServerInfo();
|
infoProvider.getServerInfo();
|
||||||
|
|
||||||
if (Store.isBetaTimelineEnabled) {
|
|
||||||
await backgroundManager.syncLocal();
|
|
||||||
await backgroundManager.syncRemote();
|
|
||||||
await backgroundManager.hashAssets();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Store.get(StoreKey.syncAlbums, false)) {
|
|
||||||
await backgroundManager.syncLinkedAlbum();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.severe('Failed establishing connection to the server: $e');
|
log.severe('Failed establishing connection to the server: $e');
|
||||||
}
|
}
|
||||||
@@ -82,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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.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/memory.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||||
@@ -15,7 +17,11 @@ import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.da
|
|||||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
import 'package:immich_mobile/providers/tab.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/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/migration.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class TabShellPage extends ConsumerStatefulWidget {
|
class TabShellPage extends ConsumerStatefulWidget {
|
||||||
@@ -26,6 +32,28 @@ class TabShellPage extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _TabShellPageState extends ConsumerState<TabShellPage> {
|
class _TabShellPageState extends ConsumerState<TabShellPage> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
ref.read(websocketProvider.notifier).connect();
|
||||||
|
|
||||||
|
final isEnableBackup = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||||
|
|
||||||
|
await runNewSync(ref, full: true).then((_) async {
|
||||||
|
if (isEnableBackup) {
|
||||||
|
final currentUser = ref.read(currentUserProvider);
|
||||||
|
if (currentUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isScreenLandscape = context.orientation == Orientation.landscape;
|
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||||
|
|||||||
@@ -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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
28
mobile/lib/platform/native_sync_api.g.dart
generated
28
mobile/lib/platform/native_sync_api.g.dart
generated
@@ -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?>();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,49 +125,94 @@ 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
|
||||||
await _safeRun(backgroundManager.syncLocal(), "syncLocal");
|
if (_shouldContinueOperation()) {
|
||||||
await _safeRun(backgroundManager.syncRemote(), "syncRemote");
|
try {
|
||||||
await _safeRun(backgroundManager.hashAssets(), "hashAssets");
|
await backgroundManager.syncLocal();
|
||||||
if (isAlbumLinkedSyncEnable) {
|
} catch (e, stackTrace) {
|
||||||
await _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) {
|
|
||||||
await _safeRun(
|
if (isEnableBackup) {
|
||||||
_ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id),
|
final currentUser = _ref.read(currentUserProvider);
|
||||||
"handleBackupResume",
|
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) {
|
||||||
_log.severe("Error during background sync", e, stackTrace);
|
_log.severe("Error during background sync", e, stackTrace);
|
||||||
|
} finally {
|
||||||
|
// Ensure lock is released even if operations fail
|
||||||
|
try {
|
||||||
|
lockManager.releaseLock();
|
||||||
|
_log.fine("Lock released after background sync operations");
|
||||||
|
} catch (lockError) {
|
||||||
|
_log.warning("Failed to release lock after error: $lockError");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,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();
|
||||||
@@ -244,6 +312,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
if (Store.isBetaTimelineEnabled) {
|
if (Store.isBetaTimelineEnabled) {
|
||||||
|
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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: '/'),
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'dart:io';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.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/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
@@ -22,16 +23,21 @@ import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.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/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/backup/backup.provider.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';
|
||||||
// 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);
|
||||||
@@ -61,26 +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
|
|
||||||
final needBetaMigration = Store.tryGet(StoreKey.needBetaMigration);
|
|
||||||
if (version == 15 && needBetaMigration == null) {
|
|
||||||
// Check both databases directly instead of relying on cache
|
|
||||||
|
|
||||||
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);
|
|
||||||
await Store.put(StoreKey.betaTimeline, true);
|
|
||||||
} 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;
|
||||||
@@ -93,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 {
|
||||||
@@ -308,26 +265,16 @@ class _DeviceAsset {
|
|||||||
const _DeviceAsset({required this.assetId, this.hash, this.dateTime});
|
const _DeviceAsset({required this.assetId, this.hash, this.dateTime});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> resetDriftDatabase(Drift drift) async {
|
Future<void> runNewSync(WidgetRef ref, {bool full = false}) {
|
||||||
// https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94
|
ref.read(backupProvider.notifier).cancelBackup();
|
||||||
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');
|
final backgroundManager = ref.read(backgroundSyncProvider);
|
||||||
await database.beforeOpen(
|
final isAlbumLinkedSyncEnable = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||||
// 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
|
return (backgroundManager.syncLocal(full: full), backgroundManager.syncRemote()).wait.whenComplete(() async {
|
||||||
database.notifyUpdates({for (final table in database.allTables) TableUpdate.onTable(table)});
|
await backgroundManager.hashAssets();
|
||||||
|
if (isAlbumLinkedSyncEnable) {
|
||||||
|
await backgroundManager.syncLinkedAlbum();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,11 +10,9 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.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/oauth.provider.dart';
|
import 'package:immich_mobile/providers/oauth.provider.dart';
|
||||||
@@ -163,18 +161,6 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
serverEndpointController.text = 'http://10.1.15.216:2283/api';
|
serverEndpointController.text = 'http://10.1.15.216:2283/api';
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> handleSyncFlow() async {
|
|
||||||
final backgroundManager = ref.read(backgroundSyncProvider);
|
|
||||||
|
|
||||||
await backgroundManager.syncLocal(full: true);
|
|
||||||
await backgroundManager.syncRemote();
|
|
||||||
await backgroundManager.hashAssets();
|
|
||||||
|
|
||||||
if (Store.get(StoreKey.syncAlbums, false)) {
|
|
||||||
await backgroundManager.syncLinkedAlbum();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
login() async {
|
login() async {
|
||||||
TextInput.finishAutofillContext();
|
TextInput.finishAutofillContext();
|
||||||
|
|
||||||
@@ -192,7 +178,7 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
final isBeta = Store.isBetaTimelineEnabled;
|
final isBeta = Store.isBetaTimelineEnabled;
|
||||||
if (isBeta) {
|
if (isBeta) {
|
||||||
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||||
handleSyncFlow();
|
|
||||||
context.replaceRoute(const TabShellRoute());
|
context.replaceRoute(const TabShellRoute());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -290,7 +276,6 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
if (isBeta) {
|
if (isBeta) {
|
||||||
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||||
handleSyncFlow();
|
|
||||||
context.replaceRoute(const TabShellRoute());
|
context.replaceRoute(const TabShellRoute());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,355 +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: [
|
|
||||||
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(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
mobile/openapi/README.md
generated
3
mobile/openapi/README.md
generated
@@ -107,7 +107,7 @@ Class | Method | HTTP request | Description
|
|||||||
*AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics |
|
*AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics |
|
||||||
*AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random |
|
*AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random |
|
||||||
*AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback |
|
*AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback |
|
||||||
*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace the asset with new file, without changing its id
|
*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | replaceAsset
|
||||||
*AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs |
|
*AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs |
|
||||||
*AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} |
|
*AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} |
|
||||||
*AssetsApi* | [**updateAssetMetadata**](doc//AssetsApi.md#updateassetmetadata) | **PUT** /assets/{id}/metadata |
|
*AssetsApi* | [**updateAssetMetadata**](doc//AssetsApi.md#updateassetmetadata) | **PUT** /assets/{id}/metadata |
|
||||||
@@ -128,7 +128,6 @@ Class | Method | HTTP request | Description
|
|||||||
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||||
*DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} |
|
*DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} |
|
||||||
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random |
|
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random |
|
||||||
*DeprecatedApi* | [**replaceAsset**](doc//DeprecatedApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace the asset with new file, without changing its id
|
|
||||||
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
|
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
|
||||||
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
|
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
|
||||||
*DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} |
|
*DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} |
|
||||||
|
|||||||
16
mobile/openapi/lib/api/assets_api.dart
generated
16
mobile/openapi/lib/api/assets_api.dart
generated
@@ -608,8 +608,8 @@ class AssetsApi {
|
|||||||
///
|
///
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [int] count:
|
/// * [num] count:
|
||||||
Future<Response> getRandomWithHttpInfo({ int? count, }) async {
|
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/assets/random';
|
final apiPath = r'/assets/random';
|
||||||
|
|
||||||
@@ -642,8 +642,8 @@ class AssetsApi {
|
|||||||
///
|
///
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [int] count:
|
/// * [num] count:
|
||||||
Future<List<AssetResponseDto>?> getRandom({ int? count, }) async {
|
Future<List<AssetResponseDto>?> getRandom({ num? count, }) async {
|
||||||
final response = await getRandomWithHttpInfo( count: count, );
|
final response = await getRandomWithHttpInfo( count: count, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
@@ -729,9 +729,9 @@ class AssetsApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the asset with new file, without changing its id
|
/// replaceAsset
|
||||||
///
|
///
|
||||||
/// This property was deprecated in v1.142.0. Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.
|
/// Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.
|
||||||
///
|
///
|
||||||
/// Note: This method returns the HTTP [Response].
|
/// Note: This method returns the HTTP [Response].
|
||||||
///
|
///
|
||||||
@@ -823,9 +823,9 @@ class AssetsApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the asset with new file, without changing its id
|
/// replaceAsset
|
||||||
///
|
///
|
||||||
/// This property was deprecated in v1.142.0. Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.
|
/// Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.
|
||||||
///
|
///
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
|||||||
142
mobile/openapi/lib/api/deprecated_api.dart
generated
142
mobile/openapi/lib/api/deprecated_api.dart
generated
@@ -75,8 +75,8 @@ class DeprecatedApi {
|
|||||||
///
|
///
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [int] count:
|
/// * [num] count:
|
||||||
Future<Response> getRandomWithHttpInfo({ int? count, }) async {
|
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/assets/random';
|
final apiPath = r'/assets/random';
|
||||||
|
|
||||||
@@ -109,8 +109,8 @@ class DeprecatedApi {
|
|||||||
///
|
///
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [int] count:
|
/// * [num] count:
|
||||||
Future<List<AssetResponseDto>?> getRandom({ int? count, }) async {
|
Future<List<AssetResponseDto>?> getRandom({ num? count, }) async {
|
||||||
final response = await getRandomWithHttpInfo( count: count, );
|
final response = await getRandomWithHttpInfo( count: count, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
@@ -127,138 +127,4 @@ class DeprecatedApi {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the asset with new file, without changing its id
|
|
||||||
///
|
|
||||||
/// This property was deprecated in v1.142.0. Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.
|
|
||||||
///
|
|
||||||
/// Note: This method returns the HTTP [Response].
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [String] id (required):
|
|
||||||
///
|
|
||||||
/// * [MultipartFile] assetData (required):
|
|
||||||
///
|
|
||||||
/// * [String] deviceAssetId (required):
|
|
||||||
///
|
|
||||||
/// * [String] deviceId (required):
|
|
||||||
///
|
|
||||||
/// * [DateTime] fileCreatedAt (required):
|
|
||||||
///
|
|
||||||
/// * [DateTime] fileModifiedAt (required):
|
|
||||||
///
|
|
||||||
/// * [String] key:
|
|
||||||
///
|
|
||||||
/// * [String] slug:
|
|
||||||
///
|
|
||||||
/// * [String] duration:
|
|
||||||
///
|
|
||||||
/// * [String] filename:
|
|
||||||
Future<Response> replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async {
|
|
||||||
// ignore: prefer_const_declarations
|
|
||||||
final apiPath = r'/assets/{id}/original'
|
|
||||||
.replaceAll('{id}', id);
|
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
|
||||||
Object? postBody;
|
|
||||||
|
|
||||||
final queryParams = <QueryParam>[];
|
|
||||||
final headerParams = <String, String>{};
|
|
||||||
final formParams = <String, String>{};
|
|
||||||
|
|
||||||
if (key != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
|
||||||
}
|
|
||||||
if (slug != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'slug', slug));
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentTypes = <String>['multipart/form-data'];
|
|
||||||
|
|
||||||
bool hasFields = false;
|
|
||||||
final mp = MultipartRequest('PUT', Uri.parse(apiPath));
|
|
||||||
if (assetData != null) {
|
|
||||||
hasFields = true;
|
|
||||||
mp.fields[r'assetData'] = assetData.field;
|
|
||||||
mp.files.add(assetData);
|
|
||||||
}
|
|
||||||
if (deviceAssetId != null) {
|
|
||||||
hasFields = true;
|
|
||||||
mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId);
|
|
||||||
}
|
|
||||||
if (deviceId != null) {
|
|
||||||
hasFields = true;
|
|
||||||
mp.fields[r'deviceId'] = parameterToString(deviceId);
|
|
||||||
}
|
|
||||||
if (duration != null) {
|
|
||||||
hasFields = true;
|
|
||||||
mp.fields[r'duration'] = parameterToString(duration);
|
|
||||||
}
|
|
||||||
if (fileCreatedAt != null) {
|
|
||||||
hasFields = true;
|
|
||||||
mp.fields[r'fileCreatedAt'] = parameterToString(fileCreatedAt);
|
|
||||||
}
|
|
||||||
if (fileModifiedAt != null) {
|
|
||||||
hasFields = true;
|
|
||||||
mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt);
|
|
||||||
}
|
|
||||||
if (filename != null) {
|
|
||||||
hasFields = true;
|
|
||||||
mp.fields[r'filename'] = parameterToString(filename);
|
|
||||||
}
|
|
||||||
if (hasFields) {
|
|
||||||
postBody = mp;
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiClient.invokeAPI(
|
|
||||||
apiPath,
|
|
||||||
'PUT',
|
|
||||||
queryParams,
|
|
||||||
postBody,
|
|
||||||
headerParams,
|
|
||||||
formParams,
|
|
||||||
contentTypes.isEmpty ? null : contentTypes.first,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace the asset with new file, without changing its id
|
|
||||||
///
|
|
||||||
/// This property was deprecated in v1.142.0. Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.
|
|
||||||
///
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [String] id (required):
|
|
||||||
///
|
|
||||||
/// * [MultipartFile] assetData (required):
|
|
||||||
///
|
|
||||||
/// * [String] deviceAssetId (required):
|
|
||||||
///
|
|
||||||
/// * [String] deviceId (required):
|
|
||||||
///
|
|
||||||
/// * [DateTime] fileCreatedAt (required):
|
|
||||||
///
|
|
||||||
/// * [DateTime] fileModifiedAt (required):
|
|
||||||
///
|
|
||||||
/// * [String] key:
|
|
||||||
///
|
|
||||||
/// * [String] slug:
|
|
||||||
///
|
|
||||||
/// * [String] duration:
|
|
||||||
///
|
|
||||||
/// * [String] filename:
|
|
||||||
Future<AssetMediaResponseDto?> replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async {
|
|
||||||
final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, duration: duration, filename: filename, );
|
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
|
||||||
}
|
|
||||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
|
||||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
|
||||||
// FormatException when trying to decode an empty string.
|
|
||||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
|
||||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetMediaResponseDto',) as AssetMediaResponseDto;
|
|
||||||
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
12
mobile/openapi/lib/api/people_api.dart
generated
12
mobile/openapi/lib/api/people_api.dart
generated
@@ -167,14 +167,14 @@ class PeopleApi {
|
|||||||
///
|
///
|
||||||
/// * [String] closestPersonId:
|
/// * [String] closestPersonId:
|
||||||
///
|
///
|
||||||
/// * [int] page:
|
/// * [num] page:
|
||||||
/// Page number for pagination
|
/// Page number for pagination
|
||||||
///
|
///
|
||||||
/// * [int] size:
|
/// * [num] size:
|
||||||
/// Number of items per page
|
/// Number of items per page
|
||||||
///
|
///
|
||||||
/// * [bool] withHidden:
|
/// * [bool] withHidden:
|
||||||
Future<Response> getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async {
|
Future<Response> getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/people';
|
final apiPath = r'/people';
|
||||||
|
|
||||||
@@ -223,14 +223,14 @@ class PeopleApi {
|
|||||||
///
|
///
|
||||||
/// * [String] closestPersonId:
|
/// * [String] closestPersonId:
|
||||||
///
|
///
|
||||||
/// * [int] page:
|
/// * [num] page:
|
||||||
/// Page number for pagination
|
/// Page number for pagination
|
||||||
///
|
///
|
||||||
/// * [int] size:
|
/// * [num] size:
|
||||||
/// Number of items per page
|
/// Number of items per page
|
||||||
///
|
///
|
||||||
/// * [bool] withHidden:
|
/// * [bool] withHidden:
|
||||||
Future<PeopleResponseDto?> getAllPeople({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async {
|
Future<PeopleResponseDto?> getAllPeople({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async {
|
||||||
final response = await getAllPeopleWithHttpInfo( closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, );
|
final response = await getAllPeopleWithHttpInfo( closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
|||||||
12
mobile/openapi/lib/api/search_api.dart
generated
12
mobile/openapi/lib/api/search_api.dart
generated
@@ -348,9 +348,9 @@ class SearchApi {
|
|||||||
///
|
///
|
||||||
/// * [List<String>] personIds:
|
/// * [List<String>] personIds:
|
||||||
///
|
///
|
||||||
/// * [int] rating:
|
/// * [num] rating:
|
||||||
///
|
///
|
||||||
/// * [int] size:
|
/// * [num] size:
|
||||||
///
|
///
|
||||||
/// * [String] state:
|
/// * [String] state:
|
||||||
///
|
///
|
||||||
@@ -375,7 +375,7 @@ class SearchApi {
|
|||||||
/// * [bool] withDeleted:
|
/// * [bool] withDeleted:
|
||||||
///
|
///
|
||||||
/// * [bool] withExif:
|
/// * [bool] withExif:
|
||||||
Future<Response> searchLargeAssetsWithHttpInfo({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, List<String>? personIds, int? rating, int? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
|
Future<Response> searchLargeAssetsWithHttpInfo({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, List<String>? personIds, num? rating, num? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/search/large-assets';
|
final apiPath = r'/search/large-assets';
|
||||||
|
|
||||||
@@ -532,9 +532,9 @@ class SearchApi {
|
|||||||
///
|
///
|
||||||
/// * [List<String>] personIds:
|
/// * [List<String>] personIds:
|
||||||
///
|
///
|
||||||
/// * [int] rating:
|
/// * [num] rating:
|
||||||
///
|
///
|
||||||
/// * [int] size:
|
/// * [num] size:
|
||||||
///
|
///
|
||||||
/// * [String] state:
|
/// * [String] state:
|
||||||
///
|
///
|
||||||
@@ -559,7 +559,7 @@ class SearchApi {
|
|||||||
/// * [bool] withDeleted:
|
/// * [bool] withDeleted:
|
||||||
///
|
///
|
||||||
/// * [bool] withExif:
|
/// * [bool] withExif:
|
||||||
Future<List<AssetResponseDto>?> searchLargeAssets({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, List<String>? personIds, int? rating, int? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
|
Future<List<AssetResponseDto>?> searchLargeAssets({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, List<String>? personIds, num? rating, num? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
|
||||||
final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceId: deviceId, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, );
|
final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceId: deviceId, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
|||||||
30
mobile/openapi/lib/api/timeline_api.dart
generated
30
mobile/openapi/lib/api/timeline_api.dart
generated
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
16
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
16
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
@@ -40,7 +40,7 @@ class AssetBulkUpdateDto {
|
|||||||
/// source code must fall back to having a nullable type.
|
/// source code must fall back to having a nullable type.
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
///
|
///
|
||||||
int? dateTimeRelative;
|
num? dateTimeRelative;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
@@ -68,7 +68,7 @@ class AssetBulkUpdateDto {
|
|||||||
/// source code must fall back to having a nullable type.
|
/// source code must fall back to having a nullable type.
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
///
|
///
|
||||||
double? latitude;
|
num? latitude;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
@@ -76,7 +76,7 @@ class AssetBulkUpdateDto {
|
|||||||
/// source code must fall back to having a nullable type.
|
/// source code must fall back to having a nullable type.
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
///
|
///
|
||||||
double? longitude;
|
num? longitude;
|
||||||
|
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// Maximum value: 5
|
||||||
@@ -86,7 +86,7 @@ class AssetBulkUpdateDto {
|
|||||||
/// source code must fall back to having a nullable type.
|
/// source code must fall back to having a nullable type.
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
///
|
///
|
||||||
int? rating;
|
num? rating;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
@@ -202,16 +202,16 @@ class AssetBulkUpdateDto {
|
|||||||
|
|
||||||
return AssetBulkUpdateDto(
|
return AssetBulkUpdateDto(
|
||||||
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
|
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
|
||||||
dateTimeRelative: mapValueOfType<int>(json, r'dateTimeRelative'),
|
dateTimeRelative: num.parse('${json[r'dateTimeRelative']}'),
|
||||||
description: mapValueOfType<String>(json, r'description'),
|
description: mapValueOfType<String>(json, r'description'),
|
||||||
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
|
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
|
||||||
ids: json[r'ids'] is Iterable
|
ids: json[r'ids'] is Iterable
|
||||||
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
||||||
: const [],
|
: const [],
|
||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||||
latitude: (mapValueOfType<num>(json, r'latitude')).toDouble(),
|
latitude: num.parse('${json[r'latitude']}'),
|
||||||
longitude: (mapValueOfType<num>(json, r'longitude')).toDouble(),
|
longitude: num.parse('${json[r'longitude']}'),
|
||||||
rating: mapValueOfType<int>(json, r'rating'),
|
rating: num.parse('${json[r'rating']}'),
|
||||||
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
||||||
visibility: AssetVisibility.fromJson(json[r'visibility']),
|
visibility: AssetVisibility.fromJson(json[r'visibility']),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class DatabaseBackupConfig {
|
|||||||
bool enabled;
|
bool enabled;
|
||||||
|
|
||||||
/// Minimum value: 1
|
/// Minimum value: 1
|
||||||
int keepLastAmount;
|
num keepLastAmount;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupConfig &&
|
bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupConfig &&
|
||||||
@@ -60,7 +60,7 @@ class DatabaseBackupConfig {
|
|||||||
return DatabaseBackupConfig(
|
return DatabaseBackupConfig(
|
||||||
cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
|
cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
keepLastAmount: mapValueOfType<int>(json, r'keepLastAmount')!,
|
keepLastAmount: num.parse('${json[r'keepLastAmount']}'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
48
mobile/openapi/lib/model/exif_response_dto.dart
generated
48
mobile/openapi/lib/model/exif_response_dto.dart
generated
@@ -45,25 +45,25 @@ class ExifResponseDto {
|
|||||||
|
|
||||||
String? description;
|
String? description;
|
||||||
|
|
||||||
int? exifImageHeight;
|
num? exifImageHeight;
|
||||||
|
|
||||||
int? exifImageWidth;
|
num? exifImageWidth;
|
||||||
|
|
||||||
String? exposureTime;
|
String? exposureTime;
|
||||||
|
|
||||||
double? fNumber;
|
num? fNumber;
|
||||||
|
|
||||||
int? fileSizeInByte;
|
int? fileSizeInByte;
|
||||||
|
|
||||||
double? focalLength;
|
num? focalLength;
|
||||||
|
|
||||||
int? iso;
|
num? iso;
|
||||||
|
|
||||||
double? latitude;
|
num? latitude;
|
||||||
|
|
||||||
String? lensModel;
|
String? lensModel;
|
||||||
|
|
||||||
double? longitude;
|
num? longitude;
|
||||||
|
|
||||||
String? make;
|
String? make;
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ class ExifResponseDto {
|
|||||||
|
|
||||||
String? projectionType;
|
String? projectionType;
|
||||||
|
|
||||||
int? rating;
|
num? rating;
|
||||||
|
|
||||||
String? state;
|
String? state;
|
||||||
|
|
||||||
@@ -263,22 +263,38 @@ class ExifResponseDto {
|
|||||||
country: mapValueOfType<String>(json, r'country'),
|
country: mapValueOfType<String>(json, r'country'),
|
||||||
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''),
|
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''),
|
||||||
description: mapValueOfType<String>(json, r'description'),
|
description: mapValueOfType<String>(json, r'description'),
|
||||||
exifImageHeight: mapValueOfType<int>(json, r'exifImageHeight'),
|
exifImageHeight: json[r'exifImageHeight'] == null
|
||||||
exifImageWidth: mapValueOfType<int>(json, r'exifImageWidth'),
|
? null
|
||||||
|
: num.parse('${json[r'exifImageHeight']}'),
|
||||||
|
exifImageWidth: json[r'exifImageWidth'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'exifImageWidth']}'),
|
||||||
exposureTime: mapValueOfType<String>(json, r'exposureTime'),
|
exposureTime: mapValueOfType<String>(json, r'exposureTime'),
|
||||||
fNumber: (mapValueOfType<num>(json, r'fNumber'))?.toDouble(),
|
fNumber: json[r'fNumber'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'fNumber']}'),
|
||||||
fileSizeInByte: mapValueOfType<int>(json, r'fileSizeInByte'),
|
fileSizeInByte: mapValueOfType<int>(json, r'fileSizeInByte'),
|
||||||
focalLength: (mapValueOfType<num>(json, r'focalLength'))?.toDouble(),
|
focalLength: json[r'focalLength'] == null
|
||||||
iso: mapValueOfType<int>(json, r'iso'),
|
? null
|
||||||
latitude: (mapValueOfType<num>(json, r'latitude'))?.toDouble(),
|
: num.parse('${json[r'focalLength']}'),
|
||||||
|
iso: json[r'iso'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'iso']}'),
|
||||||
|
latitude: json[r'latitude'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'latitude']}'),
|
||||||
lensModel: mapValueOfType<String>(json, r'lensModel'),
|
lensModel: mapValueOfType<String>(json, r'lensModel'),
|
||||||
longitude: (mapValueOfType<num>(json, r'longitude'))?.toDouble(),
|
longitude: json[r'longitude'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'longitude']}'),
|
||||||
make: mapValueOfType<String>(json, r'make'),
|
make: mapValueOfType<String>(json, r'make'),
|
||||||
model: mapValueOfType<String>(json, r'model'),
|
model: mapValueOfType<String>(json, r'model'),
|
||||||
modifyDate: mapDateTime(json, r'modifyDate', r''),
|
modifyDate: mapDateTime(json, r'modifyDate', r''),
|
||||||
orientation: mapValueOfType<String>(json, r'orientation'),
|
orientation: mapValueOfType<String>(json, r'orientation'),
|
||||||
projectionType: mapValueOfType<String>(json, r'projectionType'),
|
projectionType: mapValueOfType<String>(json, r'projectionType'),
|
||||||
rating: mapValueOfType<int>(json, r'rating'),
|
rating: json[r'rating'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'rating']}'),
|
||||||
state: mapValueOfType<String>(json, r'state'),
|
state: mapValueOfType<String>(json, r'state'),
|
||||||
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
||||||
);
|
);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user