mirror of
https://github.com/immich-app/immich.git
synced 2025-12-08 05:41:04 -08:00
Compare commits
1 Commits
fix/19543
...
shared-dat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9996a1ec3e |
11
.github/workflows/close-duplicates.yml
vendored
11
.github/workflows/close-duplicates.yml
vendored
@@ -35,21 +35,22 @@ jobs:
|
||||
needs: [get_body, should_run]
|
||||
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
||||
container:
|
||||
image: yshavit/mdq:0.9.0@sha256:4399483ca857fb1a7ed28a596f754c7373e358647de31ce14b79a27c91e1e35e
|
||||
image: yshavit/mdq:0.8.0@sha256:c69224d34224a0043d9a3ee46679ba4a2a25afaac445f293d92afe13cd47fcea
|
||||
outputs:
|
||||
checked: ${{ steps.get_checkbox.outputs.checked }}
|
||||
json: ${{ steps.get_checkbox.outputs.json }}
|
||||
steps:
|
||||
- id: get_checkbox
|
||||
env:
|
||||
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: |
|
||||
CHECKED=$(echo "$BODY" | base64 -d | /mdq --output json '# I have searched | - [?] Yes' | jq '.items[0].list[0].checked // false')
|
||||
echo "checked=$CHECKED" >> $GITHUB_OUTPUT
|
||||
JSON=$(echo "$BODY" | base64 -d | /mdq --output json '# I have searched | - [?] Yes')
|
||||
echo "json=$JSON" >> $GITHUB_OUTPUT
|
||||
|
||||
close_and_comment:
|
||||
runs-on: ubuntu-latest
|
||||
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:
|
||||
issues: 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.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0
|
||||
uses: github/codeql-action/init@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# 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).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- 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.
|
||||
# 📚 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
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@2d92b76c45b91eb80fc44c74ce3fce0ee94e8f9d # v3.30.0
|
||||
uses: github/codeql-action/analyze@df559355d593797519d70b90fc8edd5db049e7a2 # v3.29.9
|
||||
with:
|
||||
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
|
||||
- name: Get artifact
|
||||
id: get-artifact
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
return { found: true, id: matchArtifact.id };
|
||||
- name: Determine deploy parameters
|
||||
id: parameters
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
env:
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
with:
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
|
||||
- name: Load parameters
|
||||
id: parameters
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
env:
|
||||
PARAM_JSON: ${{ needs.checks.outputs.parameters }}
|
||||
with:
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
core.setOutput("shouldDeploy", parameters.shouldDeploy);
|
||||
|
||||
- name: Download artifact
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
env:
|
||||
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
|
||||
with:
|
||||
|
||||
2
.github/workflows/fix-format.yml
vendored
2
.github/workflows/fix-format.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
message: 'chore: fix formatting'
|
||||
|
||||
- name: Remove label
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
script: |
|
||||
|
||||
15
.github/workflows/merge-translations.yml
vendored
15
.github/workflows/merge-translations.yml
vendored
@@ -23,24 +23,21 @@ jobs:
|
||||
run: |
|
||||
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"
|
||||
|
||||
PR_NUMBER=$(echo "$PR" | jq '
|
||||
echo "$PR" | jq '
|
||||
if length == 1 then
|
||||
.[0].number
|
||||
else
|
||||
error("Expected exactly 1 entry, got \(length)")
|
||||
end
|
||||
' 2>&1) || exit 1
|
||||
' 2>&1 | read PR_NUMBER || exit 1
|
||||
|
||||
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT
|
||||
echo "Selected PR $PR_NUMBER"
|
||||
|
||||
if ! echo "$PR" | jq -e '.[0].mergeable == "MERGEABLE"'; then
|
||||
echo "PR is not mergeable"
|
||||
exit 1
|
||||
fi
|
||||
echo "$PR" | jq -e '.[0].mergeable == "MERGEABLE"' || { echo "PR is not mergeable" ; exit 1 }
|
||||
|
||||
- name: Generate a token
|
||||
id: generate_token
|
||||
@@ -67,8 +64,8 @@ jobs:
|
||||
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
PR_NUMBER: ${{ steps.find_pr.outputs.PR_NUMBER }}
|
||||
run: |
|
||||
REVIEW_ID=$(gh api -X POST "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" --field event='APPROVE' --field body='Automatically merging translations PR' \
|
||||
| jq '.id')
|
||||
gh api -X POST "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" --field event='APPROVE' --field body='Automatically merging translations PR' \
|
||||
| jq '.id' | read REVIEW_ID
|
||||
echo "REVIEW_ID=$REVIEW_ID" >> $GITHUB_OUTPUT
|
||||
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:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
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 }}
|
||||
|
||||
- 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:
|
||||
sarif_file: results.sarif
|
||||
category: zizmor
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -594,7 +594,7 @@ jobs:
|
||||
contents: read
|
||||
services:
|
||||
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:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,5 +25,3 @@ mobile/ios/fastlane/report.xml
|
||||
|
||||
vite.config.js.timestamp-*
|
||||
.pnpm-store
|
||||
.devcontainer/library
|
||||
.devcontainer/.env*
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"cli"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@types/byte-size": "^8.1.0",
|
||||
|
||||
@@ -2,37 +2,37 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "4.52.3"
|
||||
constraints = "4.52.3"
|
||||
version = "4.52.1"
|
||||
constraints = "4.52.1"
|
||||
hashes = [
|
||||
"h1:3jU62KY4Oj3xzMwkTQWon1nlIvFkgTCqI93IzUGaa0c=",
|
||||
"h1:BWimtYXrvbzbbuoVcyobjQnXjjOb9X69JFTw+GuPxfk=",
|
||||
"h1:C/KvLEm8dVQ6zG2X4asLDtmw2JW/xu7E8MddtaXniO0=",
|
||||
"h1:Doo0xcLFf+CnfDWjsA7G1NvSLURuwcgyVy8k0NF1gJA=",
|
||||
"h1:Gc3FGDtR8lUWsi9VImnnE5/USDXiIwYsv4Hbl+d2lwY=",
|
||||
"h1:HsDY6s1gup5fW9TeuTUy85QMIld1nDOUFlwsfxIq1ig=",
|
||||
"h1:MnHkB56E4b/kT6WZigsZJnB5rgnCfDVbrLBNxIsEXPY=",
|
||||
"h1:O/FUQEqhtknJNdsaMbIBi2pLWBds2VvN5FsTVVntzb0=",
|
||||
"h1:OKQBynkp0J5DIf5FOl/NR3S2rvh89pY+t5wevYxdTJs=",
|
||||
"h1:On+vPsYV8U/J/8wFZPXjeAgNJqFFQj42vNOKuNKURkY=",
|
||||
"h1:SPkrMRJahxK0uum7FnUugbGN/JepHMH8M71DBtYrvG0=",
|
||||
"h1:bEh1ASPMiin3F36+hTfjMQTBnuDl2DzjzSCdova3JEM=",
|
||||
"h1:dtIK+x5Q1sh5SMPaHBHXhL9XDIqbRW0EBmVZ+KHQB8E=",
|
||||
"h1:kZcwWfODMWWyauZ66oaO/X+xXkqBtrbYwfUFEtspwEc=",
|
||||
"zh:53946fce4a631f1d98c61550821c88edede9169dfe5cc254e09a2ab207f76b3f",
|
||||
"zh:61654a21f1dd4331492d4ef77e9ebff066bc01e1281f92b925e5697c9138d681",
|
||||
"zh:6a54e9d129b276f052a2f1b73ad0b8735fe6a7403c6a8f6aa111e525eeefaf35",
|
||||
"zh:7692374e655c346a630b5a7cd776c5e0b2388900dcd7ab69a3af85d0c31c6c43",
|
||||
"h1:2lHvafwGbLdmc9lYkuJFw3nsInaQjRpjX/JfIRKmq/M=",
|
||||
"h1:596JomwjrtUrOSreq9NNCS+rj70+jOV+0pfja5MXiTI=",
|
||||
"h1:7mBOA5TVAIt3qAwPXKCtE0RSYeqij9v30mnksuBbpEg=",
|
||||
"h1:ELVgzh4kHKBCYdL+2A8JjWS0E1snLUN3Mmz3Vo6qSfw=",
|
||||
"h1:FGGM5yLFf72g3kSXM3LAN64Gf/AkXr5WCmhixgnP+l4=",
|
||||
"h1:JupkJbQALcIVoMhHImrLeLDsQR1ET7VJLGC7ONxjqGU=",
|
||||
"h1:KsaE4JNq+1uV1nJsuTcYar/8lyY6zKS5UBEpfYg3wvc=",
|
||||
"h1:NHZ5RJIzQDLhie/ykl3uI6UPfNQR9Lu5Ti7JPR6X904=",
|
||||
"h1:NfAuMbn6LQPLDtJhbzO1MX9JMIGLMa8K6CpekvtsuX8=",
|
||||
"h1:e+vNKokamDsp/kJvFr2pRudzwEz2r49iZ/oSggw+1LY=",
|
||||
"h1:jnb4VdfNZ79I3yj7Q8x+JmOT+FxbfjjRfrF0dL0yCW8=",
|
||||
"h1:kmF//O539d7NuHU7qIxDj7Wz4eJmLKFiI5glwQivldU=",
|
||||
"h1:s6XriaKwOgV4jvKAGPXkrxhhOQxpNU5dceZwi9Z/1k8=",
|
||||
"h1:wt3WBEBAeSGTlC9OlnTlAALxRiK4SQgLy0KgBIS7qzs=",
|
||||
"zh:2fb95e1d3229b9b6c704e1a413c7481c60f139780d9641f657b6eb9b633b90f2",
|
||||
"zh:379c7680983383862236e9e6e720c3114195c40526172188e88d0ffcf50dfe2e",
|
||||
"zh:55533beb6cfc02d22ffda8cba8027bc2c841bb172cd637ed0d28323d41395f8f",
|
||||
"zh:5abd70760e4eb1f37a1c307cbd2989ea7c9ba0afb93818c67c1d363a31f75703",
|
||||
"zh:699f1c8cd66129176fe659ebf0e6337632a8967a28d2630b6ae5948665c0c2ae",
|
||||
"zh:69c15acd73c451e89de6477059cda2f3ec200b48ae4b9ff3646c4d389fd3205e",
|
||||
"zh:6e02b687de21b844f8266dff99e93e7c61fc8eb688f4bbb23803caceb251839e",
|
||||
"zh:7a51d17b87ed87b7bebf2ad9fc7c3a74f16a1b44eee92c779c08eb89258c0496",
|
||||
"zh:88ad84436837b0f55302f22748505972634e87400d6902260fd6b7ba1610f937",
|
||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||
"zh:8fe5b792a4d2b1c3a0e573649642962494faa00299baa6aaf813b9a43203dc02",
|
||||
"zh:a0f403a4862df90f09de65c6e939d6cfd069a8dda2dd33f82948bf6f5f1124ef",
|
||||
"zh:a25dc3eb60777b600f8f125d321fe7c50b811c5302b58e9a727ceb749a04e35d",
|
||||
"zh:a2f2ac7dc703c69d2e8c67c9cb5620b5348cb4fd6b98515fbe3f478517b56602",
|
||||
"zh:d452e7bd24445ee14166470cf50f3aca566d46cab5f26f1c5c988c0f3106b697",
|
||||
"zh:e10a52b0294735659eb3f0821ad2006ec097918efe58d31d37a5e3c47efef5f6",
|
||||
"zh:e28dd0954cef9f05adf4d4b440d6f134f605344dfa56307181996675e6550af2",
|
||||
"zh:f1e3b2f43a472280442f01ba71a3c06c9167432e553381132ea5c4a77e0b6dd5",
|
||||
"zh:f71fd63718d38fd43829861e91fe79e16d7b4c7c3d508ae3d077368d89b8e5a0",
|
||||
"zh:faf8d3da4b819c4ae8e565d2b1a684c6a948a086cb299189a5e7b30b2178409d",
|
||||
"zh:8d46c3d9f4f7ad20ac6ef01daa63f4e30a2d16dcb1bb5c7c7ee3dc6be38e9ca1",
|
||||
"zh:913d64e72a4929dae1d4793e2004f4f9a58b138ea337d9d94fa35cafbf06550a",
|
||||
"zh:c8d93cf86e2e49f6cec665cfe78b82c144cce15a8b2e30f343385fadd1251849",
|
||||
"zh:cc4f69397d9bc34a528a5609a024c3a48f54f21616c0008792dd417297add955",
|
||||
"zh:df99cdb8b064aad35ffea77e645cf6541d0b1b2ebc51b6d26c42031de60ab69e",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "4.52.3"
|
||||
version = "4.52.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,37 +2,37 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "4.52.3"
|
||||
constraints = "4.52.3"
|
||||
version = "4.52.1"
|
||||
constraints = "4.52.1"
|
||||
hashes = [
|
||||
"h1:3jU62KY4Oj3xzMwkTQWon1nlIvFkgTCqI93IzUGaa0c=",
|
||||
"h1:BWimtYXrvbzbbuoVcyobjQnXjjOb9X69JFTw+GuPxfk=",
|
||||
"h1:C/KvLEm8dVQ6zG2X4asLDtmw2JW/xu7E8MddtaXniO0=",
|
||||
"h1:Doo0xcLFf+CnfDWjsA7G1NvSLURuwcgyVy8k0NF1gJA=",
|
||||
"h1:Gc3FGDtR8lUWsi9VImnnE5/USDXiIwYsv4Hbl+d2lwY=",
|
||||
"h1:HsDY6s1gup5fW9TeuTUy85QMIld1nDOUFlwsfxIq1ig=",
|
||||
"h1:MnHkB56E4b/kT6WZigsZJnB5rgnCfDVbrLBNxIsEXPY=",
|
||||
"h1:O/FUQEqhtknJNdsaMbIBi2pLWBds2VvN5FsTVVntzb0=",
|
||||
"h1:OKQBynkp0J5DIf5FOl/NR3S2rvh89pY+t5wevYxdTJs=",
|
||||
"h1:On+vPsYV8U/J/8wFZPXjeAgNJqFFQj42vNOKuNKURkY=",
|
||||
"h1:SPkrMRJahxK0uum7FnUugbGN/JepHMH8M71DBtYrvG0=",
|
||||
"h1:bEh1ASPMiin3F36+hTfjMQTBnuDl2DzjzSCdova3JEM=",
|
||||
"h1:dtIK+x5Q1sh5SMPaHBHXhL9XDIqbRW0EBmVZ+KHQB8E=",
|
||||
"h1:kZcwWfODMWWyauZ66oaO/X+xXkqBtrbYwfUFEtspwEc=",
|
||||
"zh:53946fce4a631f1d98c61550821c88edede9169dfe5cc254e09a2ab207f76b3f",
|
||||
"zh:61654a21f1dd4331492d4ef77e9ebff066bc01e1281f92b925e5697c9138d681",
|
||||
"zh:6a54e9d129b276f052a2f1b73ad0b8735fe6a7403c6a8f6aa111e525eeefaf35",
|
||||
"zh:7692374e655c346a630b5a7cd776c5e0b2388900dcd7ab69a3af85d0c31c6c43",
|
||||
"h1:2lHvafwGbLdmc9lYkuJFw3nsInaQjRpjX/JfIRKmq/M=",
|
||||
"h1:596JomwjrtUrOSreq9NNCS+rj70+jOV+0pfja5MXiTI=",
|
||||
"h1:7mBOA5TVAIt3qAwPXKCtE0RSYeqij9v30mnksuBbpEg=",
|
||||
"h1:ELVgzh4kHKBCYdL+2A8JjWS0E1snLUN3Mmz3Vo6qSfw=",
|
||||
"h1:FGGM5yLFf72g3kSXM3LAN64Gf/AkXr5WCmhixgnP+l4=",
|
||||
"h1:JupkJbQALcIVoMhHImrLeLDsQR1ET7VJLGC7ONxjqGU=",
|
||||
"h1:KsaE4JNq+1uV1nJsuTcYar/8lyY6zKS5UBEpfYg3wvc=",
|
||||
"h1:NHZ5RJIzQDLhie/ykl3uI6UPfNQR9Lu5Ti7JPR6X904=",
|
||||
"h1:NfAuMbn6LQPLDtJhbzO1MX9JMIGLMa8K6CpekvtsuX8=",
|
||||
"h1:e+vNKokamDsp/kJvFr2pRudzwEz2r49iZ/oSggw+1LY=",
|
||||
"h1:jnb4VdfNZ79I3yj7Q8x+JmOT+FxbfjjRfrF0dL0yCW8=",
|
||||
"h1:kmF//O539d7NuHU7qIxDj7Wz4eJmLKFiI5glwQivldU=",
|
||||
"h1:s6XriaKwOgV4jvKAGPXkrxhhOQxpNU5dceZwi9Z/1k8=",
|
||||
"h1:wt3WBEBAeSGTlC9OlnTlAALxRiK4SQgLy0KgBIS7qzs=",
|
||||
"zh:2fb95e1d3229b9b6c704e1a413c7481c60f139780d9641f657b6eb9b633b90f2",
|
||||
"zh:379c7680983383862236e9e6e720c3114195c40526172188e88d0ffcf50dfe2e",
|
||||
"zh:55533beb6cfc02d22ffda8cba8027bc2c841bb172cd637ed0d28323d41395f8f",
|
||||
"zh:5abd70760e4eb1f37a1c307cbd2989ea7c9ba0afb93818c67c1d363a31f75703",
|
||||
"zh:699f1c8cd66129176fe659ebf0e6337632a8967a28d2630b6ae5948665c0c2ae",
|
||||
"zh:69c15acd73c451e89de6477059cda2f3ec200b48ae4b9ff3646c4d389fd3205e",
|
||||
"zh:6e02b687de21b844f8266dff99e93e7c61fc8eb688f4bbb23803caceb251839e",
|
||||
"zh:7a51d17b87ed87b7bebf2ad9fc7c3a74f16a1b44eee92c779c08eb89258c0496",
|
||||
"zh:88ad84436837b0f55302f22748505972634e87400d6902260fd6b7ba1610f937",
|
||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||
"zh:8fe5b792a4d2b1c3a0e573649642962494faa00299baa6aaf813b9a43203dc02",
|
||||
"zh:a0f403a4862df90f09de65c6e939d6cfd069a8dda2dd33f82948bf6f5f1124ef",
|
||||
"zh:a25dc3eb60777b600f8f125d321fe7c50b811c5302b58e9a727ceb749a04e35d",
|
||||
"zh:a2f2ac7dc703c69d2e8c67c9cb5620b5348cb4fd6b98515fbe3f478517b56602",
|
||||
"zh:d452e7bd24445ee14166470cf50f3aca566d46cab5f26f1c5c988c0f3106b697",
|
||||
"zh:e10a52b0294735659eb3f0821ad2006ec097918efe58d31d37a5e3c47efef5f6",
|
||||
"zh:e28dd0954cef9f05adf4d4b440d6f134f605344dfa56307181996675e6550af2",
|
||||
"zh:f1e3b2f43a472280442f01ba71a3c06c9167432e553381132ea5c4a77e0b6dd5",
|
||||
"zh:f71fd63718d38fd43829861e91fe79e16d7b4c7c3d508ae3d077368d89b8e5a0",
|
||||
"zh:faf8d3da4b819c4ae8e565d2b1a684c6a948a086cb299189a5e7b30b2178409d",
|
||||
"zh:8d46c3d9f4f7ad20ac6ef01daa63f4e30a2d16dcb1bb5c7c7ee3dc6be38e9ca1",
|
||||
"zh:913d64e72a4929dae1d4793e2004f4f9a58b138ea337d9d94fa35cafbf06550a",
|
||||
"zh:c8d93cf86e2e49f6cec665cfe78b82c144cce15a8b2e30f343385fadd1251849",
|
||||
"zh:cc4f69397d9bc34a528a5609a024c3a48f54f21616c0008792dd417297add955",
|
||||
"zh:df99cdb8b064aad35ffea77e645cf6541d0b1b2ebc51b6d26c42031de60ab69e",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "4.52.3"
|
||||
version = "4.52.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,13 +143,13 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:fea8b3e67b15729d4bb70589eb03367bab9ad1ee89c876f54327fc7c6e618571
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:a137a2b60aca1a75130022d6bb96af423fefae4eb55faf395732db3544803280
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
|
||||
database:
|
||||
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
|
||||
environment:
|
||||
|
||||
@@ -56,14 +56,14 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:fea8b3e67b15729d4bb70589eb03367bab9ad1ee89c876f54327fc7c6e618571
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:a137a2b60aca1a75130022d6bb96af423fefae4eb55faf395732db3544803280
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
database:
|
||||
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
|
||||
environment:
|
||||
|
||||
@@ -49,14 +49,14 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:fea8b3e67b15729d4bb70589eb03367bab9ad1ee89c876f54327fc7c6e618571
|
||||
image: docker.io/valkey/valkey:8-bookworm@sha256:a137a2b60aca1a75130022d6bb96af423fefae4eb55faf395732db3544803280
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
database:
|
||||
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:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
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.
|
||||
|
||||
:::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.
|
||||
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
|
||||
|
||||
```sql title="Without thumbnails"
|
||||
SELECT * FROM "asset"
|
||||
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';
|
||||
SELECT * FROM "asset" WHERE "asset"."previewPath" IS NULL OR "asset"."thumbnailPath" IS NULL;
|
||||
```
|
||||
|
||||
```sql title="Failed file movements"
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"classnames": "^2.3.2",
|
||||
"clsx": "^2.0.0",
|
||||
"docusaurus-lunr-search": "^3.3.2",
|
||||
"docusaurus-preset-openapi": "^0.7.5",
|
||||
"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',
|
||||
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 {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"author": "",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@immich/cli": "file:../cli",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
@@ -30,6 +31,7 @@
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
|
||||
@@ -1466,10 +1466,10 @@ describe('/asset', () => {
|
||||
expectedDate: '2023-04-04T04:00:00.000Z',
|
||||
},
|
||||
{
|
||||
name: 'CreationDate when DateTimeOriginal missing',
|
||||
name: 'CreateDate when DateTimeOriginal missing',
|
||||
exifData: {
|
||||
CreationDate: '2023:05:05 05:00:00', // TESTABLE
|
||||
CreateDate: '2023:07:07 07:00:00', // TESTABLE
|
||||
CreateDate: '2023:05:05 05:00:00', // TESTABLE
|
||||
CreationDate: '2023:07:07 07:00:00', // TESTABLE
|
||||
GPSDateTime: '2023:10:10 10:00:00', // TESTABLE
|
||||
},
|
||||
expectedDate: '2023-05-05T05:00:00.000Z',
|
||||
|
||||
14
i18n/en.json
14
i18n/en.json
@@ -597,6 +597,8 @@
|
||||
"backup_setting_subtitle": "Manage background and foreground upload settings",
|
||||
"backup_settings_subtitle": "Manage upload settings",
|
||||
"backward": "Backward",
|
||||
"beta_sync": "Beta Sync Status",
|
||||
"beta_sync_subtitle": "Manage the new sync system",
|
||||
"biometric_auth_enabled": "Biometric authentication enabled",
|
||||
"biometric_locked_out": "You are locked out of biometric authentication",
|
||||
"biometric_no_options": "No biometric options available",
|
||||
@@ -1074,7 +1076,10 @@
|
||||
"gcast_enabled": "Google Cast",
|
||||
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
|
||||
"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_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_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",
|
||||
@@ -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_server_up_to_date": "Client and Server are up-to-date",
|
||||
"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_minor": "Server is out of date. Please update to the latest minor version.",
|
||||
"profile_image_of_user": "Profile image of {user}",
|
||||
@@ -1640,7 +1645,6 @@
|
||||
"restore_user": "Restore user",
|
||||
"restored_asset": "Restored asset",
|
||||
"resume": "Resume",
|
||||
"resume_paused_jobs": "Resume {count, plural, one {# paused job} other {# paused jobs}}",
|
||||
"retry_upload": "Retry upload",
|
||||
"review_duplicates": "Review duplicates",
|
||||
"review_large_files": "Review large files",
|
||||
@@ -1845,8 +1849,10 @@
|
||||
"shift_to_permanent_delete": "press ⇧ to permanently delete asset",
|
||||
"show_album_options": "Show album options",
|
||||
"show_albums": "Show albums",
|
||||
"show_all_assets": "Show all assets",
|
||||
"show_all_people": "Show all people",
|
||||
"show_and_hide_people": "Show & hide people",
|
||||
"show_assets_without_location": "Show assets without location",
|
||||
"show_file_location": "Show file location",
|
||||
"show_gallery": "Show gallery",
|
||||
"show_hidden_people": "Show hidden people",
|
||||
@@ -1917,8 +1923,6 @@
|
||||
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
|
||||
"sync_local": "Sync Local",
|
||||
"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",
|
||||
"tag": "Tag",
|
||||
"tag_assets": "Tag assets",
|
||||
@@ -1978,7 +1982,6 @@
|
||||
"trash_page_select_assets_btn": "Select assets",
|
||||
"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}}.",
|
||||
"troubleshoot": "Troubleshoot",
|
||||
"type": "Type",
|
||||
"unable_to_change_pin_code": "Unable to change PIN code",
|
||||
"unable_to_setup_pin_code": "Unable to setup PIN code",
|
||||
@@ -2034,6 +2037,7 @@
|
||||
"use_biometric": "Use biometric",
|
||||
"use_current_connection": "use current connection",
|
||||
"use_custom_date_range": "Use custom date range instead",
|
||||
"use_this_location": "Click to use location",
|
||||
"user": "User",
|
||||
"user_has_been_deleted": "This user has been deleted.",
|
||||
"user_id": "User ID",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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
|
||||
|
||||
@@ -68,11 +68,11 @@ RUN if [ "$DEVICE" = "rocm" ]; then \
|
||||
uv pip install /opt/onnxruntime_rocm-*.whl; \
|
||||
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
|
||||
|
||||
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 && \
|
||||
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]]
|
||||
name = "locust"
|
||||
version = "2.40.2"
|
||||
version = "2.39.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "configargparse" },
|
||||
@@ -1353,7 +1353,6 @@ dependencies = [
|
||||
{ name = "locust-cloud" },
|
||||
{ name = "msgpack" },
|
||||
{ name = "psutil" },
|
||||
{ name = "pytest" },
|
||||
{ name = "python-engineio" },
|
||||
{ name = "python-socketio", extra = ["client"] },
|
||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
@@ -1361,12 +1360,12 @@ dependencies = [
|
||||
{ name = "requests" },
|
||||
{ name = "setuptools" },
|
||||
{ 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" },
|
||||
]
|
||||
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 = [
|
||||
{ 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]]
|
||||
@@ -2205,7 +2204,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.2"
|
||||
version = "8.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
@@ -2216,9 +2215,9 @@ dependencies = [
|
||||
{ name = "pygments" },
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
@@ -2250,14 +2249,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest-mock"
|
||||
version = "3.15.0"
|
||||
version = "3.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
@@ -2533,28 +2532,28 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.13.0"
|
||||
version = "0.12.11"
|
||||
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 = [
|
||||
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]]
|
||||
@@ -2877,27 +2876,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tokenizers"
|
||||
version = "0.22.0"
|
||||
version = "0.21.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ 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 = [
|
||||
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]]
|
||||
|
||||
@@ -39,7 +39,7 @@ class ImmichTestHelper {
|
||||
static Future<void> loadApp(WidgetTester tester) async {
|
||||
await EasyLocalization.ensureInitialized();
|
||||
// Clear all data from Isar (reuse existing instance if available)
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB(shareAcrossIsolates: true);
|
||||
await Bootstrap.initDomain(isar, drift, logDb);
|
||||
await Store.clear();
|
||||
await isar.writeTxn(() => isar.clear());
|
||||
|
||||
@@ -76,8 +76,7 @@ enum StoreKey<T> {
|
||||
betaTimeline<bool>._(1002),
|
||||
enableBackup<bool>._(1003),
|
||||
useWifiForUploadVideos<bool>._(1004),
|
||||
useWifiForUploadPhotos<bool>._(1005),
|
||||
needBetaMigration<bool>._(1006);
|
||||
useWifiForUploadPhotos<bool>._(1005);
|
||||
|
||||
const StoreKey._(this.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/exif.model.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);
|
||||
}
|
||||
|
||||
Future<List<LocalAsset?>> getLocalAssetsByChecksum(String checksum) {
|
||||
return _localAssetRepository.getByChecksum(checksum);
|
||||
}
|
||||
|
||||
Future<RemoteAsset?> getRemoteAssetByChecksum(String checksum) {
|
||||
return _remoteAssetRepository.getByChecksum(checksum);
|
||||
}
|
||||
|
||||
Future<RemoteAsset?> getRemoteAsset(String id) {
|
||||
return _remoteAssetRepository.get(id);
|
||||
}
|
||||
@@ -98,8 +89,4 @@ class AssetService {
|
||||
Future<int> getLocalHashedCount() {
|
||||
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:hooks_riverpod/hooks_riverpod.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/logger_db.repository.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:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:worker_manager/worker_manager.dart';
|
||||
|
||||
class BackgroundWorkerFgService {
|
||||
final BackgroundWorkerFgHostApi _foregroundHostApi;
|
||||
@@ -42,7 +42,8 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
final Drift _drift;
|
||||
final DriftLogger _driftLogger;
|
||||
final BackgroundWorkerBgHostApi _backgroundHostApi;
|
||||
final Logger _logger = Logger('BackgroundWorkerBgService');
|
||||
final Logger _logger = Logger('BackgroundUploadBgService');
|
||||
late final IsolateLockManager _lockManager;
|
||||
|
||||
bool _isCleanedUp = false;
|
||||
|
||||
@@ -58,6 +59,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
driftProvider.overrideWith(driftOverride(drift)),
|
||||
],
|
||||
);
|
||||
_lockManager = IsolateLockManager(onCloseRequest: _cleanup);
|
||||
BackgroundWorkerFlutterApi.setUp(this);
|
||||
}
|
||||
|
||||
@@ -65,30 +67,41 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
|
||||
Future<void> init() async {
|
||||
try {
|
||||
await loadTranslations();
|
||||
HttpSSLOptions.apply(applyNative: false);
|
||||
await _ref.read(authServiceProvider).setOpenApiServiceEndpoint();
|
||||
|
||||
await Future.wait([
|
||||
loadTranslations(),
|
||||
workerManager.init(dynamicSpawning: true),
|
||||
_ref.read(authServiceProvider).setOpenApiServiceEndpoint(),
|
||||
// Initialize the file downloader
|
||||
FileDownloader().configure(
|
||||
globalConfig: [
|
||||
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
||||
(Config.holdingQueue, (6, 6, 3)),
|
||||
// On Android, if files are larger than 256MB, run in foreground service
|
||||
(Config.runInForegroundIfFileLargerThan, 256),
|
||||
],
|
||||
),
|
||||
FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false),
|
||||
FileDownloader().trackTasks(),
|
||||
_ref.read(fileMediaRepositoryProvider).enableBackgroundAccess(),
|
||||
]);
|
||||
|
||||
// Initialize the file downloader
|
||||
await FileDownloader().configure(
|
||||
globalConfig: [
|
||||
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
||||
(Config.holdingQueue, (6, 6, 3)),
|
||||
// On Android, if files are larger than 256MB, run in foreground service
|
||||
(Config.runInForegroundIfFileLargerThan, 256),
|
||||
],
|
||||
);
|
||||
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
|
||||
await FileDownloader().trackTasks();
|
||||
configureFileDownloaderNotifications();
|
||||
await _ref.read(fileMediaRepositoryProvider).enableBackgroundAccess();
|
||||
|
||||
// Notify the host that the background worker service has been initialized and is ready to use
|
||||
_backgroundHostApi.onInitialized();
|
||||
// Notify the host that the background upload service has been initialized and is ready to use
|
||||
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) {
|
||||
_logger.severe("Failed to initialize background worker", error, stack);
|
||||
_backgroundHostApi.close();
|
||||
@@ -157,7 +170,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
_isCleanedUp = true;
|
||||
_logger.info("Cleaning up background worker");
|
||||
final cleanupFutures = [
|
||||
workerManager.dispose(),
|
||||
_drift.close(),
|
||||
_driftLogger.close(),
|
||||
_ref.read(backgroundSyncProvider).cancel(),
|
||||
@@ -168,6 +180,8 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
cleanupFutures.add(_isar.close());
|
||||
}
|
||||
_ref.dispose();
|
||||
_lockManager.releaseLock();
|
||||
|
||||
await Future.wait(cleanupFutures);
|
||||
_logger.info("Background worker resources cleaned up");
|
||||
} catch (error, stack) {
|
||||
@@ -176,56 +190,52 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
}
|
||||
|
||||
Future<void> _handleBackup({bool processBulk = true}) async {
|
||||
if (!_isBackupEnabled || _isCleanedUp) {
|
||||
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
|
||||
if (!_isBackupEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service");
|
||||
|
||||
final currentUser = _ref.read(currentUserProvider);
|
||||
if (currentUser == null) {
|
||||
_logger.warning("[_handleBackup 3] No current user found. Skipping backup from background");
|
||||
return;
|
||||
}
|
||||
|
||||
if (processBulk) {
|
||||
_logger.info("[_handleBackup 4] Resume backup from background");
|
||||
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
||||
}
|
||||
|
||||
final activeTask = await _ref.read(uploadServiceProvider).getActiveTasks(currentUser.id);
|
||||
if (activeTask.isNotEmpty) {
|
||||
_logger.info("[_handleBackup 5] Resuming backup for active tasks from background");
|
||||
await _ref.read(uploadServiceProvider).resumeBackup();
|
||||
} else {
|
||||
_logger.info("[_handleBackup 6] Starting serial backup for new tasks from background");
|
||||
await _ref.read(uploadServiceProvider).startBackupSerial(currentUser.id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _syncAssets({Duration? hashTimeout}) async {
|
||||
await _ref.read(backgroundSyncProvider).syncLocal();
|
||||
if (_isCleanedUp) {
|
||||
return;
|
||||
}
|
||||
final futures = <Future<void>>[];
|
||||
|
||||
await _ref.read(backgroundSyncProvider).syncRemote();
|
||||
if (_isCleanedUp) {
|
||||
return;
|
||||
}
|
||||
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async {
|
||||
if (_isCleanedUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hashFuture = _ref.read(backgroundSyncProvider).hashAssets();
|
||||
if (hashTimeout != null) {
|
||||
hashFuture = hashFuture.timeout(
|
||||
hashTimeout,
|
||||
onTimeout: () {
|
||||
// Consume cancellation errors as we want to continue processing
|
||||
},
|
||||
);
|
||||
}
|
||||
var hashFuture = _ref.read(backgroundSyncProvider).hashAssets();
|
||||
if (hashTimeout != null) {
|
||||
hashFuture = hashFuture.timeout(
|
||||
hashTimeout,
|
||||
onTimeout: () {
|
||||
// Consume cancellation errors as we want to continue processing
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await hashFuture;
|
||||
return hashFuture;
|
||||
});
|
||||
|
||||
futures.add(localSyncFuture);
|
||||
futures.add(_ref.read(backgroundSyncProvider).syncRemote());
|
||||
|
||||
await Future.wait(futures);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +246,7 @@ Future<void> backgroundSyncNativeEntrypoint() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
DartPluginRegistrant.ensureInitialized();
|
||||
|
||||
final (isar, drift, logDB) = await Bootstrap.initDB();
|
||||
final (isar, drift, logDB) = await Bootstrap.initDB(shareAcrossIsolates: false);
|
||||
await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false);
|
||||
await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init();
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class StoreService {
|
||||
_cache.clear();
|
||||
}
|
||||
|
||||
bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? true;
|
||||
bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? false;
|
||||
}
|
||||
|
||||
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/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
final syncLinkedAlbumServiceProvider = Provider(
|
||||
(ref) => SyncLinkedAlbumService(
|
||||
@@ -20,9 +19,7 @@ class SyncLinkedAlbumService {
|
||||
final DriftRemoteAlbumRepository _remoteAlbumRepository;
|
||||
final DriftAlbumApiRepository _albumApiRepository;
|
||||
|
||||
SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
|
||||
|
||||
final _log = Logger("SyncLinkedAlbumService");
|
||||
const SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
|
||||
|
||||
Future<void> syncLinkedAlbums(String userId) async {
|
||||
final selectedAlbums = await _localAlbumRepository.getBackupAlbums();
|
||||
@@ -51,12 +48,8 @@ class SyncLinkedAlbumService {
|
||||
}
|
||||
|
||||
Future<void> manageLinkedAlbums(List<LocalAlbum> localAlbums, String ownerId) async {
|
||||
try {
|
||||
for (final album in localAlbums) {
|
||||
await _processLocalAlbum(album, ownerId);
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
_log.severe("Error managing linked albums", error, stackTrace);
|
||||
for (final album in localAlbums) {
|
||||
await _processLocalAlbum(album, ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:drift/drift.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/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/repositories/db.repository.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();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,8 +66,14 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
||||
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
|
||||
)
|
||||
class Drift extends $Drift implements IDatabaseRepository {
|
||||
Drift([QueryExecutor? executor])
|
||||
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
|
||||
Drift({QueryExecutor? executor, bool shareAcrossIsolates = true})
|
||||
: super(
|
||||
executor ??
|
||||
driftDatabase(
|
||||
name: 'immich',
|
||||
native: DriftNativeOptions(shareAcrossIsolates: shareAcrossIsolates),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 10;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'package:collection/collection.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/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.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
@@ -28,12 +26,6 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
|
||||
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();
|
||||
|
||||
Future<void> updateHashes(Iterable<LocalAsset> hashes) {
|
||||
@@ -77,23 +69,4 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||
Future<int> getHashedCount() {
|
||||
return _db.managers.localAssetEntity.filter((e) => e.checksum.isNull().not()).count();
|
||||
}
|
||||
|
||||
Future<List<LocalAlbum>> getSourceAlbums(String localAssetId, {BackupSelection? backupSelection}) {
|
||||
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),
|
||||
),
|
||||
),
|
||||
)
|
||||
..orderBy([(lae) => OrderingTerm.asc(lae.name)]);
|
||||
if (backupSelection != null) {
|
||||
query.where((lae) => lae.backupSelection.equalsValue(backupSelection));
|
||||
}
|
||||
return query.map((localAlbum) => localAlbum.toDto()).get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,13 @@ import 'logger_db.repository.drift.dart';
|
||||
|
||||
@DriftDatabase(tables: [LogMessageEntity])
|
||||
class DriftLogger extends $DriftLogger implements IDatabaseRepository {
|
||||
DriftLogger([QueryExecutor? executor])
|
||||
DriftLogger({QueryExecutor? executor, bool shareAcrossIsolates = true})
|
||||
: super(
|
||||
executor ?? driftDatabase(name: 'immich_logs', native: const DriftNativeOptions(shareAcrossIsolates: true)),
|
||||
executor ??
|
||||
driftDatabase(
|
||||
name: 'immich_logs',
|
||||
native: DriftNativeOptions(shareAcrossIsolates: shareAcrossIsolates),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
|
||||
@@ -55,12 +55,6 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
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) {
|
||||
if (asset.stackId == null) {
|
||||
return Future.value([]);
|
||||
|
||||
@@ -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/providers/app_life_cycle.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/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/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||
@@ -42,7 +42,7 @@ import 'package:worker_manager/worker_manager.dart';
|
||||
|
||||
void main() async {
|
||||
ImmichWidgetsBinding();
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB(shareAcrossIsolates: true);
|
||||
await Bootstrap.initDomain(isar, drift, logDb);
|
||||
await initApp();
|
||||
// Warm-up isolate pool for worker manager
|
||||
@@ -205,9 +205,9 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
||||
// needs to be delayed so that EasyLocalization is working
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
ref.read(backgroundServiceProvider).disableService();
|
||||
ref.read(backgroundWorkerFgServiceProvider).enable();
|
||||
ref.read(driftBackgroundUploadFgService).enable();
|
||||
} else {
|
||||
ref.read(backgroundWorkerFgServiceProvider).disable();
|
||||
ref.read(driftBackgroundUploadFgService).disable();
|
||||
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.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/gallery_permission.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/websocket.provider.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
@@ -44,13 +41,49 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||
|
||||
Future<void> _handleMigration() async {
|
||||
try {
|
||||
await _performMigrationLogic().timeout(
|
||||
const Duration(minutes: 3),
|
||||
onTimeout: () async {
|
||||
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||
},
|
||||
);
|
||||
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(driftBackgroundUploadFgService).disable();
|
||||
}
|
||||
|
||||
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||
|
||||
if (mounted) {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
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/backup_settings/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/language_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';
|
||||
|
||||
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"),
|
||||
assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"),
|
||||
backup('backup', Icons.cloud_upload_outlined, "backup_settings_subtitle"),
|
||||
@@ -76,9 +76,9 @@ class _MobileLayout extends StatelessWidget {
|
||||
if (Store.isBetaTimelineEnabled)
|
||||
SettingsCard(
|
||||
icon: Icons.sync_outlined,
|
||||
title: 'sync_status'.tr(),
|
||||
subtitle: 'sync_status_subtitle'.tr(),
|
||||
settingRoute: const SyncStatusRoute(),
|
||||
title: 'beta_sync'.tr(),
|
||||
subtitle: 'beta_sync_subtitle'.tr(),
|
||||
settingRoute: const BetaSyncSettingsRoute(),
|
||||
),
|
||||
]
|
||||
: [
|
||||
@@ -143,7 +143,7 @@ class _BetaLandscapeToggle extends HookWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
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:hooks_riverpod/hooks_riverpod.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/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
@@ -22,14 +23,23 @@ class SplashScreenPage extends StatefulHookConsumerWidget {
|
||||
|
||||
class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
final log = Logger("SplashScreenPage");
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ref
|
||||
.read(authProvider.notifier)
|
||||
.setOpenApiServiceEndpoint()
|
||||
.then(logConnectionInfo)
|
||||
.whenComplete(() => resumeSession());
|
||||
final lockManager = ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
|
||||
|
||||
lockManager.requestHolderToClose();
|
||||
lockManager
|
||||
.acquireLock()
|
||||
.timeout(const Duration(seconds: 5))
|
||||
.whenComplete(
|
||||
() => ref
|
||||
.read(authProvider.notifier)
|
||||
.setOpenApiServiceEndpoint()
|
||||
.then(logConnectionInfo)
|
||||
.whenComplete(() => resumeSession()),
|
||||
);
|
||||
}
|
||||
|
||||
void logConnectionInfo(String? endpoint) {
|
||||
@@ -48,23 +58,11 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
if (accessToken != null && serverUrl != null && endpoint != null) {
|
||||
final infoProvider = ref.read(serverInfoProvider.notifier);
|
||||
final wsProvider = ref.read(websocketProvider.notifier);
|
||||
final backgroundManager = ref.read(backgroundSyncProvider);
|
||||
|
||||
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
|
||||
(_) async {
|
||||
(a) {
|
||||
try {
|
||||
wsProvider.connect();
|
||||
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) {
|
||||
log.severe('Failed establishing connection to the server: $e');
|
||||
}
|
||||
@@ -82,16 +80,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
// clean install - change the default of the flag
|
||||
// current install not using beta timeline
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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/utils/event_stream.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/infrastructure/album.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/tab.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/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
|
||||
@RoutePage()
|
||||
class TabShellPage extends ConsumerStatefulWidget {
|
||||
@@ -26,6 +32,28 @@ class TabShellPage extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.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()
|
||||
class SyncStatusPage extends StatelessWidget {
|
||||
const SyncStatusPage({super.key});
|
||||
class BetaSyncSettingsPage extends StatelessWidget {
|
||||
const BetaSyncSettingsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
title: const Text("sync_status").t(context: context),
|
||||
title: const Text("beta_sync").t(context: context),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.maybePop(true),
|
||||
splashRadius: 24,
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
body: const SyncStatusAndActions(),
|
||||
body: const BetaSyncSettings(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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/domain/models/asset/base_asset.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/translate_extensions.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/asset_viewer/current_asset.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/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
@@ -43,7 +41,6 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||
|
||||
final buttonContext = ActionButtonContext(
|
||||
asset: asset,
|
||||
@@ -52,7 +49,6 @@ class AssetDetailBottomSheet extends ConsumerWidget {
|
||||
isTrashEnabled: isTrashEnable,
|
||||
isInLockedView: isInLockedView,
|
||||
currentAlbum: currentAlbum,
|
||||
advancedTroubleshooting: advancedTroubleshooting,
|
||||
source: ActionSource.viewer,
|
||||
);
|
||||
|
||||
@@ -126,10 +122,6 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
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
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final asset = ref.watch(currentAssetNotifier);
|
||||
@@ -140,6 +132,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
|
||||
final cameraTitle = _getCameraInfoTitle(exifInfo);
|
||||
|
||||
Future<void> editDateTime() async {
|
||||
await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context);
|
||||
}
|
||||
|
||||
return SliverList.list(
|
||||
children: [
|
||||
// Asset Date and Time
|
||||
@@ -147,7 +143,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||
title: _getDateTime(context, asset),
|
||||
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
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),
|
||||
const SheetPeopleDetails(),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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_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/edit_date_time_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/domain/models/album/album.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/delete_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/bottom_sheet/base_bottom_sheet.widget.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/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
@@ -54,7 +51,6 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
Widget build(BuildContext context) {
|
||||
final multiselect = ref.watch(multiSelectProvider);
|
||||
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 {
|
||||
final selectedAssets = multiselect.selectedAssets;
|
||||
@@ -92,9 +88,6 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
|
||||
maxChildSize: 0.85,
|
||||
shouldCloseOnMinExtent: false,
|
||||
actions: [
|
||||
if (multiselect.selectedAssets.length == 1 && advancedTroubleshooting) ...[
|
||||
const AdvancedInfoActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
const ShareActionButton(source: ActionSource.timeline),
|
||||
if (multiselect.hasRemote) ...[
|
||||
const ShareLinkActionButton(source: ActionSource.timeline),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/models/backup/backup_state.model.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 {
|
||||
_ref.read(backupProvider.notifier).cancelBackup();
|
||||
final lockManager = _ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
|
||||
|
||||
// Give isolates time to complete any ongoing database transactions
|
||||
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 isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||
|
||||
try {
|
||||
// Run operations sequentially with state checks and error handling for each
|
||||
await _safeRun(backgroundManager.syncLocal(), "syncLocal");
|
||||
await _safeRun(backgroundManager.syncRemote(), "syncRemote");
|
||||
await _safeRun(backgroundManager.hashAssets(), "hashAssets");
|
||||
if (isAlbumLinkedSyncEnable) {
|
||||
await _safeRun(backgroundManager.syncLinkedAlbum(), "syncLinkedAlbum");
|
||||
if (_shouldContinueOperation()) {
|
||||
try {
|
||||
await backgroundManager.syncLocal();
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning("Failed syncLocal: $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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle backup resume only if still active
|
||||
if (isEnableBackup) {
|
||||
final currentUser = _ref.read(currentUserProvider);
|
||||
if (currentUser != null) {
|
||||
await _safeRun(
|
||||
_ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id),
|
||||
"handleBackupResume",
|
||||
);
|
||||
if (_shouldContinueOperation()) {
|
||||
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||
|
||||
if (isEnableBackup) {
|
||||
final currentUser = _ref.read(currentUserProvider);
|
||||
if (currentUser != null) {
|
||||
try {
|
||||
await _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
||||
_log.fine("Completed backup resume");
|
||||
} catch (e, stackTrace) {
|
||||
_log.warning("Failed backup resume: $e", e, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_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) {
|
||||
_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();
|
||||
@@ -244,6 +312,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
} catch (_) {}
|
||||
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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';
|
||||
|
||||
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
||||
@@ -18,3 +19,7 @@ final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
||||
ref.onDispose(manager.cancel);
|
||||
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:hooks_riverpod/hooks_riverpod.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/backup_album.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/success_upload_asset.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/auth.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:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||
|
||||
final driftBackgroundUploadFgService = Provider((ref) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
|
||||
|
||||
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
||||
return BackupNotifier(
|
||||
ref.watch(backupServiceProvider),
|
||||
|
||||
@@ -6,11 +6,11 @@ import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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/asset/base_asset.model.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/services/upload.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -380,5 +380,5 @@ final driftCandidateBackupAlbumInfoProvider = FutureProvider.autoDispose.family<
|
||||
ref,
|
||||
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:flutter/material.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/timeline/multiselect.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/download.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 {
|
||||
final ids = _getRemoteIdsForSource(source);
|
||||
try {
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
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/thumbnail_api.g.dart';
|
||||
|
||||
final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
|
||||
|
||||
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
|
||||
|
||||
final thumbnailApi = ThumbnailApi();
|
||||
|
||||
@@ -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/recently_taken.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/presentation/pages/dev/feat_in_development.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_archive.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_favorite.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: DriftPartnerRoute.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: DriftPersonRoute.page, guards: [_authGuard]),
|
||||
AutoRoute(page: DriftBackupOptionsRoute.page, guards: [_authGuard, _duplicateGuard]),
|
||||
@@ -344,7 +343,6 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: DriftFilterImageRoute.page),
|
||||
AutoRoute(page: DriftActivitiesRoute.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
|
||||
// auto_route_library#1722
|
||||
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
|
||||
/// [AssetViewerPage]
|
||||
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
|
||||
/// [ChangeExperiencePage]
|
||||
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
|
||||
/// [TabControllerPage]
|
||||
class TabControllerRoute extends PageRouteInfo<void> {
|
||||
|
||||
@@ -46,7 +46,7 @@ enum AppSettingsEnum<T> {
|
||||
syncAlbums<bool>(StoreKey.syncAlbums, null, false),
|
||||
autoEndpointSwitching<bool>(StoreKey.autoEndpointSwitching, null, false),
|
||||
photoManagerCustomFilter<bool>(StoreKey.photoManagerCustomFilter, null, true),
|
||||
betaTimeline<bool>(StoreKey.betaTimeline, null, true),
|
||||
betaTimeline<bool>(StoreKey.betaTimeline, null, false),
|
||||
enableBackup<bool>(StoreKey.enableBackup, null, false),
|
||||
useCellularForUploadVideos<bool>(StoreKey.useWifiForUploadVideos, null, false),
|
||||
useCellularForUploadPhotos<bool>(StoreKey.useWifiForUploadPhotos, null, false),
|
||||
|
||||
@@ -330,7 +330,7 @@ class BackgroundService {
|
||||
}
|
||||
|
||||
Future<bool> _onAssetsChanged() async {
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB(shareAcrossIsolates: false);
|
||||
await Bootstrap.initDomain(isar, drift, logDb);
|
||||
|
||||
final ref = ProviderContainer(
|
||||
|
||||
@@ -115,7 +115,7 @@ class BackupVerificationService {
|
||||
assert(tuple.deleteCandidates.length == tuple.originals.length);
|
||||
final List<Asset> result = [];
|
||||
BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken);
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB(shareAcrossIsolates: true);
|
||||
await Bootstrap.initDomain(isar, drift, logDb);
|
||||
await tuple.fileMediaRepository.enableBackgroundAccess();
|
||||
final ApiService apiService = ApiService();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:immich_mobile/constants/enums.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/delete_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/unarchive_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 {
|
||||
final BaseAsset asset;
|
||||
@@ -25,7 +24,6 @@ class ActionButtonContext {
|
||||
final bool isTrashEnabled;
|
||||
final bool isInLockedView;
|
||||
final RemoteAlbum? currentAlbum;
|
||||
final bool advancedTroubleshooting;
|
||||
final ActionSource source;
|
||||
|
||||
const ActionButtonContext({
|
||||
@@ -35,13 +33,11 @@ class ActionButtonContext {
|
||||
required this.isTrashEnabled,
|
||||
required this.isInLockedView,
|
||||
required this.currentAlbum,
|
||||
required this.advancedTroubleshooting,
|
||||
required this.source,
|
||||
});
|
||||
}
|
||||
|
||||
enum ActionButtonType {
|
||||
advancedInfo,
|
||||
share,
|
||||
shareLink,
|
||||
archive,
|
||||
@@ -59,7 +55,6 @@ enum ActionButtonType {
|
||||
|
||||
bool shouldShow(ActionButtonContext context) {
|
||||
return switch (this) {
|
||||
ActionButtonType.advancedInfo => context.advancedTroubleshooting,
|
||||
ActionButtonType.share => true,
|
||||
ActionButtonType.shareLink =>
|
||||
!context.isInLockedView && //
|
||||
@@ -120,7 +115,6 @@ enum ActionButtonType {
|
||||
|
||||
Widget buildButton(ActionButtonContext context) {
|
||||
return switch (this) {
|
||||
ActionButtonType.advancedInfo => AdvancedInfoActionButton(source: context.source),
|
||||
ActionButtonType.share => ShareActionButton(source: context.source),
|
||||
ActionButtonType.shareLink => ShareLinkActionButton(source: context.source),
|
||||
ActionButtonType.archive => ArchiveActionButton(source: context.source),
|
||||
@@ -144,7 +138,6 @@ enum ActionButtonType {
|
||||
|
||||
class ActionButtonBuilder {
|
||||
static const List<ActionButtonType> _actionTypes = [
|
||||
ActionButtonType.advancedInfo,
|
||||
ActionButtonType.share,
|
||||
ActionButtonType.shareLink,
|
||||
ActionButtonType.likeActivity,
|
||||
|
||||
@@ -56,9 +56,9 @@ void configureFileDownloaderNotifications() {
|
||||
}
|
||||
|
||||
abstract final class Bootstrap {
|
||||
static Future<(Isar isar, Drift drift, DriftLogger logDb)> initDB() async {
|
||||
final drift = Drift();
|
||||
final logDb = DriftLogger();
|
||||
static Future<(Isar isar, Drift drift, DriftLogger logDb)> initDB({required bool shareAcrossIsolates}) async {
|
||||
final drift = Drift(shareAcrossIsolates: shareAcrossIsolates);
|
||||
final logDb = DriftLogger(shareAcrossIsolates: shareAcrossIsolates);
|
||||
|
||||
Isar? isar = Isar.getInstance();
|
||||
|
||||
@@ -90,7 +90,7 @@ abstract final class Bootstrap {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
await StoreService.init(storeRepository: storeRepo);
|
||||
|
||||
@@ -34,7 +34,7 @@ Cancelable<T?> runInIsolateGentle<T>({
|
||||
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
||||
DartPluginRegistrant.ensureInitialized();
|
||||
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||
final (isar, drift, logDb) = await Bootstrap.initDB(shareAcrossIsolates: true);
|
||||
await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false);
|
||||
final ref = ProviderContainer(
|
||||
overrides: [
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'dart:io';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.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/store.model.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
@@ -22,16 +23,22 @@ 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/user.entity.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:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
// ignore: import_rule_photo_manager
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
const int targetVersion = 15;
|
||||
const int targetVersion = 14;
|
||||
|
||||
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||
final hasVersion = Store.tryGet(StoreKey.version) != null;
|
||||
final int version = Store.get(StoreKey.version, targetVersion);
|
||||
|
||||
if (version < 9) {
|
||||
await Store.put(StoreKey.version, targetVersion);
|
||||
final value = await db.storeValues.get(StoreKey.currentUser.id);
|
||||
@@ -61,26 +68,6 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||
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) {
|
||||
await Store.put(StoreKey.version, targetVersion);
|
||||
return;
|
||||
@@ -93,35 +80,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 {
|
||||
await Store.delete(StoreKey.assetETag);
|
||||
await db.writeTxn(() async {
|
||||
@@ -308,26 +266,21 @@ class _DeviceAsset {
|
||||
const _DeviceAsset({required this.assetId, this.hash, this.dateTime});
|
||||
}
|
||||
|
||||
Future<void> resetDriftDatabase(Drift drift) async {
|
||||
// https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94
|
||||
final database = drift.attachedDatabase;
|
||||
await database.exclusively(() async {
|
||||
// https://stackoverflow.com/a/65743498/25690041
|
||||
await database.customStatement('PRAGMA writable_schema = 1;');
|
||||
await database.customStatement('DELETE FROM sqlite_master;');
|
||||
await database.customStatement('VACUUM;');
|
||||
await database.customStatement('PRAGMA writable_schema = 0;');
|
||||
await database.customStatement('PRAGMA integrity_check');
|
||||
Future<List<void>> runNewSync(WidgetRef ref, {bool full = false}) {
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
|
||||
await database.customStatement('PRAGMA user_version = 0');
|
||||
await database.beforeOpen(
|
||||
// ignore: invalid_use_of_internal_member
|
||||
database.resolvedEngine.executor,
|
||||
OpeningDetails(null, database.schemaVersion),
|
||||
);
|
||||
await database.customStatement('PRAGMA user_version = ${database.schemaVersion}');
|
||||
final backgroundManager = ref.read(backgroundSyncProvider);
|
||||
final isAlbumLinkedSyncEnable = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||
|
||||
// Refresh all stream queries
|
||||
database.notifyUpdates({for (final table in database.allTables) TableUpdate.onTable(table)});
|
||||
});
|
||||
return Future.wait([
|
||||
backgroundManager.syncLocal(full: full).then((_) {
|
||||
Logger("runNewSync").fine("Hashing assets after syncLocal");
|
||||
return backgroundManager.hashAssets();
|
||||
}),
|
||||
backgroundManager.syncRemote().then((_) {
|
||||
if (isAlbumLinkedSyncEnable) {
|
||||
return backgroundManager.syncLinkedAlbum();
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -90,11 +90,11 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
|
||||
minLeadingWidth: 50,
|
||||
leading: GestureDetector(
|
||||
onTap: pickUserProfileImage,
|
||||
onLongPress: toggleReadonlyMode,
|
||||
onDoubleTap: toggleReadonlyMode,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
AbsorbPointer(child: buildUserProfileImage()),
|
||||
buildUserProfileImage(),
|
||||
if (!isReadonlyModeEnabled)
|
||||
Positioned(
|
||||
bottom: -5,
|
||||
|
||||
@@ -157,7 +157,7 @@ class _ProfileIndicator extends ConsumerWidget {
|
||||
|
||||
return InkWell(
|
||||
onTap: () => showDialog(context: context, useRootNavigator: false, builder: (ctx) => const ImmichAppBarDialog()),
|
||||
onLongPress: () => toggleReadonlyMode(),
|
||||
onDoubleTap: () => toggleReadonlyMode(),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||
child: Badge(
|
||||
label: Container(
|
||||
@@ -173,7 +173,7 @@ class _ProfileIndicator extends ConsumerWidget {
|
||||
? const Icon(Icons.face_outlined, size: widgetSize)
|
||||
: Semantics(
|
||||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -297,7 +297,9 @@ class _SyncStatusIndicatorState extends ConsumerState<_SyncStatusIndicator> with
|
||||
Widget build(BuildContext context) {
|
||||
final syncStatus = ref.watch(syncStatusProvider);
|
||||
final isSyncing = syncStatus.isRemoteSyncing || syncStatus.isLocalSyncing;
|
||||
|
||||
print(
|
||||
"SyncStatusIndicator build - syncStatus.isRemoteSyncing: ${syncStatus.isRemoteSyncing} , isLocalSyncing: ${syncStatus.isLocalSyncing}, isHashing: ${syncStatus.isHashing}",
|
||||
);
|
||||
// Control animations based on sync status
|
||||
if (isSyncing) {
|
||||
if (!_rotationController.isAnimating) {
|
||||
|
||||
@@ -10,11 +10,9 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:fluttertoast/fluttertoast.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/extensions/build_context_extensions.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/gallery_permission.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';
|
||||
}
|
||||
|
||||
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 {
|
||||
TextInput.finishAutofillContext();
|
||||
|
||||
@@ -192,7 +178,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
final isBeta = Store.isBetaTimelineEnabled;
|
||||
if (isBeta) {
|
||||
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||
handleSyncFlow();
|
||||
|
||||
context.replaceRoute(const TabShellRoute());
|
||||
return;
|
||||
}
|
||||
@@ -290,7 +276,6 @@ class LoginForm extends HookConsumerWidget {
|
||||
}
|
||||
if (isBeta) {
|
||||
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||
handleSyncFlow();
|
||||
context.replaceRoute(const TabShellRoute());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
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/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/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();
|
||||
},
|
||||
),
|
||||
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 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random |
|
||||
*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* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} |
|
||||
*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 |
|
||||
*DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} |
|
||||
*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* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
|
||||
*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:
|
||||
///
|
||||
/// * [int] count:
|
||||
Future<Response> getRandomWithHttpInfo({ int? count, }) async {
|
||||
/// * [num] count:
|
||||
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/random';
|
||||
|
||||
@@ -642,8 +642,8 @@ class AssetsApi {
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [int] count:
|
||||
Future<List<AssetResponseDto>?> getRandom({ int? count, }) async {
|
||||
/// * [num] count:
|
||||
Future<List<AssetResponseDto>?> getRandom({ num? count, }) async {
|
||||
final response = await getRandomWithHttpInfo( count: count, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
@@ -729,9 +729,9 @@ class AssetsApi {
|
||||
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].
|
||||
///
|
||||
@@ -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:
|
||||
///
|
||||
|
||||
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:
|
||||
///
|
||||
/// * [int] count:
|
||||
Future<Response> getRandomWithHttpInfo({ int? count, }) async {
|
||||
/// * [num] count:
|
||||
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/random';
|
||||
|
||||
@@ -109,8 +109,8 @@ class DeprecatedApi {
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [int] count:
|
||||
Future<List<AssetResponseDto>?> getRandom({ int? count, }) async {
|
||||
/// * [num] count:
|
||||
Future<List<AssetResponseDto>?> getRandom({ num? count, }) async {
|
||||
final response = await getRandomWithHttpInfo( count: count, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
@@ -127,138 +127,4 @@ class DeprecatedApi {
|
||||
}
|
||||
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:
|
||||
///
|
||||
/// * [int] page:
|
||||
/// * [num] page:
|
||||
/// Page number for pagination
|
||||
///
|
||||
/// * [int] size:
|
||||
/// * [num] size:
|
||||
/// Number of items per page
|
||||
///
|
||||
/// * [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
|
||||
final apiPath = r'/people';
|
||||
|
||||
@@ -223,14 +223,14 @@ class PeopleApi {
|
||||
///
|
||||
/// * [String] closestPersonId:
|
||||
///
|
||||
/// * [int] page:
|
||||
/// * [num] page:
|
||||
/// Page number for pagination
|
||||
///
|
||||
/// * [int] size:
|
||||
/// * [num] size:
|
||||
/// Number of items per page
|
||||
///
|
||||
/// * [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, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
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:
|
||||
///
|
||||
/// * [int] rating:
|
||||
/// * [num] rating:
|
||||
///
|
||||
/// * [int] size:
|
||||
/// * [num] size:
|
||||
///
|
||||
/// * [String] state:
|
||||
///
|
||||
@@ -375,7 +375,7 @@ class SearchApi {
|
||||
/// * [bool] withDeleted:
|
||||
///
|
||||
/// * [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
|
||||
final apiPath = r'/search/large-assets';
|
||||
|
||||
@@ -532,9 +532,9 @@ class SearchApi {
|
||||
///
|
||||
/// * [List<String>] personIds:
|
||||
///
|
||||
/// * [int] rating:
|
||||
/// * [num] rating:
|
||||
///
|
||||
/// * [int] size:
|
||||
/// * [num] size:
|
||||
///
|
||||
/// * [String] state:
|
||||
///
|
||||
@@ -559,7 +559,7 @@ class SearchApi {
|
||||
/// * [bool] withDeleted:
|
||||
///
|
||||
/// * [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, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
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:
|
||||
/// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)
|
||||
///
|
||||
/// * [bool] withCoordinates:
|
||||
/// Include location data in the response
|
||||
///
|
||||
/// * [bool] withPartners:
|
||||
/// Include assets shared by partners
|
||||
///
|
||||
/// * [bool] withStacked:
|
||||
/// 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
|
||||
final apiPath = r'/timeline/bucket';
|
||||
|
||||
@@ -103,9 +100,6 @@ class TimelineApi {
|
||||
if (visibility != null) {
|
||||
queryParams.addAll(_queryParams('', 'visibility', visibility));
|
||||
}
|
||||
if (withCoordinates != null) {
|
||||
queryParams.addAll(_queryParams('', 'withCoordinates', withCoordinates));
|
||||
}
|
||||
if (withPartners != null) {
|
||||
queryParams.addAll(_queryParams('', 'withPartners', withPartners));
|
||||
}
|
||||
@@ -162,16 +156,13 @@ class TimelineApi {
|
||||
/// * [AssetVisibility] visibility:
|
||||
/// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)
|
||||
///
|
||||
/// * [bool] withCoordinates:
|
||||
/// Include location data in the response
|
||||
///
|
||||
/// * [bool] withPartners:
|
||||
/// Include assets shared by partners
|
||||
///
|
||||
/// * [bool] withStacked:
|
||||
/// 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 {
|
||||
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, );
|
||||
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, withPartners: withPartners, withStacked: withStacked, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
@@ -219,15 +210,12 @@ class TimelineApi {
|
||||
/// * [AssetVisibility] visibility:
|
||||
/// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)
|
||||
///
|
||||
/// * [bool] withCoordinates:
|
||||
/// Include location data in the response
|
||||
///
|
||||
/// * [bool] withPartners:
|
||||
/// Include assets shared by partners
|
||||
///
|
||||
/// * [bool] withStacked:
|
||||
/// 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
|
||||
final apiPath = r'/timeline/buckets';
|
||||
|
||||
@@ -268,9 +256,6 @@ class TimelineApi {
|
||||
if (visibility != null) {
|
||||
queryParams.addAll(_queryParams('', 'visibility', visibility));
|
||||
}
|
||||
if (withCoordinates != null) {
|
||||
queryParams.addAll(_queryParams('', 'withCoordinates', withCoordinates));
|
||||
}
|
||||
if (withPartners != null) {
|
||||
queryParams.addAll(_queryParams('', 'withPartners', withPartners));
|
||||
}
|
||||
@@ -324,16 +309,13 @@ class TimelineApi {
|
||||
/// * [AssetVisibility] visibility:
|
||||
/// Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)
|
||||
///
|
||||
/// * [bool] withCoordinates:
|
||||
/// Include location data in the response
|
||||
///
|
||||
/// * [bool] withPartners:
|
||||
/// Include assets shared by partners
|
||||
///
|
||||
/// * [bool] withStacked:
|
||||
/// 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 {
|
||||
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, );
|
||||
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, withPartners: withPartners, withStacked: withStacked, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
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.
|
||||
/// 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
|
||||
@@ -68,7 +68,7 @@ class AssetBulkUpdateDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// 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
|
||||
@@ -76,7 +76,7 @@ class AssetBulkUpdateDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
double? longitude;
|
||||
num? longitude;
|
||||
|
||||
/// Minimum value: -1
|
||||
/// Maximum value: 5
|
||||
@@ -86,7 +86,7 @@ class AssetBulkUpdateDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// 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
|
||||
@@ -202,16 +202,16 @@ class AssetBulkUpdateDto {
|
||||
|
||||
return AssetBulkUpdateDto(
|
||||
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
|
||||
dateTimeRelative: mapValueOfType<int>(json, r'dateTimeRelative'),
|
||||
dateTimeRelative: num.parse('${json[r'dateTimeRelative']}'),
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
|
||||
ids: json[r'ids'] is Iterable
|
||||
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||
latitude: (mapValueOfType<num>(json, r'latitude')).toDouble(),
|
||||
longitude: (mapValueOfType<num>(json, r'longitude')).toDouble(),
|
||||
rating: mapValueOfType<int>(json, r'rating'),
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
||||
visibility: AssetVisibility.fromJson(json[r'visibility']),
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ class DatabaseBackupConfig {
|
||||
bool enabled;
|
||||
|
||||
/// Minimum value: 1
|
||||
int keepLastAmount;
|
||||
num keepLastAmount;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupConfig &&
|
||||
@@ -60,7 +60,7 @@ class DatabaseBackupConfig {
|
||||
return DatabaseBackupConfig(
|
||||
cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
|
||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||
keepLastAmount: mapValueOfType<int>(json, r'keepLastAmount')!,
|
||||
keepLastAmount: num.parse('${json[r'keepLastAmount']}'),
|
||||
);
|
||||
}
|
||||
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;
|
||||
|
||||
int? exifImageHeight;
|
||||
num? exifImageHeight;
|
||||
|
||||
int? exifImageWidth;
|
||||
num? exifImageWidth;
|
||||
|
||||
String? exposureTime;
|
||||
|
||||
double? fNumber;
|
||||
num? fNumber;
|
||||
|
||||
int? fileSizeInByte;
|
||||
|
||||
double? focalLength;
|
||||
num? focalLength;
|
||||
|
||||
int? iso;
|
||||
num? iso;
|
||||
|
||||
double? latitude;
|
||||
num? latitude;
|
||||
|
||||
String? lensModel;
|
||||
|
||||
double? longitude;
|
||||
num? longitude;
|
||||
|
||||
String? make;
|
||||
|
||||
@@ -75,7 +75,7 @@ class ExifResponseDto {
|
||||
|
||||
String? projectionType;
|
||||
|
||||
int? rating;
|
||||
num? rating;
|
||||
|
||||
String? state;
|
||||
|
||||
@@ -263,22 +263,38 @@ class ExifResponseDto {
|
||||
country: mapValueOfType<String>(json, r'country'),
|
||||
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''),
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
exifImageHeight: mapValueOfType<int>(json, r'exifImageHeight'),
|
||||
exifImageWidth: mapValueOfType<int>(json, r'exifImageWidth'),
|
||||
exifImageHeight: json[r'exifImageHeight'] == null
|
||||
? null
|
||||
: num.parse('${json[r'exifImageHeight']}'),
|
||||
exifImageWidth: json[r'exifImageWidth'] == null
|
||||
? null
|
||||
: num.parse('${json[r'exifImageWidth']}'),
|
||||
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'),
|
||||
focalLength: (mapValueOfType<num>(json, r'focalLength'))?.toDouble(),
|
||||
iso: mapValueOfType<int>(json, r'iso'),
|
||||
latitude: (mapValueOfType<num>(json, r'latitude'))?.toDouble(),
|
||||
focalLength: json[r'focalLength'] == null
|
||||
? null
|
||||
: 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'),
|
||||
longitude: (mapValueOfType<num>(json, r'longitude'))?.toDouble(),
|
||||
longitude: json[r'longitude'] == null
|
||||
? null
|
||||
: num.parse('${json[r'longitude']}'),
|
||||
make: mapValueOfType<String>(json, r'make'),
|
||||
model: mapValueOfType<String>(json, r'model'),
|
||||
modifyDate: mapDateTime(json, r'modifyDate', r''),
|
||||
orientation: mapValueOfType<String>(json, r'orientation'),
|
||||
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'),
|
||||
timeZone: mapValueOfType<String>(json, r'timeZone'),
|
||||
);
|
||||
|
||||
12
mobile/openapi/lib/model/metadata_search_dto.dart
generated
12
mobile/openapi/lib/model/metadata_search_dto.dart
generated
@@ -207,7 +207,7 @@ class MetadataSearchDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
int? page;
|
||||
num? page;
|
||||
|
||||
List<String> personIds;
|
||||
|
||||
@@ -227,7 +227,7 @@ class MetadataSearchDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
int? rating;
|
||||
num? rating;
|
||||
|
||||
/// Minimum value: 1
|
||||
/// Maximum value: 1000
|
||||
@@ -237,7 +237,7 @@ class MetadataSearchDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
int? size;
|
||||
num? size;
|
||||
|
||||
String? state;
|
||||
|
||||
@@ -685,13 +685,13 @@ class MetadataSearchDto {
|
||||
order: AssetOrder.fromJson(json[r'order']) ?? AssetOrder.desc,
|
||||
originalFileName: mapValueOfType<String>(json, r'originalFileName'),
|
||||
originalPath: mapValueOfType<String>(json, r'originalPath'),
|
||||
page: mapValueOfType<int>(json, r'page'),
|
||||
page: num.parse('${json[r'page']}'),
|
||||
personIds: json[r'personIds'] is Iterable
|
||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
previewPath: mapValueOfType<String>(json, r'previewPath'),
|
||||
rating: mapValueOfType<int>(json, r'rating'),
|
||||
size: mapValueOfType<int>(json, r'size'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
size: num.parse('${json[r'size']}'),
|
||||
state: mapValueOfType<String>(json, r'state'),
|
||||
tagIds: json[r'tagIds'] is Iterable
|
||||
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
|
||||
4
mobile/openapi/lib/model/on_this_day_dto.dart
generated
4
mobile/openapi/lib/model/on_this_day_dto.dart
generated
@@ -17,7 +17,7 @@ class OnThisDayDto {
|
||||
});
|
||||
|
||||
/// Minimum value: 1
|
||||
int year;
|
||||
num year;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is OnThisDayDto &&
|
||||
@@ -46,7 +46,7 @@ class OnThisDayDto {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return OnThisDayDto(
|
||||
year: mapValueOfType<int>(json, r'year')!,
|
||||
year: num.parse('${json[r'year']}'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -36,9 +36,9 @@ class PlacesResponseDto {
|
||||
///
|
||||
String? admin2name;
|
||||
|
||||
double latitude;
|
||||
num latitude;
|
||||
|
||||
double longitude;
|
||||
num longitude;
|
||||
|
||||
String name;
|
||||
|
||||
@@ -91,8 +91,8 @@ class PlacesResponseDto {
|
||||
return PlacesResponseDto(
|
||||
admin1name: mapValueOfType<String>(json, r'admin1name'),
|
||||
admin2name: mapValueOfType<String>(json, r'admin2name'),
|
||||
latitude: (mapValueOfType<num>(json, r'latitude')!).toDouble(),
|
||||
longitude: (mapValueOfType<num>(json, r'longitude')!).toDouble(),
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
);
|
||||
}
|
||||
|
||||
8
mobile/openapi/lib/model/random_search_dto.dart
generated
8
mobile/openapi/lib/model/random_search_dto.dart
generated
@@ -141,7 +141,7 @@ class RandomSearchDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
int? rating;
|
||||
num? rating;
|
||||
|
||||
/// Minimum value: 1
|
||||
/// Maximum value: 1000
|
||||
@@ -151,7 +151,7 @@ class RandomSearchDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
int? size;
|
||||
num? size;
|
||||
|
||||
String? state;
|
||||
|
||||
@@ -513,8 +513,8 @@ class RandomSearchDto {
|
||||
personIds: json[r'personIds'] is Iterable
|
||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
rating: mapValueOfType<int>(json, r'rating'),
|
||||
size: mapValueOfType<int>(json, r'size'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
size: num.parse('${json[r'size']}'),
|
||||
state: mapValueOfType<String>(json, r'state'),
|
||||
tagIds: json[r'tagIds'] is Iterable
|
||||
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
|
||||
4
mobile/openapi/lib/model/session_create_dto.dart
generated
4
mobile/openapi/lib/model/session_create_dto.dart
generated
@@ -43,7 +43,7 @@ class SessionCreateDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
int? duration;
|
||||
num? duration;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SessionCreateDto &&
|
||||
@@ -92,7 +92,7 @@ class SessionCreateDto {
|
||||
return SessionCreateDto(
|
||||
deviceOS: mapValueOfType<String>(json, r'deviceOS'),
|
||||
deviceType: mapValueOfType<String>(json, r'deviceType'),
|
||||
duration: mapValueOfType<int>(json, r'duration'),
|
||||
duration: num.parse('${json[r'duration']}'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
12
mobile/openapi/lib/model/smart_search_dto.dart
generated
12
mobile/openapi/lib/model/smart_search_dto.dart
generated
@@ -148,7 +148,7 @@ class SmartSearchDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
int? page;
|
||||
num? page;
|
||||
|
||||
List<String> personIds;
|
||||
|
||||
@@ -176,7 +176,7 @@ class SmartSearchDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
int? rating;
|
||||
num? rating;
|
||||
|
||||
/// Minimum value: 1
|
||||
/// Maximum value: 1000
|
||||
@@ -186,7 +186,7 @@ class SmartSearchDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
int? size;
|
||||
num? size;
|
||||
|
||||
String? state;
|
||||
|
||||
@@ -544,14 +544,14 @@ class SmartSearchDto {
|
||||
libraryId: mapValueOfType<String>(json, r'libraryId'),
|
||||
make: mapValueOfType<String>(json, r'make'),
|
||||
model: mapValueOfType<String>(json, r'model'),
|
||||
page: mapValueOfType<int>(json, r'page'),
|
||||
page: num.parse('${json[r'page']}'),
|
||||
personIds: json[r'personIds'] is Iterable
|
||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
query: mapValueOfType<String>(json, r'query'),
|
||||
queryAssetId: mapValueOfType<String>(json, r'queryAssetId'),
|
||||
rating: mapValueOfType<int>(json, r'rating'),
|
||||
size: mapValueOfType<int>(json, r'size'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
size: num.parse('${json[r'size']}'),
|
||||
state: mapValueOfType<String>(json, r'state'),
|
||||
tagIds: json[r'tagIds'] is Iterable
|
||||
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
|
||||
@@ -145,7 +145,7 @@ class StatisticsSearchDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
int? rating;
|
||||
num? rating;
|
||||
|
||||
String? state;
|
||||
|
||||
@@ -448,7 +448,7 @@ class StatisticsSearchDto {
|
||||
personIds: json[r'personIds'] is Iterable
|
||||
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
rating: mapValueOfType<int>(json, r'rating'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
state: mapValueOfType<String>(json, r'state'),
|
||||
tagIds: json[r'tagIds'] is Iterable
|
||||
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
|
||||
@@ -28,7 +28,7 @@ class SystemConfigSmtpTransportDto {
|
||||
|
||||
/// Minimum value: 0
|
||||
/// Maximum value: 65535
|
||||
int port;
|
||||
num port;
|
||||
|
||||
String username;
|
||||
|
||||
@@ -74,7 +74,7 @@ class SystemConfigSmtpTransportDto {
|
||||
host: mapValueOfType<String>(json, r'host')!,
|
||||
ignoreCert: mapValueOfType<bool>(json, r'ignoreCert')!,
|
||||
password: mapValueOfType<String>(json, r'password')!,
|
||||
port: mapValueOfType<int>(json, r'port')!,
|
||||
port: num.parse('${json[r'port']}'),
|
||||
username: mapValueOfType<String>(json, r'username')!,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,10 +21,8 @@ class TimeBucketAssetResponseDto {
|
||||
this.isFavorite = const [],
|
||||
this.isImage = const [],
|
||||
this.isTrashed = const [],
|
||||
this.latitude = const [],
|
||||
this.livePhotoVideoId = const [],
|
||||
this.localOffsetHours = const [],
|
||||
this.longitude = const [],
|
||||
this.ownerId = const [],
|
||||
this.projectionType = const [],
|
||||
this.ratio = const [],
|
||||
@@ -57,18 +55,12 @@ class TimeBucketAssetResponseDto {
|
||||
/// Array indicating whether each asset is in the trash
|
||||
List<bool> isTrashed;
|
||||
|
||||
/// Array of latitude coordinates extracted from EXIF GPS data
|
||||
List<num?> latitude;
|
||||
|
||||
/// Array of live photo video asset IDs (null for non-live photos)
|
||||
List<String?> livePhotoVideoId;
|
||||
|
||||
/// Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.
|
||||
List<num> localOffsetHours;
|
||||
|
||||
/// Array of longitude coordinates extracted from EXIF GPS data
|
||||
List<num?> longitude;
|
||||
|
||||
/// Array of owner IDs for each asset
|
||||
List<String> ownerId;
|
||||
|
||||
@@ -97,10 +89,8 @@ class TimeBucketAssetResponseDto {
|
||||
_deepEquality.equals(other.isFavorite, isFavorite) &&
|
||||
_deepEquality.equals(other.isImage, isImage) &&
|
||||
_deepEquality.equals(other.isTrashed, isTrashed) &&
|
||||
_deepEquality.equals(other.latitude, latitude) &&
|
||||
_deepEquality.equals(other.livePhotoVideoId, livePhotoVideoId) &&
|
||||
_deepEquality.equals(other.localOffsetHours, localOffsetHours) &&
|
||||
_deepEquality.equals(other.longitude, longitude) &&
|
||||
_deepEquality.equals(other.ownerId, ownerId) &&
|
||||
_deepEquality.equals(other.projectionType, projectionType) &&
|
||||
_deepEquality.equals(other.ratio, ratio) &&
|
||||
@@ -119,10 +109,8 @@ class TimeBucketAssetResponseDto {
|
||||
(isFavorite.hashCode) +
|
||||
(isImage.hashCode) +
|
||||
(isTrashed.hashCode) +
|
||||
(latitude.hashCode) +
|
||||
(livePhotoVideoId.hashCode) +
|
||||
(localOffsetHours.hashCode) +
|
||||
(longitude.hashCode) +
|
||||
(ownerId.hashCode) +
|
||||
(projectionType.hashCode) +
|
||||
(ratio.hashCode) +
|
||||
@@ -131,7 +119,7 @@ class TimeBucketAssetResponseDto {
|
||||
(visibility.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'TimeBucketAssetResponseDto[city=$city, country=$country, duration=$duration, fileCreatedAt=$fileCreatedAt, id=$id, isFavorite=$isFavorite, isImage=$isImage, isTrashed=$isTrashed, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, localOffsetHours=$localOffsetHours, longitude=$longitude, ownerId=$ownerId, projectionType=$projectionType, ratio=$ratio, stack=$stack, thumbhash=$thumbhash, visibility=$visibility]';
|
||||
String toString() => 'TimeBucketAssetResponseDto[city=$city, country=$country, duration=$duration, fileCreatedAt=$fileCreatedAt, id=$id, isFavorite=$isFavorite, isImage=$isImage, isTrashed=$isTrashed, livePhotoVideoId=$livePhotoVideoId, localOffsetHours=$localOffsetHours, ownerId=$ownerId, projectionType=$projectionType, ratio=$ratio, stack=$stack, thumbhash=$thumbhash, visibility=$visibility]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -143,10 +131,8 @@ class TimeBucketAssetResponseDto {
|
||||
json[r'isFavorite'] = this.isFavorite;
|
||||
json[r'isImage'] = this.isImage;
|
||||
json[r'isTrashed'] = this.isTrashed;
|
||||
json[r'latitude'] = this.latitude;
|
||||
json[r'livePhotoVideoId'] = this.livePhotoVideoId;
|
||||
json[r'localOffsetHours'] = this.localOffsetHours;
|
||||
json[r'longitude'] = this.longitude;
|
||||
json[r'ownerId'] = this.ownerId;
|
||||
json[r'projectionType'] = this.projectionType;
|
||||
json[r'ratio'] = this.ratio;
|
||||
@@ -189,18 +175,12 @@ class TimeBucketAssetResponseDto {
|
||||
isTrashed: json[r'isTrashed'] is Iterable
|
||||
? (json[r'isTrashed'] as Iterable).cast<bool>().toList(growable: false)
|
||||
: const [],
|
||||
latitude: json[r'latitude'] is Iterable
|
||||
? (json[r'latitude'] as Iterable).cast<num>().toList(growable: false)
|
||||
: const [],
|
||||
livePhotoVideoId: json[r'livePhotoVideoId'] is Iterable
|
||||
? (json[r'livePhotoVideoId'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
localOffsetHours: json[r'localOffsetHours'] is Iterable
|
||||
? (json[r'localOffsetHours'] as Iterable).cast<num>().toList(growable: false)
|
||||
: const [],
|
||||
longitude: json[r'longitude'] is Iterable
|
||||
? (json[r'longitude'] as Iterable).cast<num>().toList(growable: false)
|
||||
: const [],
|
||||
ownerId: json[r'ownerId'] is Iterable
|
||||
? (json[r'ownerId'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
|
||||
12
mobile/openapi/lib/model/update_asset_dto.dart
generated
12
mobile/openapi/lib/model/update_asset_dto.dart
generated
@@ -53,7 +53,7 @@ class UpdateAssetDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
double? latitude;
|
||||
num? latitude;
|
||||
|
||||
String? livePhotoVideoId;
|
||||
|
||||
@@ -63,7 +63,7 @@ class UpdateAssetDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
double? longitude;
|
||||
num? longitude;
|
||||
|
||||
/// Minimum value: -1
|
||||
/// Maximum value: 5
|
||||
@@ -73,7 +73,7 @@ class UpdateAssetDto {
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// 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
|
||||
@@ -166,10 +166,10 @@ class UpdateAssetDto {
|
||||
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||
latitude: (mapValueOfType<num>(json, r'latitude')).toDouble(),
|
||||
latitude: num.parse('${json[r'latitude']}'),
|
||||
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||
longitude: (mapValueOfType<num>(json, r'longitude')).toDouble(),
|
||||
rating: mapValueOfType<int>(json, r'rating'),
|
||||
longitude: num.parse('${json[r'longitude']}'),
|
||||
rating: num.parse('${json[r'rating']}'),
|
||||
visibility: AssetVisibility.fromJson(json[r'visibility']),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ void main() {
|
||||
for (final toVersion in versions.skip(i + 1)) {
|
||||
test('to $toVersion', () async {
|
||||
final schema = await verifier.schemaAt(fromVersion);
|
||||
final db = Drift(schema.newConnection());
|
||||
final db = Drift(executor: schema.newConnection());
|
||||
await verifier.migrateAndValidate(db, toVersion);
|
||||
await db.close();
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ void main() {
|
||||
late MediumFactory mediumFactory;
|
||||
|
||||
setUp(() {
|
||||
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
db = Drift(executor: DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
mediumFactory = MediumFactory(db);
|
||||
});
|
||||
|
||||
|
||||
@@ -62,7 +62,10 @@ void main() {
|
||||
);
|
||||
|
||||
setUpAll(() async {
|
||||
final loggerDb = DriftLogger(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
final loggerDb = DriftLogger(
|
||||
executor: DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true),
|
||||
shareAcrossIsolates: true,
|
||||
);
|
||||
final LogRepository logRepository = LogRepository(loggerDb);
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
@@ -81,7 +81,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -111,7 +110,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -126,7 +124,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: true,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -144,7 +141,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -160,7 +156,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: true,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -176,7 +171,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -194,7 +188,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -210,7 +203,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -226,7 +218,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: true,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -242,7 +233,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -258,7 +248,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -276,7 +265,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -292,7 +280,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -308,7 +295,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -326,7 +312,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -342,7 +327,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -358,7 +342,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: true,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -376,7 +359,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -392,7 +374,6 @@ void main() {
|
||||
isTrashEnabled: false,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -410,7 +391,6 @@ void main() {
|
||||
isTrashEnabled: false,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -426,7 +406,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -444,7 +423,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -462,7 +440,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -480,7 +457,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -496,7 +472,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -514,7 +489,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -532,7 +506,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -547,7 +520,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -565,7 +537,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -581,7 +552,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -597,7 +567,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -612,45 +581,12 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.likeActivity.shouldShow(context), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('advancedTroubleshooting button', () {
|
||||
test('should show when in advanced troubleshooting mode', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: mergedAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: true,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.advancedInfo.shouldShow(context), isTrue);
|
||||
});
|
||||
|
||||
test('should not show when not in advanced troubleshooting mode', () {
|
||||
final context = ActionButtonContext(
|
||||
asset: mergedAsset,
|
||||
isOwner: true,
|
||||
isArchived: false,
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
expect(ActionButtonType.advancedInfo.shouldShow(context), isFalse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('ActionButtonType.buildButton', () {
|
||||
@@ -666,7 +602,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
});
|
||||
@@ -682,7 +617,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
final widget = buttonType.buildButton(contextWithAlbum);
|
||||
@@ -705,7 +639,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -725,7 +658,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: album,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -743,7 +675,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -762,7 +693,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
@@ -775,7 +705,6 @@ void main() {
|
||||
isTrashEnabled: true,
|
||||
isInLockedView: false,
|
||||
currentAlbum: null,
|
||||
advancedTroubleshooting: false,
|
||||
source: ActionSource.timeline,
|
||||
);
|
||||
|
||||
|
||||
@@ -2035,7 +2035,7 @@
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -2504,8 +2504,7 @@
|
||||
"description": "This endpoint requires the `asset.download` permission."
|
||||
},
|
||||
"put": {
|
||||
"deprecated": true,
|
||||
"description": "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.",
|
||||
"description": "Replace the asset with new file, without changing its id. This endpoint requires the `asset.replace` permission.",
|
||||
"operationId": "replaceAsset",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -2567,14 +2566,12 @@
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Replace the asset with new file, without changing its id",
|
||||
"summary": "replaceAsset",
|
||||
"tags": [
|
||||
"Assets",
|
||||
"Deprecated"
|
||||
"Assets"
|
||||
],
|
||||
"x-immich-lifecycle": {
|
||||
"addedAt": "v1.106.0",
|
||||
"deprecatedAt": "v1.142.0"
|
||||
"addedAt": "v1.106.0"
|
||||
},
|
||||
"x-immich-permission": "asset.replace"
|
||||
}
|
||||
@@ -5242,7 +5239,7 @@
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"default": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5254,7 +5251,7 @@
|
||||
"minimum": 1,
|
||||
"maximum": 1000,
|
||||
"default": 500,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5955,7 +5952,7 @@
|
||||
"schema": {
|
||||
"minimum": -1,
|
||||
"maximum": 5,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5965,7 +5962,7 @@
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"maximum": 1000,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -8898,15 +8895,6 @@
|
||||
"$ref": "#/components/schemas/AssetVisibility"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "withCoordinates",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Include location data in the response",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "withPartners",
|
||||
"required": false,
|
||||
@@ -9052,15 +9040,6 @@
|
||||
"$ref": "#/components/schemas/AssetVisibility"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "withCoordinates",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Include location data in the response",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "withPartners",
|
||||
"required": false,
|
||||
@@ -10408,7 +10387,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"dateTimeRelative": {
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
@@ -10428,17 +10407,15 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"latitude": {
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"longitude": {
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string"
|
||||
@@ -11573,7 +11550,7 @@
|
||||
},
|
||||
"keepLastAmount": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -11766,12 +11743,12 @@
|
||||
"exifImageHeight": {
|
||||
"default": null,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"exifImageWidth": {
|
||||
"default": null,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"exposureTime": {
|
||||
"default": null,
|
||||
@@ -11780,7 +11757,6 @@
|
||||
},
|
||||
"fNumber": {
|
||||
"default": null,
|
||||
"format": "double",
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
@@ -11792,18 +11768,16 @@
|
||||
},
|
||||
"focalLength": {
|
||||
"default": null,
|
||||
"format": "double",
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"iso": {
|
||||
"default": null,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"latitude": {
|
||||
"default": null,
|
||||
"format": "double",
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
@@ -11814,7 +11788,6 @@
|
||||
},
|
||||
"longitude": {
|
||||
"default": null,
|
||||
"format": "double",
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
@@ -11847,7 +11820,7 @@
|
||||
"rating": {
|
||||
"default": null,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"state": {
|
||||
"default": null,
|
||||
@@ -12600,7 +12573,7 @@
|
||||
},
|
||||
"page": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"personIds": {
|
||||
"items": {
|
||||
@@ -12615,12 +12588,12 @@
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"size": {
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"state": {
|
||||
"nullable": true,
|
||||
@@ -12897,7 +12870,7 @@
|
||||
"properties": {
|
||||
"year": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -13446,11 +13419,9 @@
|
||||
"type": "string"
|
||||
},
|
||||
"latitude": {
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"longitude": {
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
@@ -13574,12 +13545,12 @@
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"size": {
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"state": {
|
||||
"nullable": true,
|
||||
@@ -14262,7 +14233,7 @@
|
||||
"duration": {
|
||||
"description": "session duration, in seconds",
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -14658,7 +14629,7 @@
|
||||
},
|
||||
"page": {
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"personIds": {
|
||||
"items": {
|
||||
@@ -14677,12 +14648,12 @@
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"size": {
|
||||
"maximum": 1000,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"state": {
|
||||
"nullable": true,
|
||||
@@ -14871,7 +14842,7 @@
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"state": {
|
||||
"nullable": true,
|
||||
@@ -16691,7 +16662,7 @@
|
||||
"port": {
|
||||
"maximum": 65535,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
@@ -17095,14 +17066,6 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"latitude": {
|
||||
"description": "Array of latitude coordinates extracted from EXIF GPS data",
|
||||
"items": {
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"livePhotoVideoId": {
|
||||
"description": "Array of live photo video asset IDs (null for non-live photos)",
|
||||
"items": {
|
||||
@@ -17118,14 +17081,6 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"longitude": {
|
||||
"description": "Array of longitude coordinates extracted from EXIF GPS data",
|
||||
"items": {
|
||||
"nullable": true,
|
||||
"type": "number"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"ownerId": {
|
||||
"description": "Array of owner IDs for each asset",
|
||||
"items": {
|
||||
@@ -17307,7 +17262,6 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"latitude": {
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"livePhotoVideoId": {
|
||||
@@ -17316,13 +17270,12 @@
|
||||
"type": "string"
|
||||
},
|
||||
"longitude": {
|
||||
"format": "double",
|
||||
"type": "number"
|
||||
},
|
||||
"rating": {
|
||||
"maximum": 5,
|
||||
"minimum": -1,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"visibility": {
|
||||
"allOf": [
|
||||
|
||||
@@ -1561,14 +1561,10 @@ export type TimeBucketAssetResponseDto = {
|
||||
isImage: boolean[];
|
||||
/** Array indicating whether each asset is in the trash */
|
||||
isTrashed: boolean[];
|
||||
/** Array of latitude coordinates extracted from EXIF GPS data */
|
||||
latitude?: (number | null)[];
|
||||
/** Array of live photo video asset IDs (null for non-live photos) */
|
||||
livePhotoVideoId: (string | null)[];
|
||||
/** Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective. */
|
||||
localOffsetHours: number[];
|
||||
/** Array of longitude coordinates extracted from EXIF GPS data */
|
||||
longitude?: (number | null)[];
|
||||
/** Array of owner IDs for each asset */
|
||||
ownerId: string[];
|
||||
/** Array of projection types for 360° content (e.g., "EQUIRECTANGULAR", "CUBEFACE", "CYLINDRICAL") */
|
||||
@@ -2368,7 +2364,7 @@ export function downloadAsset({ id, key, slug }: {
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Replace the asset with new file, without changing its id
|
||||
* replaceAsset
|
||||
*/
|
||||
export function replaceAsset({ id, key, slug, assetMediaReplaceDto }: {
|
||||
id: string;
|
||||
@@ -4297,7 +4293,7 @@ export function tagAssets({ id, bulkIdsDto }: {
|
||||
/**
|
||||
* This endpoint requires the `asset.read` permission.
|
||||
*/
|
||||
export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, timeBucket, userId, visibility, withCoordinates, withPartners, withStacked }: {
|
||||
export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, timeBucket, userId, visibility, withPartners, withStacked }: {
|
||||
albumId?: string;
|
||||
isFavorite?: boolean;
|
||||
isTrashed?: boolean;
|
||||
@@ -4309,7 +4305,6 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers
|
||||
timeBucket: string;
|
||||
userId?: string;
|
||||
visibility?: AssetVisibility;
|
||||
withCoordinates?: boolean;
|
||||
withPartners?: boolean;
|
||||
withStacked?: boolean;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
@@ -4328,7 +4323,6 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers
|
||||
timeBucket,
|
||||
userId,
|
||||
visibility,
|
||||
withCoordinates,
|
||||
withPartners,
|
||||
withStacked
|
||||
}))}`, {
|
||||
@@ -4338,7 +4332,7 @@ export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, pers
|
||||
/**
|
||||
* This endpoint requires the `asset.read` permission.
|
||||
*/
|
||||
export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, userId, visibility, withCoordinates, withPartners, withStacked }: {
|
||||
export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, slug, tagId, userId, visibility, withPartners, withStacked }: {
|
||||
albumId?: string;
|
||||
isFavorite?: boolean;
|
||||
isTrashed?: boolean;
|
||||
@@ -4349,7 +4343,6 @@ export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, per
|
||||
tagId?: string;
|
||||
userId?: string;
|
||||
visibility?: AssetVisibility;
|
||||
withCoordinates?: boolean;
|
||||
withPartners?: boolean;
|
||||
withStacked?: boolean;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
@@ -4367,7 +4360,6 @@ export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, per
|
||||
tagId,
|
||||
userId,
|
||||
visibility,
|
||||
withCoordinates,
|
||||
withPartners,
|
||||
withStacked
|
||||
}))}`, {
|
||||
|
||||
1104
pnpm-lock.yaml
generated
1104
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -42,11 +42,11 @@
|
||||
|
||||
- ⚠️ Цей проєкт перебуває **в дуже активній** розробці.
|
||||
- ⚠️ Очікуйте безліч помилок і глобальних змін.
|
||||
- ⚠️ **Не використовуйте цей застосунок як єдине сховище своїх фото та відео.**
|
||||
- ⚠️ **Не використовуйте цей додаток як єдине сховище своїх фото та відео.**
|
||||
- ⚠️ Завжди дотримуйтесь [плану резервного копіювання 3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) для ваших дорогоцінних фотографій та відео!
|
||||
|
||||
> [!NOTE]
|
||||
> Основну документацію, зокрема посібники зі встановлення, можна знайти за адресою https://immich.app/.
|
||||
> Основну документацію, зокрема посібники з встановлення, можна знайти за адресою https://immich.app/.
|
||||
|
||||
## Посилання
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
## Демо
|
||||
|
||||
Доступ до демо-версії [тут](https://demo.immich.app). Для мобільного застосунку ви можете використовувати `https://demo.immich.app` в якості `Server Endpoint URL`.
|
||||
Доступ до демо-версії [тут](https://demo.immich.app). Для мобільного додатку ви можете використовувати `https://demo.immich.app` в якості `Server Endpoint URL`.
|
||||
|
||||
### Облікові дані для входу
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
| Функції | Додаток | Веб |
|
||||
| :------------------------------------------------------- | ------- | --- |
|
||||
| Завантаження та перегляд відео й фото | Так | Так |
|
||||
| Автоматичне резервне копіювання при відкритті застосунку | Так | Н/Д |
|
||||
| Автоматичне резервне копіювання при відкритті додатка | Так | Н/Д |
|
||||
| Запобігання дублюванню файлів | Так | Так |
|
||||
| Вибір альбомів для резервного копіювання | Так | Н/Д |
|
||||
| Завантаження фото та відео на локальний пристрій | Так | Так |
|
||||
@@ -112,7 +112,7 @@
|
||||
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Статус перекладів" />
|
||||
</a>
|
||||
|
||||
## Активність репозиторію
|
||||
## Активність репозитарію
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:202509091104@sha256:4f9275330f1e49e7ce9840758ea91839052fe6ed40972d5bb97a9af857fa956a AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:202509021104@sha256:47d38c94775332000a93fbbeca1c796687b2d2919e3c75b6e26ab8a65d1864f3 AS dev
|
||||
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
CI=1 \
|
||||
@@ -77,7 +77,7 @@ RUN apt-get update \
|
||||
RUN dart --disable-analytics
|
||||
|
||||
# production-builder-base image
|
||||
FROM ghcr.io/immich-app/base-server-dev:202509091104@sha256:4f9275330f1e49e7ce9840758ea91839052fe6ed40972d5bb97a9af857fa956a AS prod-builder-base
|
||||
FROM ghcr.io/immich-app/base-server-dev:202509021104@sha256:47d38c94775332000a93fbbeca1c796687b2d2919e3c75b6e26ab8a65d1864f3 AS prod-builder-base
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
CI=1 \
|
||||
COREPACK_HOME=/tmp
|
||||
@@ -115,7 +115,7 @@ RUN pnpm --filter @immich/sdk --filter @immich/cli --frozen-lockfile install &&
|
||||
pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned
|
||||
|
||||
# prod base image
|
||||
FROM ghcr.io/immich-app/base-server-prod:202509091104@sha256:d1ccbac24c84f2f8277cf85281edfca62d85d7daed6a62b8efd3a81bcd3c5e0e
|
||||
FROM ghcr.io/immich-app/base-server-prod:202509021104@sha256:84f3727cff75c623f79236cdd9a2b72c84f7665057f474851016f702c67157af
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
@@ -125,7 +125,7 @@ ENV NODE_ENV=production \
|
||||
COPY --from=server-prod /output/server-pruned ./server
|
||||
COPY --from=web-prod /usr/src/app/web/build /build/www
|
||||
COPY --from=cli-prod /output/cli-pruned ./cli
|
||||
RUN ln -s ../../cli/bin/immich server/bin/immich
|
||||
RUN ln -s ./cli/bin/immich server/bin/immich
|
||||
COPY LICENSE /licenses/LICENSE.txt
|
||||
COPY LICENSE /LICENSE
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ else
|
||||
echo "skipping libmimalloc - path not found $lib_path"
|
||||
fi
|
||||
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/lib/jellyfin-ffmpeg/lib"
|
||||
SERVER_HOME="$(readlink -f "$(dirname "$0")/..")"
|
||||
SERVER_HOME=/usr/src/app/server
|
||||
|
||||
read_file_and_export() {
|
||||
fname="${!1}"
|
||||
|
||||
@@ -37,12 +37,14 @@
|
||||
"@nestjs/bullmq": "^11.0.1",
|
||||
"@nestjs/common": "^11.0.4",
|
||||
"@nestjs/core": "^11.0.4",
|
||||
"@nestjs/event-emitter": "^3.0.0",
|
||||
"@nestjs/platform-express": "^11.0.4",
|
||||
"@nestjs/platform-socket.io": "^11.0.4",
|
||||
"@nestjs/schedule": "^6.0.0",
|
||||
"@nestjs/swagger": "^11.0.2",
|
||||
"@nestjs/websockets": "^11.0.4",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.62.0",
|
||||
"@opentelemetry/context-async-hooks": "^2.0.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.203.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.203.0",
|
||||
@@ -106,16 +108,20 @@
|
||||
"socket.io": "^4.8.1",
|
||||
"tailwindcss-preset-email": "^1.4.0",
|
||||
"thumbhash": "^0.1.1",
|
||||
"typeorm": "^0.3.17",
|
||||
"ua-parser-js": "^2.0.0",
|
||||
"uuid": "^11.1.0",
|
||||
"validator": "^13.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@nestjs/cli": "^11.0.2",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.4",
|
||||
"@swc/core": "^1.4.14",
|
||||
"@testcontainers/postgresql": "^11.0.0",
|
||||
"@testcontainers/redis": "^11.0.0",
|
||||
"@types/archiver": "^6.0.0",
|
||||
"@types/async-lock": "^1.4.2",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
@@ -140,23 +146,29 @@
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/validator": "^13.15.2",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"canvas": "^3.1.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^60.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"node-addon-api": "^8.3.1",
|
||||
"node-gyp": "^11.2.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.0.2",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"rimraf": "^6.0.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"sql-formatter": "^15.0.0",
|
||||
"supertest": "^7.1.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"testcontainers": "^11.0.0",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
"unplugin-swc": "^1.4.5",
|
||||
"utimes": "^5.2.1",
|
||||
"vite-tsconfig-paths": "^5.0.0",
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
|
||||
@@ -96,9 +96,8 @@ export class AssetMediaController {
|
||||
@Put(':id/original')
|
||||
@UseInterceptors(FileUploadInterceptor)
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@EndpointLifecycle({
|
||||
addedAt: 'v1.106.0',
|
||||
deprecatedAt: 'v1.142.0',
|
||||
@EndpointLifecycle({ addedAt: 'v1.106.0' })
|
||||
@ApiOperation({
|
||||
summary: 'replaceAsset',
|
||||
description: 'Replace the asset with new file, without changing its id',
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user