Merge branch 'main' into new-upload

This commit is contained in:
Alex 2025-06-03 13:59:31 -05:00
commit aa0140d3cb
No known key found for this signature in database
GPG Key ID: 53CD082B3A5E1082
304 changed files with 13354 additions and 3868 deletions

3
.gitattributes vendored
View File

@ -9,6 +9,9 @@ mobile/lib/**/*.g.dart linguist-generated=true
mobile/lib/**/*.drift.dart -diff -merge
mobile/lib/**/*.drift.dart linguist-generated=true
mobile/drift_schemas/main/drift_schema_*.json -diff -merge
mobile/drift_schemas/main/drift_schema_*.json linguist-generated=true
open-api/typescript-sdk/fetch-client.ts -diff -merge
open-api/typescript-sdk/fetch-client.ts linguist-generated=true

View File

@ -1,118 +0,0 @@
name: 'Single arch image build'
description: 'Build single-arch image on platform appropriate runner'
inputs:
image:
description: 'Name of the image to build'
required: true
ghcr-token:
description: 'GitHub Container Registry token'
required: true
platform:
description: 'Platform to build for'
required: true
artifact-key-base:
description: 'Base key for artifact name'
required: true
context:
description: 'Path to build context'
required: true
dockerfile:
description: 'Path to Dockerfile'
required: true
build-args:
description: 'Docker build arguments'
required: false
runs:
using: 'composite'
steps:
- name: Prepare
id: prepare
shell: bash
env:
PLATFORM: ${{ inputs.platform }}
run: |
echo "platform-pair=${PLATFORM//\//-}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ inputs.ghcr-token }}
- name: Generate cache key suffix
id: cache-key-suffix
shell: bash
env:
REF: ${{ github.ref_name }}
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "cache-key-suffix=pr-${{ github.event.number }}" >> $GITHUB_OUTPUT
else
SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g')
echo "suffix=${SUFFIX}" >> $GITHUB_OUTPUT
fi
- name: Generate cache target
id: cache-target
shell: bash
env:
BUILD_ARGS: ${{ inputs.build-args }}
IMAGE: ${{ inputs.image }}
SUFFIX: ${{ steps.cache-key-suffix.outputs.suffix }}
PLATFORM_PAIR: ${{ steps.prepare.outputs.platform-pair }}
run: |
HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1)
CACHE_KEY="${PLATFORM_PAIR}-${HASH}"
echo "cache-key-base=${CACHE_KEY}" >> $GITHUB_OUTPUT
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
# Essentially just ignore the cache output (forks can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
echo "cache-to=type=registry,ref=${IMAGE}-build-cache:${CACHE_KEY}-${SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT
fi
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
env:
DOCKER_METADATA_PR_HEAD_SHA: 'true'
- name: Build and push image
id: build
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
with:
context: ${{ inputs.context }}
file: ${{ inputs.dockerfile }}
platforms: ${{ inputs.platform }}
labels: ${{ steps.meta.outputs.labels }}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
cache-from: |
type=registry,ref=${{ inputs.image }}-build-cache:${{ steps.cache-target.outputs.cache-key-base }}-${{ steps.cache-key-suffix.outputs.suffix }}
type=registry,ref=${{ inputs.image }}-build-cache:${{ steps.cache-target.outputs.cache-key-base }}-main
outputs: type=image,"name=${{ inputs.image }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
build-args: |
BUILD_ID=${{ github.run_id }}
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.meta.outputs.tags }}
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}
${{ inputs.build-args }}
- name: Export digest
shell: bash
run: | # zizmor: ignore[template-injection]
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ${{ inputs.artifact-key-base }}-${{ steps.cache-target.outputs.cache-key-base }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1

View File

@ -93,6 +93,10 @@ jobs:
run: make translation
working-directory: ./mobile
- name: Generate platform APIs
run: make pigeon
working-directory: ./mobile
- name: Build Android App Bundle
working-directory: ./mobile
env:

View File

@ -1,185 +0,0 @@
name: 'Multi-runner container image build'
on:
workflow_call:
inputs:
image:
description: 'Name of the image'
type: string
required: true
context:
description: 'Path to build context'
type: string
required: true
dockerfile:
description: 'Path to Dockerfile'
type: string
required: true
tag-suffix:
description: 'Suffix to append to the image tag'
type: string
default: ''
dockerhub-push:
description: 'Push to Docker Hub'
type: boolean
default: false
build-args:
description: 'Docker build arguments'
type: string
required: false
platforms:
description: 'Platforms to build for'
type: string
runner-mapping:
description: 'Mapping from platforms to runners'
type: string
secrets:
DOCKERHUB_USERNAME:
required: false
DOCKERHUB_TOKEN:
required: false
env:
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ inputs.image }}
DOCKERHUB_IMAGE: altran1502/${{ inputs.image }}
jobs:
matrix:
name: 'Generate matrix'
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
key: ${{ steps.artifact-key.outputs.base }}
steps:
- name: Generate build matrix
id: matrix
shell: bash
env:
PLATFORMS: ${{ inputs.platforms || 'linux/amd64,linux/arm64' }}
RUNNER_MAPPING: ${{ inputs.runner-mapping || '{"linux/amd64":"ubuntu-latest","linux/arm64":"ubuntu-24.04-arm"}' }}
run: |
matrix=$(jq -R -c \
--argjson runner_mapping "${RUNNER_MAPPING}" \
'split(",") | map({platform: ., runner: $runner_mapping[.]})' \
<<< "${PLATFORMS}")
echo "${matrix}"
echo "matrix=${matrix}" >> $GITHUB_OUTPUT
- name: Determine artifact key
id: artifact-key
shell: bash
env:
IMAGE: ${{ inputs.image }}
SUFFIX: ${{ inputs.tag-suffix }}
run: |
if [[ -n "${SUFFIX}" ]]; then
base="${IMAGE}${SUFFIX}-digests"
else
base="${IMAGE}-digests"
fi
echo "${base}"
echo "base=${base}" >> $GITHUB_OUTPUT
build:
needs: matrix
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.matrix.outputs.matrix) }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
- uses: ./.github/actions/image-build
with:
context: ${{ inputs.context }}
dockerfile: ${{ inputs.dockerfile }}
image: ${{ env.GHCR_IMAGE }}
ghcr-token: ${{ secrets.GITHUB_TOKEN }}
platform: ${{ matrix.platform }}
artifact-key-base: ${{ needs.matrix.outputs.key }}
build-args: ${{ inputs.build-args }}
merge:
needs: [matrix, build]
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.head.repo.fork }}
permissions:
contents: read
actions: read
packages: write
steps:
- name: Download digests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: ${{ runner.temp }}/digests
pattern: ${{ needs.matrix.outputs.key }}-*
merge-multiple: true
- name: Login to Docker Hub
if: ${{ inputs.dockerhub-push }}
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
env:
DOCKER_METADATA_PR_HEAD_SHA: 'true'
with:
flavor: |
# Disable latest tag
latest=false
suffix=${{ inputs.tag-suffix }}
images: |
name=${{ env.GHCR_IMAGE }}
name=${{ env.DOCKERHUB_IMAGE }},enable=${{ inputs.dockerhub-push }}
tags: |
# Tag with branch name
type=ref,event=branch
# Tag with pr-number
type=ref,event=pr
# Tag with long commit sha hash
type=sha,format=long,prefix=commit-
# Tag with git tag on release
type=ref,event=tag
type=raw,value=release,enable=${{ github.event_name == 'release' }}
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
# Process annotations
declare -a ANNOTATIONS=()
if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then
while IFS= read -r annotation; do
# Extract key and value by removing the manifest: prefix
if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then
key="${BASH_REMATCH[1]}"
value="${BASH_REMATCH[2]}"
# Use array to properly handle arguments with spaces
ANNOTATIONS+=(--annotation "index:$key=$value")
fi
done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
fi
TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
SOURCE_ARGS=$(printf "${GHCR_IMAGE}@sha256:%s " *)
docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS

View File

@ -59,13 +59,17 @@ jobs:
working-directory: ./mobile
- name: Generate translation file
run: make translation; dart format lib/generated/codegen_loader.g.dart
run: make translation
working-directory: ./mobile
- name: Run Build Runner
run: make build
working-directory: ./mobile
- name: Generate platform API
run: make pigeon
working-directory: ./mobile
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files

146
cli/package-lock.json generated
View File

@ -1370,17 +1370,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
"integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz",
"integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/type-utils": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/type-utils": "8.33.0",
"@typescript-eslint/utils": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@ -1394,7 +1394,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
"@typescript-eslint/parser": "^8.33.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@ -1410,16 +1410,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz",
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz",
"integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"debug": "^4.3.4"
},
"engines": {
@ -1434,15 +1434,16 @@
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
"integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
"node_modules/@typescript-eslint/project-service": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz",
"integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1"
"@typescript-eslint/tsconfig-utils": "^8.33.0",
"@typescript-eslint/types": "^8.33.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1452,15 +1453,50 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz",
"integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==",
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz",
"integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz",
"integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz",
"integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/utils": "8.33.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@ -1477,9 +1513,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
"integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz",
"integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==",
"dev": true,
"license": "MIT",
"engines": {
@ -1491,14 +1527,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
"integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz",
"integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"@typescript-eslint/project-service": "8.33.0",
"@typescript-eslint/tsconfig-utils": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -1544,16 +1582,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
"integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz",
"integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1"
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.33.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1568,13 +1606,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
"integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz",
"integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/types": "8.33.0",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
@ -2756,9 +2794,9 @@
}
},
"node_modules/globals": {
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz",
"integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==",
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
"integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
"dev": true,
"license": "MIT",
"engines": {
@ -4200,15 +4238,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz",
"integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz",
"integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"@typescript-eslint/utils": "8.32.1"
"@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.33.0",
"@typescript-eslint/utils": "8.33.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@ -43,6 +43,7 @@ export interface UploadOptionsDto {
concurrency: number;
progress?: boolean;
watch?: boolean;
jsonOutput?: boolean;
}
class UploadFile extends File {
@ -65,6 +66,9 @@ class UploadFile extends File {
const uploadBatch = async (files: string[], options: UploadOptionsDto) => {
const { newFiles, duplicates } = await checkForDuplicates(files, options);
const newAssets = await uploadFiles(newFiles, options);
if (options.jsonOutput) {
console.log(JSON.stringify({ newFiles, duplicates, newAssets }, undefined, 4));
}
await updateAlbums([...newAssets, ...duplicates], options);
await deleteFiles(newFiles, options);
};

View File

@ -68,6 +68,11 @@ program
.env('IMMICH_UPLOAD_CONCURRENCY')
.default(4),
)
.addOption(
new Option('-j, --json-output', 'Output detailed information in json format')
.env('IMMICH_JSON_OUTPUT')
.default(false),
)
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
.addOption(new Option('--no-progress', 'Hide progress bars').env('IMMICH_PROGRESS_BAR').default(true))
.addOption(

View File

@ -219,3 +219,10 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
Do not touch the files inside these folders under any circumstances except taking a backup. Changing or removing an asset can cause untracked and missing files.
You can think of it as App-Which-Must-Not-Be-Named, the only access to viewing, changing and deleting assets is only through the mobile or browser interface.
:::
## Backup ordering
A backup of Immich should contain both the database and the asset files. When backing these up it's possible for them to get out of sync, potentially resulting in broken assets after you restore.
The best way of dealing with this is to stop the immich-server container while you take a backup. If nothing is changing then the backup will always be in sync.
If stopping the container is not an option, then the recommended order is to back up the database first, and the filesystem second. This way, the worst case scenario is that there are files on the filesystem that the database doesn't know about. If necessary, these can be (re)uploaded manually after a restore. If the backup is done the other way around, with the filesystem first and the database second, it's possible for the restored database to reference files that aren't in the filesystem backup, thus resulting in broken assets.

View File

@ -93,6 +93,7 @@ The `.well-known/openid-configuration` part of the url is optional and will be a
## Auto Launch
When Auto Launch is enabled, the login page will automatically redirect the user to the OAuth authorization url, to login with OAuth. To access the login screen again, use the browser's back button, or navigate directly to `/auth/login?autoLaunch=0`.
Auto Launch can also be enabled on a per-request basis by navigating to `/auth/login?authLaunch=1`, this can be useful in situations where Immich is called from e.g. Nextcloud using the _External sites_ app and the _oidc_ app so as to enable users to directly interact with a logged-in instance of Immich.
## Mobile Redirect URI

View File

@ -2,6 +2,14 @@
Immich supports the Google's Cast protocol so that photos and videos can be cast to devices such as a Chromecast and a Nest Hub. This feature is considered experimental and has several important limitations listed below. Currently, this feature is only supported by the web client, support on Android and iOS is planned for the future.
## Enable Google Cast Support
Google Cast support is disabled by default. The web UI uses Google-provided scripts and must retreive them from Google servers when the page loads. This is a privacy concern for some and is thus opt-in.
You can enable Google Cast support through `Account Settings > Features > Cast > Google Cast`
<img src={require('./img/gcast-enable.webp').default} width="70%" title='Enable Google Cast Support' />
## Limitations
To use casting with Immich, there are a few prerequisites:

View File

@ -90,19 +90,22 @@ Usage: immich upload [paths...] [options]
Upload assets
Arguments:
paths One or more paths to assets to be uploaded
paths One or more paths to assets to be uploaded
Options:
-r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE)
-i, --ignore [paths...] Paths to ignore (default: [], env: IMMICH_IGNORE_PATHS)
-h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH)
-H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN)
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
-A, --album-name <name> Add all assets to specified album (env: IMMICH_ALBUM_NAME)
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
-c, --concurrency <number> Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY)
--delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS)
--help display help for command
-r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE)
-i, --ignore <pattern> Pattern to ignore (env: IMMICH_IGNORE_PATHS)
-h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH)
-H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN)
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
-A, --album-name <name> Add all assets to specified album (env: IMMICH_ALBUM_NAME)
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
-c, --concurrency <number> Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY)
-j, --json-output Output detailed information in json format (default: false, env: IMMICH_JSON_OUTPUT)
--delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS)
--no-progress Hide progress bars (env: IMMICH_PROGRESS_BAR)
--watch Watch for changes and upload automatically (default: false, env: IMMICH_WATCH_CHANGES)
--help display help for command
```
</details>
@ -172,6 +175,16 @@ By default, hidden files are skipped. If you want to include hidden files, use t
immich upload --include-hidden --recursive directory/
```
You can use the `--json-output` option to get a json printed which includes
three keys: `newFiles`, `duplicates` and `newAssets`. Due to some logging
output you will need to strip the first three lines of output to get the json.
For example to get a list of files that would be uploaded for further
processing:
```bash
immich upload --dry-run . | tail -n +4 | jq .newFiles[]
```
### Obtain the API Key
The API key can be obtained in the user setting panel on the web interface.

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

996
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,8 +16,8 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "~3.7.0",
"@docusaurus/preset-classic": "~3.7.0",
"@docusaurus/core": "~3.8.0",
"@docusaurus/preset-classic": "~3.8.0",
"@mdi/js": "^7.3.67",
"@mdi/react": "^1.6.1",
"@mdx-js/react": "^3.0.0",
@ -35,7 +35,7 @@
"url": "^0.11.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "~3.7.0",
"@docusaurus/module-type-aliases": "~3.8.0",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.7.0",
"prettier": "^3.2.4",

View File

@ -33,7 +33,7 @@ const items: Item[] = [
url: 'https://github.com/immich-app/immich/pull/17974',
text: '#17974',
},
date: new Date(2025, 5, 5),
date: new Date(2025, 4, 5),
},
{
icon: mdiMicrosoftWindows,

146
e2e/package-lock.json generated
View File

@ -1709,17 +1709,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
"integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz",
"integrity": "sha512-CACyQuqSHt7ma3Ns601xykeBK/rDeZa3w6IS6UtMQbixO5DWy+8TilKkviGDH6jtWCo8FGRKEK5cLLkPvEammQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/type-utils": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/type-utils": "8.33.0",
"@typescript-eslint/utils": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@ -1733,7 +1733,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
"@typescript-eslint/parser": "^8.33.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
@ -1749,16 +1749,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz",
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.0.tgz",
"integrity": "sha512-JaehZvf6m0yqYp34+RVnihBAChkqeH+tqqhS0GuX1qgPpwLvmTPheKEs6OeCK6hVJgXZHJ2vbjnC9j119auStQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"debug": "^4.3.4"
},
"engines": {
@ -1773,15 +1773,16 @@
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
"integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
"node_modules/@typescript-eslint/project-service": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.0.tgz",
"integrity": "sha512-d1hz0u9l6N+u/gcrk6s6gYdl7/+pp8yHheRTqP6X5hVDKALEaTn8WfGiit7G511yueBEL3OpOEpD+3/MBdoN+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1"
"@typescript-eslint/tsconfig-utils": "^8.33.0",
"@typescript-eslint/types": "^8.33.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1791,15 +1792,50 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz",
"integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==",
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.0.tgz",
"integrity": "sha512-LMi/oqrzpqxyO72ltP+dBSP6V0xiUb4saY7WLtxSfiNEBI8m321LLVFU9/QDJxjDQG9/tjSqKz/E3380TEqSTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.32.1",
"@typescript-eslint/utils": "8.32.1",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.0.tgz",
"integrity": "sha512-sTkETlbqhEoiFmGr1gsdq5HyVbSOF0145SYDJ/EQmXHtKViCaGvnyLqWFFHtEXoS0J1yU8Wyou2UGmgW88fEug==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.0.tgz",
"integrity": "sha512-lScnHNCBqL1QayuSrWeqAL5GmqNdVUQAAMTaCwdYEdWfIrSrOGzyLGRCHXcCixa5NK6i5l0AfSO2oBSjCjf4XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.33.0",
"@typescript-eslint/utils": "8.33.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@ -1816,9 +1852,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
"integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.0.tgz",
"integrity": "sha512-DKuXOKpM5IDT1FA2g9x9x1Ug81YuKrzf4mYX8FAVSNu5Wo/LELHWQyM1pQaDkI42bX15PWl0vNPt1uGiIFUOpg==",
"dev": true,
"license": "MIT",
"engines": {
@ -1830,14 +1866,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
"integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.0.tgz",
"integrity": "sha512-vegY4FQoB6jL97Tu/lWRsAiUUp8qJTqzAmENH2k59SJhw0Th1oszb9Idq/FyyONLuNqT1OADJPXfyUNOR8SzAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/visitor-keys": "8.32.1",
"@typescript-eslint/project-service": "8.33.0",
"@typescript-eslint/tsconfig-utils": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/visitor-keys": "8.33.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -1883,16 +1921,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
"integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.0.tgz",
"integrity": "sha512-lPFuQaLA9aSNa7D5u2EpRiqdAUhzShwGg/nhpBlc4GR6kcTABttCuyjFs8BcEZ8VWrjCBof/bePhP3Q3fS+Yrw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.32.1",
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/typescript-estree": "8.32.1"
"@typescript-eslint/scope-manager": "8.33.0",
"@typescript-eslint/types": "8.33.0",
"@typescript-eslint/typescript-estree": "8.33.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1907,13 +1945,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
"integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.0.tgz",
"integrity": "sha512-7RW7CMYoskiz5OOGAWjJFxgb7c5UNjTG292gYhWeOAcFmYCtVCSqjqSBj5zMhxbXo2JOW95YYrUWJfU0zrpaGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.32.1",
"@typescript-eslint/types": "8.33.0",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
@ -3834,9 +3872,9 @@
}
},
"node_modules/globals": {
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz",
"integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==",
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz",
"integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==",
"dev": true,
"license": "MIT",
"engines": {
@ -6779,15 +6817,15 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.32.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz",
"integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==",
"version": "8.33.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.0.tgz",
"integrity": "sha512-5YmNhF24ylCsvdNW2oJwMzTbaeO4bg90KeGtMjUw0AGtHksgEPLRTUil+coHwCfiu4QjVJFnjp94DmU6zV7DhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.32.1",
"@typescript-eslint/parser": "8.32.1",
"@typescript-eslint/utils": "8.32.1"
"@typescript-eslint/eslint-plugin": "8.33.0",
"@typescript-eslint/parser": "8.33.0",
"@typescript-eslint/utils": "8.33.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"

View File

@ -428,6 +428,15 @@ describe('/albums', () => {
order: AssetOrder.Desc,
});
});
it('should not be able to share album with owner', async () => {
const { status, body } = await request(app)
.post('/albums')
.send({ albumName: 'New album', albumUsers: [{ role: AlbumUserRole.Editor, userId: user1.userId }] })
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Cannot share album with owner'));
});
});
describe('PUT /albums/:id/assets', () => {

View File

@ -103,6 +103,7 @@ export const loginResponseDto = {
accessToken: expect.any(String),
name: 'Immich Admin',
isAdmin: true,
isOnboarded: false,
profileImagePath: '',
shouldChangePassword: true,
userEmail: 'admin@immich.cloud',

View File

@ -33,7 +33,9 @@ test.describe('Registration', () => {
// onboarding
await expect(page).toHaveURL('/auth/onboarding');
await page.getByRole('button', { name: 'Theme' }).click();
await page.getByRole('button', { name: 'Privacy' }).click();
await page.getByRole('button', { name: 'Language' }).click();
await page.getByRole('button', { name: 'Server Privacy' }).click();
await page.getByRole('button', { name: 'User Privacy' }).click();
await page.getByRole('button', { name: 'Storage Template' }).click();
await page.getByRole('button', { name: 'Done' }).click();
@ -77,6 +79,13 @@ test.describe('Registration', () => {
await page.getByLabel('Password').fill('new-password');
await page.getByRole('button', { name: 'Login' }).click();
// onboarding
await expect(page).toHaveURL('/auth/onboarding');
await page.getByRole('button', { name: 'Theme' }).click();
await page.getByRole('button', { name: 'Language' }).click();
await page.getByRole('button', { name: 'User Privacy' }).click();
await page.getByRole('button', { name: 'Done' }).click();
// success
await expect(page).toHaveURL(/\/photos/);
});

View File

@ -26,7 +26,6 @@
"add_to_album": "Add to album",
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"add_to_locked_folder": "Add to locked folder",
"add_to_shared_album": "Add to shared album",
"add_url": "Add URL",
"added_to_archive": "Added to archive",
@ -44,9 +43,7 @@
"backup_database_enable_description": "Enable database dumps",
"backup_keep_last_amount": "Amount of previous dumps to keep",
"backup_settings": "Database Dump Settings",
"backup_settings_description": "Manage database dump settings. Note: These jobs are not monitored and you will not be notified of failure.",
"check_all": "Check All",
"cleanup": "Cleanup",
"backup_settings_description": "Manage database dump settings.",
"cleared_jobs": "Cleared jobs for: {job}",
"config_set_by_file": "Config is currently set by a config file",
"confirm_delete_library": "Are you sure you want to delete {library} library?",
@ -62,14 +59,12 @@
"disable_login": "Disable login",
"duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search",
"exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.",
"external_library_created_at": "External library (created on {date})",
"external_library_management": "External Library Management",
"face_detection": "Face detection",
"face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.",
"facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"Reset\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.",
"failed_job_command": "Command {command} failed for job: {job}",
"force_delete_user_warning": "WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be recovered.",
"forcing_refresh_library_files": "Forcing refresh of all library files",
"image_format": "Format",
"image_format_description": "WebP produces smaller files than JPEG, but is slower to encode.",
"image_fullsize_description": "Full-size image with stripped metadata, used when zoomed in",
@ -210,8 +205,6 @@
"oauth_storage_quota_default_description": "Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota).",
"oauth_timeout": "Request Timeout",
"oauth_timeout_description": "Timeout for requests in milliseconds",
"offline_paths": "Offline Paths",
"offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.",
"password_enable_description": "Login with email and password",
"password_settings": "Password Login",
"password_settings_description": "Manage password login settings",
@ -221,9 +214,6 @@
"refreshing_all_libraries": "Refreshing all libraries",
"registration": "Admin Registration",
"registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
"repair_all": "Repair All",
"repair_matched_items": "Matched {count, plural, one {# item} other {# items}}",
"repaired_items": "Repaired {count, plural, one {# item} other {# items}}",
"require_password_change_on_login": "Require user to change password on first login",
"reset_settings_to_default": "Reset settings to default",
"reset_settings_to_recent_saved": "Reset settings to the recent saved settings",
@ -264,7 +254,6 @@
"template_email_invite_album": "Invite Album Template",
"template_email_preview": "Preview",
"template_email_settings": "Email Templates",
"template_email_settings_description": "Manage custom email notification templates",
"template_email_update_album": "Update Album Template",
"template_email_welcome": "Welcome email template",
"template_settings": "Notification Templates",
@ -273,7 +262,6 @@
"theme_custom_css_settings_description": "Cascading Style Sheets allow the design of Immich to be customized.",
"theme_settings": "Theme Settings",
"theme_settings_description": "Manage customization of the Immich web interface",
"these_files_matched_by_checksum": "These files are matched by their checksums",
"thumbnail_generation_job": "Generate Thumbnails",
"thumbnail_generation_job_description": "Generate large, small and blurred thumbnails for each asset, as well as thumbnails for each person",
"transcoding_acceleration_api": "Acceleration API",
@ -341,8 +329,6 @@
"trash_number_of_days_description": "Number of days to keep the assets in trash before permanently removing them",
"trash_settings": "Trash Settings",
"trash_settings_description": "Manage trash settings",
"untracked_files": "Untracked Files",
"untracked_files_description": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug",
"user_cleanup_job": "User cleanup",
"user_delete_delay": "<b>{user}</b>'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.",
"user_delete_delay_settings": "Delete delay",
@ -401,10 +387,6 @@
"album_remove_user": "Remove user?",
"album_remove_user_confirmation": "Are you sure you want to remove {user}?",
"album_share_no_users": "Looks like you have shared this album with all users or you don't have any user to share with.",
"album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{count} items",
"album_thumbnail_card_shared": " · Shared",
"album_thumbnail_shared_by": "Shared by {user}",
"album_updated": "Album updated",
"album_updated_setting_description": "Receive an email notification when a shared album has new assets",
"album_user_left": "Left {album}",
@ -495,6 +477,7 @@
"authorized_devices": "Authorized Devices",
"automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
"automatic_endpoint_switching_title": "Automatic URL switching",
"autoplay_slideshow": "Autoplay slideshow",
"back": "Back",
"back_close_deselect": "Back, close, or deselect",
"background_location_permission": "Background location permission",
@ -576,21 +559,17 @@
"bulk_keep_duplicates_confirmation": "Are you sure you want to keep {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will resolve all duplicate groups without deleting anything.",
"bulk_trash_duplicates_confirmation": "Are you sure you want to bulk trash {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and trash all other duplicates.",
"buy": "Purchase Immich",
"cache_settings_album_thumbnails": "Library page thumbnails ({count} assets)",
"cache_settings_clear_cache_button": "Clear cache",
"cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
"cache_settings_duplicated_assets_clear_button": "CLEAR",
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
"cache_settings_duplicated_assets_title": "Duplicated Assets ({count})",
"cache_settings_image_cache_size": "Image cache size ({count} assets)",
"cache_settings_statistics_album": "Library thumbnails",
"cache_settings_statistics_assets": "{count} assets ({size})",
"cache_settings_statistics_full": "Full images",
"cache_settings_statistics_shared": "Shared album thumbnails",
"cache_settings_statistics_thumbnail": "Thumbnails",
"cache_settings_statistics_title": "Cache usage",
"cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application",
"cache_settings_thumbnail_size": "Thumbnail cache size ({count} assets)",
"cache_settings_tile_subtitle": "Control the local storage behaviour",
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Caching Settings",
@ -604,6 +583,7 @@
"cannot_undo_this_action": "You cannot undo this action!",
"cannot_update_the_description": "Cannot update the description",
"cast": "Cast",
"cast_description": "Configure available cast destinations",
"change_date": "Change date",
"change_description": "Change description",
"change_display_order": "Change display order",
@ -621,7 +601,6 @@
"change_pin_code": "Change PIN code",
"change_your_password": "Change your password",
"changed_visibility_successfully": "Changed visibility successfully",
"check_all": "Check All",
"check_corrupt_asset_backup": "Check for corrupt asset backups",
"check_corrupt_asset_backup_button": "Perform check",
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
@ -667,7 +646,6 @@
"contain": "Contain",
"context": "Context",
"continue": "Continue",
"control_bottom_app_bar_album_info_shared": "{count} items · Shared",
"control_bottom_app_bar_create_new_album": "Create new album",
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
"control_bottom_app_bar_delete_from_local": "Delete from device",
@ -778,7 +756,6 @@
"download_enqueue": "Download enqueued",
"download_error": "Download Error",
"download_failed": "Download failed",
"download_filename": "file: {filename}",
"download_finished": "Download finished",
"download_include_embedded_motion_videos": "Embedded videos",
"download_include_embedded_motion_videos_description": "Include videos embedded in motion photos as a separate file",
@ -854,7 +831,6 @@
"cant_get_number_of_comments": "Can't get number of comments",
"cant_search_people": "Can't search people",
"cant_search_places": "Can't search places",
"cleared_jobs": "Cleared jobs for: {job}",
"error_adding_assets_to_album": "Error adding assets to album",
"error_adding_users_to_album": "Error adding users to album",
"error_deleting_shared_user": "Error deleting shared user",
@ -863,7 +839,6 @@
"error_removing_assets_from_album": "Error removing assets from album, check console for more details",
"error_selecting_all_assets": "Error selecting all assets",
"exclusion_pattern_already_exists": "This exclusion pattern already exists.",
"failed_job_command": "Command {command} failed for job: {job}",
"failed_to_create_album": "Failed to create album",
"failed_to_create_shared_link": "Failed to create shared link",
"failed_to_edit_shared_link": "Failed to edit shared link",
@ -882,7 +857,6 @@
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation",
"profile_picture_transparent_pixels": "Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.",
"quota_higher_than_disk_size": "You set a quota higher than the disk size",
"repair_unable_to_check_items": "Unable to check {count, select, one {item} other {items}}",
"unable_to_add_album_users": "Unable to add users to album",
"unable_to_add_assets_to_shared_link": "Unable to add assets to shared link",
"unable_to_add_comment": "Unable to add comment",
@ -901,7 +875,6 @@
"unable_to_change_visibility": "Unable to change the visibility for {count, plural, one {# person} other {# people}}",
"unable_to_complete_oauth_login": "Unable to complete OAuth login",
"unable_to_connect": "Unable to connect",
"unable_to_connect_to_server": "Unable to connect to server",
"unable_to_copy_to_clipboard": "Cannot copy to clipboard, make sure you are accessing the page through https",
"unable_to_create_admin_account": "Unable to create admin account",
"unable_to_create_api_key": "Unable to create a new API Key",
@ -925,14 +898,9 @@
"unable_to_hide_person": "Unable to hide person",
"unable_to_link_motion_video": "Unable to link motion video",
"unable_to_link_oauth_account": "Unable to link OAuth account",
"unable_to_load_album": "Unable to load album",
"unable_to_load_asset_activity": "Unable to load asset activity",
"unable_to_load_items": "Unable to load items",
"unable_to_load_liked_status": "Unable to load liked status",
"unable_to_log_out_all_devices": "Unable to log out all devices",
"unable_to_log_out_device": "Unable to log out device",
"unable_to_login_with_oauth": "Unable to login with OAuth",
"unable_to_move_to_locked_folder": "Unable to move to locked folder",
"unable_to_play_video": "Unable to play video",
"unable_to_reassign_assets_existing_person": "Unable to reassign assets to {name, select, null {an existing person} other {{name}}}",
"unable_to_reassign_assets_new_person": "Unable to reassign assets to a new person",
@ -940,11 +908,9 @@
"unable_to_remove_album_users": "Unable to remove users from album",
"unable_to_remove_api_key": "Unable to remove API Key",
"unable_to_remove_assets_from_shared_link": "Unable to remove assets from shared link",
"unable_to_remove_deleted_assets": "Unable to remove offline files",
"unable_to_remove_library": "Unable to remove library",
"unable_to_remove_partner": "Unable to remove partner",
"unable_to_remove_reaction": "Unable to remove reaction",
"unable_to_repair_items": "Unable to repair items",
"unable_to_reset_password": "Unable to reset password",
"unable_to_reset_pin_code": "Unable to reset PIN code",
"unable_to_resolve_duplicate": "Unable to resolve duplicate",
@ -1027,6 +993,8 @@
"folders": "Folders",
"folders_feature_description": "Browsing the folder view for the photos and videos on the file system",
"forward": "Forward",
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
"general": "General",
"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",
@ -1115,6 +1083,12 @@
"invalid_date_format": "Invalid date format",
"invite_people": "Invite People",
"invite_to_album": "Invite to album",
"ios_debug_info_fetch_ran_at": "Fetch ran {dateTime}",
"ios_debug_info_last_sync_at": "Last sync {dateTime}",
"ios_debug_info_no_processes_queued": "No background processes queued",
"ios_debug_info_no_sync_yet": "No background sync job has run yet",
"ios_debug_info_processes_queued": "{count, plural, one {{count} background process queued} other {{count} background processes queued}}",
"ios_debug_info_processing_ran_at": "Processing ran {dateTime}",
"items_count": "{count, plural, one {# item} other {# items}}",
"jobs": "Jobs",
"keep": "Keep",
@ -1123,6 +1097,9 @@
"kept_this_deleted_others": "Kept this asset and deleted {count, plural, one {# asset} other {# assets}}",
"keyboard_shortcuts": "Keyboard shortcuts",
"language": "Language",
"language_no_results_subtitle": "Try adjusting your search term",
"language_no_results_title": "No languages found",
"language_search_hint": "Search languages...",
"language_setting_description": "Select your preferred language",
"last_seen": "Last seen",
"latest_version": "Latest Version",
@ -1158,7 +1135,7 @@
"location_picker_longitude_error": "Enter a valid longitude",
"location_picker_longitude_hint": "Enter your longitude here",
"lock": "Lock",
"locked_folder": "Locked folder",
"locked_folder": "Locked Folder",
"log_out": "Log out",
"log_out_all_devices": "Log Out All Devices",
"logged_out_all_devices": "Logged out all devices",
@ -1316,15 +1293,15 @@
"oauth": "OAuth",
"official_immich_resources": "Official Immich Resources",
"offline": "Offline",
"offline_paths": "Offline paths",
"offline_paths_description": "These results may be due to manual deletion of files that are not part of an external library.",
"ok": "Ok",
"oldest_first": "Oldest first",
"on_this_device": "On this device",
"onboarding": "Onboarding",
"onboarding_privacy_description": "The following (optional) features rely on external services, and can be disabled at any time in the administration settings.",
"onboarding_locale_description": "Select your preferred language. You can change this later in your settings.",
"onboarding_privacy_description": "The following (optional) features rely on external services, and can be disabled at any time in settings.",
"onboarding_server_welcome_description": "Let's get your instance set up with some common settings.",
"onboarding_theme_description": "Choose a color theme for your instance. You can change this later in your settings.",
"onboarding_welcome_description": "Let's get your instance set up with some common settings.",
"onboarding_user_welcome_description": "Let's get you started!",
"onboarding_welcome_user": "Welcome, {user}",
"online": "Online",
"only_favorites": "Only favorites",
@ -1636,6 +1613,7 @@
"server_info_box_server_url": "Server URL",
"server_offline": "Server Offline",
"server_online": "Server Online",
"server_privacy": "Server Privacy",
"server_stats": "Server Stats",
"server_version": "Server Version",
"set": "Set",
@ -1653,7 +1631,6 @@
"setting_image_viewer_title": "Images",
"setting_languages_apply": "Apply",
"setting_languages_subtitle": "Change the app's language",
"setting_languages_title": "Languages",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {duration}",
"setting_notifications_notify_hours": "{count} hours",
"setting_notifications_notify_immediately": "immediately",
@ -1880,8 +1857,6 @@
"unselect_all_duplicates": "Unselect all duplicates",
"unstack": "Un-stack",
"unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}",
"untracked_files": "Untracked files",
"untracked_files_decription": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug",
"up_next": "Up next",
"updated_at": "Updated",
"updated_password": "Updated password",
@ -1909,6 +1884,7 @@
"user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}",
"user_pin_code_settings": "PIN Code",
"user_pin_code_settings_description": "Manage your PIN code",
"user_privacy": "User Privacy",
"user_purchase_settings": "Purchase",
"user_purchase_settings_description": "Manage your purchase",
"user_role_set": "Set {user} as {role}",
@ -1924,11 +1900,6 @@
"version": "Version",
"version_announcement_closing": "Your friend, Alex",
"version_announcement_message": "Hi there! A new version of Immich is available. Please take some time to read the <link>release notes</link> to ensure your setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your Immich instance automatically.",
"version_announcement_overlay_release_notes": "release notes",
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
"version_announcement_overlay_text_2": "please take your time to visit the ",
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
"version_announcement_overlay_title": "New Server Version Available 🎉",
"version_history": "Version History",
"version_history_item": "Installed {version} on {date}",
"video": "Video",

View File

@ -55,6 +55,7 @@ custom_lint:
restrict: package:photo_manager
allowed:
# required / wanted
- 'lib/infrastructure/repositories/album_media.repository.dart'
- 'lib/repositories/{album,asset,file}_media.repository.dart'
# acceptable exceptions for the time being
- lib/entities/asset.entity.dart # to provide local AssetEntity for now

View File

@ -1,103 +1,106 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
id 'com.google.devtools.ksp'
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
id 'com.google.devtools.ksp'
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withInputStream { localProperties.load(it) }
localPropertiesFile.withInputStream { localProperties.load(it) }
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
flutterVersionName = '1.0'
}
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.withInputStream { keystoreProperties.load(it) }
keystorePropertiesFile.withInputStream { keystoreProperties.load(it) }
}
android {
compileSdkVersion 35
compileSdkVersion 35
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
coreLibraryDesugaringEnabled true
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
coreLibraryDesugaringEnabled true
}
kotlinOptions {
jvmTarget = '17'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
applicationId "app.alextran.immich"
minSdkVersion 26
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
signingConfigs {
release {
def keyAliasVal = System.getenv("ALIAS")
def keyPasswordVal = System.getenv("ANDROID_KEY_PASSWORD")
def storePasswordVal = System.getenv("ANDROID_STORE_PASSWORD")
keyAlias keyAliasVal ? keyAliasVal : keystoreProperties['keyAlias']
keyPassword keyPasswordVal ? keyPasswordVal : keystoreProperties['keyPassword']
storeFile file("../key.jks") ? file("../key.jks") : file(keystoreProperties['storeFile'])
storePassword storePasswordVal ? storePasswordVal : keystoreProperties['storePassword']
}
}
buildTypes {
debug {
applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG'
}
kotlinOptions {
jvmTarget = '17'
release {
signingConfig signingConfigs.release
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
applicationId "app.alextran.immich"
minSdkVersion 26
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
signingConfigs {
release {
def keyAliasVal = System.getenv("ALIAS")
def keyPasswordVal = System.getenv("ANDROID_KEY_PASSWORD")
def storePasswordVal = System.getenv("ANDROID_STORE_PASSWORD")
keyAlias keyAliasVal ? keyAliasVal : keystoreProperties['keyAlias']
keyPassword keyPasswordVal ? keyPasswordVal : keystoreProperties['keyPassword']
storeFile file("../key.jks") ? file("../key.jks") : file(keystoreProperties['storeFile'])
storePassword storePasswordVal ? storePasswordVal : keystoreProperties['storePassword']
}
}
buildTypes {
debug {
applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG'
}
release {
signingConfig signingConfigs.release
}
}
namespace 'app.alextran.immich'
}
namespace 'app.alextran.immich'
}
flutter {
source '../..'
source '../..'
}
dependencies {
def kotlin_version = '2.0.20'
def kotlin_coroutines_version = '1.9.0'
def work_version = '2.9.1'
def concurrent_version = '1.2.0'
def guava_version = '33.3.1-android'
def glide_version = '4.16.0'
def kotlin_version = '2.0.20'
def kotlin_coroutines_version = '1.9.0'
def work_version = '2.9.1'
def concurrent_version = '1.2.0'
def guava_version = '33.3.1-android'
def glide_version = '4.16.0'
def serialization_version = '1.8.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "androidx.concurrent:concurrent-futures:$concurrent_version"
implementation "com.google.guava:guava:$guava_version"
implementation "com.github.bumptech.glide:glide:$glide_version"
ksp "com.github.bumptech.glide:ksp:$glide_version"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "androidx.concurrent:concurrent-futures:$concurrent_version"
implementation "com.google.guava:guava:$guava_version"
implementation "com.github.bumptech.glide:glide:$glide_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version"
ksp "com.github.bumptech.glide:ksp:$glide_version"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2'
}
// This is uncommented in F-Droid build script

View File

@ -1,6 +1,11 @@
package app.alextran.immich
import android.os.Build
import android.os.ext.SdkExtensions
import androidx.annotation.NonNull
import app.alextran.immich.sync.NativeSyncApi
import app.alextran.immich.sync.NativeSyncApiImpl26
import app.alextran.immich.sync.NativeSyncApiImpl30
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
@ -10,5 +15,13 @@ class MainActivity : FlutterFragmentActivity() {
flutterEngine.plugins.add(BackgroundServicePlugin())
flutterEngine.plugins.add(HttpSSLOptionsPlugin())
// No need to set up method channel here as it's now handled in the plugin
val nativeSyncApiImpl =
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 1) {
NativeSyncApiImpl26(this)
} else {
NativeSyncApiImpl30(this)
}
NativeSyncApi.setUp(flutterEngine.dartExecutor.binaryMessenger, nativeSyncApiImpl)
}
}

View File

@ -0,0 +1,393 @@
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
package app.alextran.immich.sync
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMethodCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private object MessagesPigeonUtils {
fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
fun wrapError(exception: Throwable): List<Any?> {
return if (exception is FlutterError) {
listOf(
exception.code,
exception.message,
exception.details
)
} else {
listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
)
}
}
fun deepEquals(a: Any?, b: Any?): Boolean {
if (a is ByteArray && b is ByteArray) {
return a.contentEquals(b)
}
if (a is IntArray && b is IntArray) {
return a.contentEquals(b)
}
if (a is LongArray && b is LongArray) {
return a.contentEquals(b)
}
if (a is DoubleArray && b is DoubleArray) {
return a.contentEquals(b)
}
if (a is Array<*> && b is Array<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
}
if (a is List<*> && b is List<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
}
if (a is Map<*, *> && b is Map<*, *>) {
return a.size == b.size && a.all {
(b as Map<Any?, Any?>).containsKey(it.key) &&
deepEquals(it.value, b[it.key])
}
}
return a == b
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
/** Generated class from Pigeon that represents data sent in messages. */
data class PlatformAsset (
val id: String,
val name: String,
val type: Long,
val createdAt: Long? = null,
val updatedAt: Long? = null,
val durationInSeconds: Long
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): PlatformAsset {
val id = pigeonVar_list[0] as String
val name = pigeonVar_list[1] as String
val type = pigeonVar_list[2] as Long
val createdAt = pigeonVar_list[3] as Long?
val updatedAt = pigeonVar_list[4] as Long?
val durationInSeconds = pigeonVar_list[5] as Long
return PlatformAsset(id, name, type, createdAt, updatedAt, durationInSeconds)
}
}
fun toList(): List<Any?> {
return listOf(
id,
name,
type,
createdAt,
updatedAt,
durationInSeconds,
)
}
override fun equals(other: Any?): Boolean {
if (other !is PlatformAsset) {
return false
}
if (this === other) {
return true
}
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
override fun hashCode(): Int = toList().hashCode()
}
/** Generated class from Pigeon that represents data sent in messages. */
data class PlatformAlbum (
val id: String,
val name: String,
val updatedAt: Long? = null,
val isCloud: Boolean,
val assetCount: Long
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): PlatformAlbum {
val id = pigeonVar_list[0] as String
val name = pigeonVar_list[1] as String
val updatedAt = pigeonVar_list[2] as Long?
val isCloud = pigeonVar_list[3] as Boolean
val assetCount = pigeonVar_list[4] as Long
return PlatformAlbum(id, name, updatedAt, isCloud, assetCount)
}
}
fun toList(): List<Any?> {
return listOf(
id,
name,
updatedAt,
isCloud,
assetCount,
)
}
override fun equals(other: Any?): Boolean {
if (other !is PlatformAlbum) {
return false
}
if (this === other) {
return true
}
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
override fun hashCode(): Int = toList().hashCode()
}
/** Generated class from Pigeon that represents data sent in messages. */
data class SyncDelta (
val hasChanges: Boolean,
val updates: List<PlatformAsset>,
val deletes: List<String>,
val assetAlbums: Map<String, List<String>>
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): SyncDelta {
val hasChanges = pigeonVar_list[0] as Boolean
val updates = pigeonVar_list[1] as List<PlatformAsset>
val deletes = pigeonVar_list[2] as List<String>
val assetAlbums = pigeonVar_list[3] as Map<String, List<String>>
return SyncDelta(hasChanges, updates, deletes, assetAlbums)
}
}
fun toList(): List<Any?> {
return listOf(
hasChanges,
updates,
deletes,
assetAlbums,
)
}
override fun equals(other: Any?): Boolean {
if (other !is SyncDelta) {
return false
}
if (this === other) {
return true
}
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
override fun hashCode(): Int = toList().hashCode()
}
private open class MessagesPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
129.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
PlatformAsset.fromList(it)
}
}
130.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
PlatformAlbum.fromList(it)
}
}
131.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
SyncDelta.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) {
is PlatformAsset -> {
stream.write(129)
writeValue(stream, value.toList())
}
is PlatformAlbum -> {
stream.write(130)
writeValue(stream, value.toList())
}
is SyncDelta -> {
stream.write(131)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface NativeSyncApi {
fun shouldFullSync(): Boolean
fun getMediaChanges(): SyncDelta
fun checkpointSync()
fun clearSyncCheckpoint()
fun getAssetIdsForAlbum(albumId: String): List<String>
fun getAlbums(): List<PlatformAlbum>
fun getAssetsCountSince(albumId: String, timestamp: Long): Long
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset>
companion object {
/** The codec used by NativeSyncApi. */
val codec: MessageCodec<Any?> by lazy {
MessagesPigeonCodec()
}
/** Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val taskQueue = binaryMessenger.makeBackgroundTaskQueue()
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.shouldFullSync())
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.getMediaChanges())
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.checkpointSync()
listOf(null)
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
api.clearSyncCheckpoint()
listOf(null)
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val albumIdArg = args[0] as String
val wrapped: List<Any?> = try {
listOf(api.getAssetIdsForAlbum(albumIdArg))
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.getAlbums())
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val albumIdArg = args[0] as String
val timestampArg = args[1] as Long
val wrapped: List<Any?> = try {
listOf(api.getAssetsCountSince(albumIdArg, timestampArg))
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val albumIdArg = args[0] as String
val updatedTimeCondArg = args[1] as Long?
val wrapped: List<Any?> = try {
listOf(api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg))
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@ -0,0 +1,24 @@
package app.alextran.immich.sync
import android.content.Context
class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi {
override fun shouldFullSync(): Boolean {
return true
}
// No-op for Android 10 and below
override fun checkpointSync() {
// Cannot throw exception as this is called from the Dart side
// during the full sync process as well
}
override fun clearSyncCheckpoint() {
// No-op for Android 10 and below
}
override fun getMediaChanges(): SyncDelta {
throw IllegalStateException("Method not supported on this Android version.")
}
}

View File

@ -0,0 +1,89 @@
package app.alextran.immich.sync
import android.content.Context
import android.os.Build
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresExtension
import kotlinx.serialization.json.Json
@RequiresApi(Build.VERSION_CODES.Q)
@RequiresExtension(extension = Build.VERSION_CODES.R, version = 1)
class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi {
private val ctx: Context = context.applicationContext
private val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
companion object {
const val SHARED_PREF_NAME = "Immich::MediaManager"
const val SHARED_PREF_MEDIA_STORE_VERSION_KEY = "MediaStore::getVersion"
const val SHARED_PREF_MEDIA_STORE_GEN_KEY = "MediaStore::getGeneration"
}
private fun getSavedGenerationMap(): Map<String, Long> {
return prefs.getString(SHARED_PREF_MEDIA_STORE_GEN_KEY, null)?.let {
Json.decodeFromString<Map<String, Long>>(it)
} ?: emptyMap()
}
override fun clearSyncCheckpoint() {
prefs.edit().apply {
remove(SHARED_PREF_MEDIA_STORE_VERSION_KEY)
remove(SHARED_PREF_MEDIA_STORE_GEN_KEY)
apply()
}
}
override fun shouldFullSync(): Boolean =
MediaStore.getVersion(ctx) != prefs.getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null)
override fun checkpointSync() {
val genMap = MediaStore.getExternalVolumeNames(ctx)
.associateWith { MediaStore.getGeneration(ctx, it) }
prefs.edit().apply {
putString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, MediaStore.getVersion(ctx))
putString(SHARED_PREF_MEDIA_STORE_GEN_KEY, Json.encodeToString(genMap))
apply()
}
}
override fun getMediaChanges(): SyncDelta {
val genMap = getSavedGenerationMap()
val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
val changed = mutableListOf<PlatformAsset>()
val deleted = mutableListOf<String>()
val assetAlbums = mutableMapOf<String, List<String>>()
var hasChanges = genMap.keys != currentVolumes
for (volume in currentVolumes) {
val currentGen = MediaStore.getGeneration(ctx, volume)
val storedGen = genMap[volume] ?: 0
if (currentGen <= storedGen) {
continue
}
hasChanges = true
val selection =
"$MEDIA_SELECTION AND (${MediaStore.MediaColumns.GENERATION_MODIFIED} > ? OR ${MediaStore.MediaColumns.GENERATION_ADDED} > ?)"
val selectionArgs = arrayOf(
*MEDIA_SELECTION_ARGS,
storedGen.toString(),
storedGen.toString()
)
getAssets(getCursor(volume, selection, selectionArgs)).forEach {
when (it) {
is AssetResult.ValidAsset -> {
changed.add(it.asset)
assetAlbums[it.asset.id] = listOf(it.albumId)
}
is AssetResult.InvalidAsset -> deleted.add(it.assetId)
}
}
}
// Unmounted volumes are handled in dart when the album is removed
return SyncDelta(hasChanges, changed, deleted, assetAlbums)
}
}

View File

@ -0,0 +1,177 @@
package app.alextran.immich.sync
import android.annotation.SuppressLint
import android.content.Context
import android.database.Cursor
import android.provider.MediaStore
import java.io.File
sealed class AssetResult {
data class ValidAsset(val asset: PlatformAsset, val albumId: String) : AssetResult()
data class InvalidAsset(val assetId: String) : AssetResult()
}
@SuppressLint("InlinedApi")
open class NativeSyncApiImplBase(context: Context) {
private val ctx: Context = context.applicationContext
companion object {
const val MEDIA_SELECTION =
"(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)"
val MEDIA_SELECTION_ARGS = arrayOf(
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(),
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()
)
const val BUCKET_SELECTION = "(${MediaStore.Files.FileColumns.BUCKET_ID} = ?)"
val ASSET_PROJECTION = arrayOf(
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.DATE_TAKEN,
MediaStore.MediaColumns.DATE_ADDED,
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.Files.FileColumns.MEDIA_TYPE,
MediaStore.MediaColumns.BUCKET_ID,
MediaStore.MediaColumns.DURATION
)
}
protected fun getCursor(
volume: String,
selection: String,
selectionArgs: Array<String>,
projection: Array<String> = ASSET_PROJECTION,
sortOrder: String? = null
): Cursor? = ctx.contentResolver.query(
MediaStore.Files.getContentUri(volume),
projection,
selection,
selectionArgs,
sortOrder,
)
protected fun getAssets(cursor: Cursor?): Sequence<AssetResult> {
return sequence {
cursor?.use { c ->
val idColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
val dataColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
val nameColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
val dateTakenColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN)
val dateAddedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
val dateModifiedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
val mediaTypeColumn = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
val bucketIdColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID)
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
while (c.moveToNext()) {
val id = c.getLong(idColumn).toString()
val path = c.getString(dataColumn)
if (path.isNullOrBlank() || !File(path).exists()) {
yield(AssetResult.InvalidAsset(id))
continue
}
val mediaType = c.getInt(mediaTypeColumn)
val name = c.getString(nameColumn)
// Date taken is milliseconds since epoch, Date added is seconds since epoch
val createdAt = (c.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000))
?: c.getLong(dateAddedColumn)
// Date modified is seconds since epoch
val modifiedAt = c.getLong(dateModifiedColumn)
// Duration is milliseconds
val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0
else c.getLong(durationColumn) / 1000
val bucketId = c.getString(bucketIdColumn)
val asset = PlatformAsset(id, name, mediaType.toLong(), createdAt, modifiedAt, duration)
yield(AssetResult.ValidAsset(asset, bucketId))
}
}
}
}
fun getAlbums(): List<PlatformAlbum> {
val albums = mutableListOf<PlatformAlbum>()
val albumsCount = mutableMapOf<String, Int>()
val projection = arrayOf(
MediaStore.Files.FileColumns.BUCKET_ID,
MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME,
MediaStore.Files.FileColumns.DATE_MODIFIED,
)
val selection =
"(${MediaStore.Files.FileColumns.BUCKET_ID} IS NOT NULL) AND $MEDIA_SELECTION"
getCursor(
MediaStore.VOLUME_EXTERNAL,
selection,
MEDIA_SELECTION_ARGS,
projection,
"${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC"
)?.use { cursor ->
val bucketIdColumn =
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_ID)
val bucketNameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME)
val dateModified =
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED)
while (cursor.moveToNext()) {
val id = cursor.getString(bucketIdColumn)
val count = albumsCount.getOrDefault(id, 0)
if (count != 0) {
albumsCount[id] = count + 1
continue
}
val name = cursor.getString(bucketNameColumn)
val updatedAt = cursor.getLong(dateModified)
albums.add(PlatformAlbum(id, name, updatedAt, false, 0))
albumsCount[id] = 1
}
}
return albums.map { it.copy(assetCount = albumsCount[it.id]?.toLong() ?: 0) }
.sortedBy { it.id }
}
fun getAssetIdsForAlbum(albumId: String): List<String> {
val projection = arrayOf(MediaStore.MediaColumns._ID)
return getCursor(
MediaStore.VOLUME_EXTERNAL,
"$BUCKET_SELECTION AND $MEDIA_SELECTION",
arrayOf(albumId, *MEDIA_SELECTION_ARGS),
projection
)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
generateSequence {
if (cursor.moveToNext()) cursor.getLong(idColumn).toString() else null
}.toList()
} ?: emptyList()
}
fun getAssetsCountSince(albumId: String, timestamp: Long): Long =
getCursor(
MediaStore.VOLUME_EXTERNAL,
"$BUCKET_SELECTION AND ${MediaStore.Files.FileColumns.DATE_ADDED} > ? AND $MEDIA_SELECTION",
arrayOf(albumId, timestamp.toString(), *MEDIA_SELECTION_ARGS),
)?.use { cursor -> cursor.count.toLong() } ?: 0L
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> {
var selection = "$BUCKET_SELECTION AND $MEDIA_SELECTION"
val selectionArgs = mutableListOf(albumId, *MEDIA_SELECTION_ARGS)
if (updatedTimeCond != null) {
selection += " AND (${MediaStore.Files.FileColumns.DATE_MODIFIED} > ? OR ${MediaStore.Files.FileColumns.DATE_ADDED} > ?)"
selectionArgs.addAll(listOf(updatedTimeCond.toString(), updatedTimeCond.toString()))
}
return getAssets(getCursor(MediaStore.VOLUME_EXTERNAL, selection, selectionArgs.toTypedArray()))
.mapNotNull { result -> (result as? AssetResult.ValidAsset)?.asset }
.toList()
}
}

View File

@ -1,26 +1,27 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.7.2' apply false
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
id 'com.google.devtools.ksp' version '2.0.20-1.0.24' apply false
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.7.2' apply false
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' apply false
id 'com.google.devtools.ksp' version '2.0.20-1.0.24' apply false
}
include ":app"

File diff suppressed because one or more lines are too long

View File

@ -5,31 +5,26 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57
url: "https://pub.dev"
source: hosted
version: "76.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.3"
version: "80.0.0"
analyzer:
dependency: "direct main"
description:
name: analyzer
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
url: "https://pub.dev"
source: hosted
version: "6.11.0"
version: "7.3.0"
analyzer_plugin:
dependency: "direct main"
description:
name: analyzer_plugin
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4
url: "https://pub.dev"
source: hosted
version: "0.11.3"
version: "0.13.0"
args:
dependency: transitive
description:
@ -106,34 +101,42 @@ packages:
dependency: transitive
description:
name: custom_lint
sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545"
sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1"
url: "https://pub.dev"
source: hosted
version: "0.6.10"
version: "0.7.5"
custom_lint_builder:
dependency: "direct main"
description:
name: custom_lint_builder
sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78"
sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228"
url: "https://pub.dev"
source: hosted
version: "0.6.10"
version: "0.7.5"
custom_lint_core:
dependency: transitive
description:
name: custom_lint_core
sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6"
sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
url: "https://pub.dev"
source: hosted
version: "0.6.10"
version: "0.7.5"
custom_lint_visitor:
dependency: transitive
description:
name: custom_lint_visitor
sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8"
url: "https://pub.dev"
source: hosted
version: "1.0.0+7.3.0"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
url: "https://pub.dev"
source: hosted
version: "2.3.8"
version: "3.1.0"
file:
dependency: transitive
description:
@ -154,10 +157,10 @@ packages:
dependency: transitive
description:
name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
url: "https://pub.dev"
source: hosted
version: "2.4.4"
version: "3.0.0"
glob:
dependency: "direct main"
description:
@ -198,14 +201,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
macros:
dependency: transitive
description:
name: macros
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
url: "https://pub.dev"
source: hosted
version: "0.1.3-main.0"
matcher:
dependency: transitive
description:
@ -367,4 +362,4 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.6.0 <4.0.0"
dart: ">=3.7.0 <4.0.0"

View File

@ -5,9 +5,9 @@ environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
analyzer: ^6.0.0
analyzer_plugin: ^0.11.3
custom_lint_builder: ^0.6.4
analyzer: ^7.0.0
analyzer_plugin: ^0.13.0
custom_lint_builder: ^0.7.5
glob: ^2.1.2
dev_dependencies:

View File

@ -89,6 +89,16 @@
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Sync;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@ -175,6 +185,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
65DD438629917FAD0047FFA8 /* BackgroundSync */,
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
@ -224,6 +235,9 @@
dependencies = (
FAC6F8992D287C890078CB2F /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */;
@ -270,7 +284,6 @@
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
@ -278,6 +291,7 @@
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
preferredProjectObjectVersion = 77;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";

View File

@ -22,6 +22,9 @@ import UIKit
BackgroundServicePlugin.registerBackgroundProcessing()
BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
NativeSyncApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: NativeSyncApiImpl())
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {

View File

@ -0,0 +1,446 @@
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
#if os(iOS)
import Flutter
#elseif os(macOS)
import FlutterMacOS
#else
#error("Unsupported platform.")
#endif
/// Error class for passing custom error details to Dart side.
final class PigeonError: Error {
let code: String
let message: String?
let details: Sendable?
init(code: String, message: String?, details: Sendable?) {
self.code = code
self.message = message
self.details = details
}
var localizedDescription: String {
return
"PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(details ?? "<nil>")"
}
}
private func wrapResult(_ result: Any?) -> [Any?] {
return [result]
}
private func wrapError(_ error: Any) -> [Any?] {
if let pigeonError = error as? PigeonError {
return [
pigeonError.code,
pigeonError.message,
pigeonError.details,
]
}
if let flutterError = error as? FlutterError {
return [
flutterError.code,
flutterError.message,
flutterError.details,
]
}
return [
"\(error)",
"\(type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
private func isNullish(_ value: Any?) -> Bool {
return value is NSNull || value == nil
}
private func nilOrValue<T>(_ value: Any?) -> T? {
if value is NSNull { return nil }
return value as! T?
}
func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool {
let cleanLhs = nilOrValue(lhs) as Any?
let cleanRhs = nilOrValue(rhs) as Any?
switch (cleanLhs, cleanRhs) {
case (nil, nil):
return true
case (nil, _), (_, nil):
return false
case is (Void, Void):
return true
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
return cleanLhsHashable == cleanRhsHashable
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
for (index, element) in cleanLhsArray.enumerated() {
if !deepEqualsMessages(element, cleanRhsArray[index]) {
return false
}
}
return true
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
for (key, cleanLhsValue) in cleanLhsDictionary {
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
if !deepEqualsMessages(cleanLhsValue, cleanRhsDictionary[key]!) {
return false
}
}
return true
default:
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
return false
}
}
func deepHashMessages(value: Any?, hasher: inout Hasher) {
if let valueList = value as? [AnyHashable] {
for item in valueList { deepHashMessages(value: item, hasher: &hasher) }
return
}
if let valueDict = value as? [AnyHashable: AnyHashable] {
for key in valueDict.keys {
hasher.combine(key)
deepHashMessages(value: valueDict[key]!, hasher: &hasher)
}
return
}
if let hashableValue = value as? AnyHashable {
hasher.combine(hashableValue.hashValue)
}
return hasher.combine(String(describing: value))
}
/// Generated class from Pigeon that represents data sent in messages.
struct PlatformAsset: Hashable {
var id: String
var name: String
var type: Int64
var createdAt: Int64? = nil
var updatedAt: Int64? = nil
var durationInSeconds: Int64
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAsset? {
let id = pigeonVar_list[0] as! String
let name = pigeonVar_list[1] as! String
let type = pigeonVar_list[2] as! Int64
let createdAt: Int64? = nilOrValue(pigeonVar_list[3])
let updatedAt: Int64? = nilOrValue(pigeonVar_list[4])
let durationInSeconds = pigeonVar_list[5] as! Int64
return PlatformAsset(
id: id,
name: name,
type: type,
createdAt: createdAt,
updatedAt: updatedAt,
durationInSeconds: durationInSeconds
)
}
func toList() -> [Any?] {
return [
id,
name,
type,
createdAt,
updatedAt,
durationInSeconds,
]
}
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
func hash(into hasher: inout Hasher) {
deepHashMessages(value: toList(), hasher: &hasher)
}
}
/// Generated class from Pigeon that represents data sent in messages.
struct PlatformAlbum: Hashable {
var id: String
var name: String
var updatedAt: Int64? = nil
var isCloud: Bool
var assetCount: Int64
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAlbum? {
let id = pigeonVar_list[0] as! String
let name = pigeonVar_list[1] as! String
let updatedAt: Int64? = nilOrValue(pigeonVar_list[2])
let isCloud = pigeonVar_list[3] as! Bool
let assetCount = pigeonVar_list[4] as! Int64
return PlatformAlbum(
id: id,
name: name,
updatedAt: updatedAt,
isCloud: isCloud,
assetCount: assetCount
)
}
func toList() -> [Any?] {
return [
id,
name,
updatedAt,
isCloud,
assetCount,
]
}
static func == (lhs: PlatformAlbum, rhs: PlatformAlbum) -> Bool {
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
func hash(into hasher: inout Hasher) {
deepHashMessages(value: toList(), hasher: &hasher)
}
}
/// Generated class from Pigeon that represents data sent in messages.
struct SyncDelta: Hashable {
var hasChanges: Bool
var updates: [PlatformAsset]
var deletes: [String]
var assetAlbums: [String: [String]]
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> SyncDelta? {
let hasChanges = pigeonVar_list[0] as! Bool
let updates = pigeonVar_list[1] as! [PlatformAsset]
let deletes = pigeonVar_list[2] as! [String]
let assetAlbums = pigeonVar_list[3] as! [String: [String]]
return SyncDelta(
hasChanges: hasChanges,
updates: updates,
deletes: deletes,
assetAlbums: assetAlbums
)
}
func toList() -> [Any?] {
return [
hasChanges,
updates,
deletes,
assetAlbums,
]
}
static func == (lhs: SyncDelta, rhs: SyncDelta) -> Bool {
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
func hash(into hasher: inout Hasher) {
deepHashMessages(value: toList(), hasher: &hasher)
}
}
private class MessagesPigeonCodecReader: FlutterStandardReader {
override func readValue(ofType type: UInt8) -> Any? {
switch type {
case 129:
return PlatformAsset.fromList(self.readValue() as! [Any?])
case 130:
return PlatformAlbum.fromList(self.readValue() as! [Any?])
case 131:
return SyncDelta.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
}
}
}
private class MessagesPigeonCodecWriter: FlutterStandardWriter {
override func writeValue(_ value: Any) {
if let value = value as? PlatformAsset {
super.writeByte(129)
super.writeValue(value.toList())
} else if let value = value as? PlatformAlbum {
super.writeByte(130)
super.writeValue(value.toList())
} else if let value = value as? SyncDelta {
super.writeByte(131)
super.writeValue(value.toList())
} else {
super.writeValue(value)
}
}
}
private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter {
override func reader(with data: Data) -> FlutterStandardReader {
return MessagesPigeonCodecReader(data: data)
}
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
return MessagesPigeonCodecWriter(data: data)
}
}
class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter())
}
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol NativeSyncApi {
func shouldFullSync() throws -> Bool
func getMediaChanges() throws -> SyncDelta
func checkpointSync() throws
func clearSyncCheckpoint() throws
func getAssetIdsForAlbum(albumId: String) throws -> [String]
func getAlbums() throws -> [PlatformAlbum]
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset]
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
class NativeSyncApiSetup {
static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared }
/// Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`.
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") {
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
#if os(iOS)
let taskQueue = binaryMessenger.makeBackgroundTaskQueue?()
#else
let taskQueue: FlutterTaskQueue? = nil
#endif
let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
shouldFullSyncChannel.setMessageHandler { _, reply in
do {
let result = try api.shouldFullSync()
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
shouldFullSyncChannel.setMessageHandler(nil)
}
let getMediaChangesChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getMediaChangesChannel.setMessageHandler { _, reply in
do {
let result = try api.getMediaChanges()
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getMediaChangesChannel.setMessageHandler(nil)
}
let checkpointSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
checkpointSyncChannel.setMessageHandler { _, reply in
do {
try api.checkpointSync()
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
}
}
} else {
checkpointSyncChannel.setMessageHandler(nil)
}
let clearSyncCheckpointChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
clearSyncCheckpointChannel.setMessageHandler { _, reply in
do {
try api.clearSyncCheckpoint()
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
}
}
} else {
clearSyncCheckpointChannel.setMessageHandler(nil)
}
let getAssetIdsForAlbumChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getAssetIdsForAlbumChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let albumIdArg = args[0] as! String
do {
let result = try api.getAssetIdsForAlbum(albumId: albumIdArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getAssetIdsForAlbumChannel.setMessageHandler(nil)
}
let getAlbumsChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getAlbumsChannel.setMessageHandler { _, reply in
do {
let result = try api.getAlbums()
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getAlbumsChannel.setMessageHandler(nil)
}
let getAssetsCountSinceChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getAssetsCountSinceChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let albumIdArg = args[0] as! String
let timestampArg = args[1] as! Int64
do {
let result = try api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getAssetsCountSinceChannel.setMessageHandler(nil)
}
let getAssetsForAlbumChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getAssetsForAlbumChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let albumIdArg = args[0] as! String
let updatedTimeCondArg: Int64? = nilOrValue(args[1])
do {
let result = try api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getAssetsForAlbumChannel.setMessageHandler(nil)
}
}
}

View File

@ -0,0 +1,246 @@
import Photos
struct AssetWrapper: Hashable, Equatable {
let asset: PlatformAsset
init(with asset: PlatformAsset) {
self.asset = asset
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.asset.id)
}
static func == (lhs: AssetWrapper, rhs: AssetWrapper) -> Bool {
return lhs.asset.id == rhs.asset.id
}
}
extension PHAsset {
func toPlatformAsset() -> PlatformAsset {
return PlatformAsset(
id: localIdentifier,
name: title(),
type: Int64(mediaType.rawValue),
createdAt: creationDate.map { Int64($0.timeIntervalSince1970) },
updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) },
durationInSeconds: Int64(duration)
)
}
}
class NativeSyncApiImpl: NativeSyncApi {
private let defaults: UserDefaults
private let changeTokenKey = "immich:changeToken"
private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
init(with defaults: UserDefaults = .standard) {
self.defaults = defaults
}
@available(iOS 16, *)
private func getChangeToken() -> PHPersistentChangeToken? {
guard let data = defaults.data(forKey: changeTokenKey) else {
return nil
}
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: PHPersistentChangeToken.self, from: data)
}
@available(iOS 16, *)
private func saveChangeToken(token: PHPersistentChangeToken) -> Void {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else {
return
}
defaults.set(data, forKey: changeTokenKey)
}
func clearSyncCheckpoint() -> Void {
defaults.removeObject(forKey: changeTokenKey)
}
func checkpointSync() {
guard #available(iOS 16, *) else {
return
}
saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
}
func shouldFullSync() -> Bool {
guard #available(iOS 16, *),
PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized,
let storedToken = getChangeToken() else {
// When we do not have access to photo library, older iOS version or No token available, fallback to full sync
return true
}
guard let _ = try? PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) else {
// Cannot fetch persistent changes
return true
}
return false
}
func getAlbums() throws -> [PlatformAlbum] {
var albums: [PlatformAlbum] = []
albumTypes.forEach { type in
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
collections.enumerateObjects { (album, _, _) in
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)]
let assets = PHAsset.fetchAssets(in: album, options: options)
let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream
var domainAlbum = PlatformAlbum(
id: album.localIdentifier,
name: album.localizedTitle!,
updatedAt: nil,
isCloud: isCloud,
assetCount: Int64(assets.count)
)
if let firstAsset = assets.firstObject {
domainAlbum.updatedAt = firstAsset.modificationDate.map { Int64($0.timeIntervalSince1970) }
}
albums.append(domainAlbum)
}
}
return albums.sorted { $0.id < $1.id }
}
func getMediaChanges() throws -> SyncDelta {
guard #available(iOS 16, *) else {
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
}
guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
throw PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil)
}
guard let storedToken = getChangeToken() else {
// No token exists, definitely need a full sync
print("MediaManager::getMediaChanges: No token found")
throw PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil)
}
let currentToken = PHPhotoLibrary.shared().currentChangeToken
if storedToken == currentToken {
return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:])
}
do {
let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
var updatedAssets: Set<AssetWrapper> = []
var deletedAssets: Set<String> = []
for change in changes {
guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue }
let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
deletedAssets.formUnion(details.deletedLocalIdentifiers)
if (updated.isEmpty) { continue }
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: nil)
for i in 0..<result.count {
let asset = result.object(at: i)
// Asset wrapper only uses the id for comparison. Multiple change can contain the same asset, skip duplicate changes
let predicate = PlatformAsset(
id: asset.localIdentifier,
name: "",
type: 0,
createdAt: nil,
updatedAt: nil,
durationInSeconds: 0
)
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
continue
}
let domainAsset = AssetWrapper(with: asset.toPlatformAsset())
updatedAssets.insert(domainAsset)
}
}
let updates = Array(updatedAssets.map { $0.asset })
return SyncDelta(hasChanges: true, updates: updates, deletes: Array(deletedAssets), assetAlbums: buildAssetAlbumsMap(assets: updates))
}
}
private func buildAssetAlbumsMap(assets: Array<PlatformAsset>) -> [String: [String]] {
guard !assets.isEmpty else {
return [:]
}
var albumAssets: [String: [String]] = [:]
for type in albumTypes {
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
collections.enumerateObjects { (album, _, _) in
let options = PHFetchOptions()
options.predicate = NSPredicate(format: "localIdentifier IN %@", assets.map(\.id))
let result = PHAsset.fetchAssets(in: album, options: options)
result.enumerateObjects { (asset, _, _) in
albumAssets[asset.localIdentifier, default: []].append(album.localIdentifier)
}
}
}
return albumAssets
}
func getAssetIdsForAlbum(albumId: String) throws -> [String] {
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
guard let album = collections.firstObject else {
return []
}
var ids: [String] = []
let assets = PHAsset.fetchAssets(in: album, options: nil)
assets.enumerateObjects { (asset, _, _) in
ids.append(asset.localIdentifier)
}
return ids
}
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 {
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
guard let album = collections.firstObject else {
return 0
}
let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp))
let options = PHFetchOptions()
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
let assets = PHAsset.fetchAssets(in: album, options: options)
return Int64(assets.count)
}
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] {
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
guard let album = collections.firstObject else {
return []
}
let options = PHFetchOptions()
if(updatedTimeCond != nil) {
let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!))
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
}
let result = PHAsset.fetchAssets(in: album, options: options)
if(result.count == 0) {
return []
}
var assets: [PlatformAsset] = []
result.enumerateObjects { (asset, _, _) in
assets.append(asset.toPlatformAsset())
}
return assets
}
}

View File

@ -7,6 +7,7 @@ const int kLogTruncateLimit = 250;
// Sync
const int kSyncEventBatchSize = 5000;
const int kFetchLocalAssetsBatchSize = 40000;
// Hash batch limits
const int kBatchHashFileLimit = 128;

View File

@ -5,6 +5,7 @@ const Map<String, Locale> locales = {
'English (en)': Locale('en'),
// Additional locales
'Arabic (ar)': Locale('ar'),
'Bulgarian (bg)': Locale('bg'),
'Catalan (ca)': Locale('ca'),
'Chinese Simplified (zh_CN)':
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'SIMPLIFIED'),
@ -31,6 +32,7 @@ const Map<String, Locale> locales = {
'Mongolian (mn)': Locale('mn'),
'Norwegian Bokmål (nb_NO)': Locale('nb', 'NO'),
'Polish (pl)': Locale('pl'),
'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'),
'Portuguese (pt)': Locale('pt'),
'Romanian (ro)': Locale('ro'),
'Russian (ru)': Locale('ru'),
@ -42,6 +44,8 @@ const Map<String, Locale> locales = {
'Slovenian (sl)': Locale('sl'),
'Spanish (es)': Locale('es'),
'Swedish (sv)': Locale('sv'),
'Tamil (ta)': Locale('ta'),
'Telugu (te)': Locale('te'),
'Thai (th)': Locale('th'),
'Turkish (tr)': Locale('tr'),
'Ukrainian (uk)': Locale('uk'),

View File

@ -0,0 +1,35 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
abstract interface class ILocalAlbumRepository implements IDatabaseRepository {
Future<List<LocalAlbum>> getAll({SortLocalAlbumsBy? sortBy});
Future<List<LocalAsset>> getAssetsForAlbum(String albumId);
Future<List<String>> getAssetIdsForAlbum(String albumId);
Future<void> upsert(
LocalAlbum album, {
Iterable<LocalAsset> toUpsert = const [],
Iterable<String> toDelete = const [],
});
Future<void> update(LocalAlbum album);
Future<void> updateAll(Iterable<LocalAlbum> albums);
Future<void> delete(String albumId);
Future<void> processDelta({
required List<LocalAsset> updates,
required List<String> deletes,
required Map<String, List<String>> assetAlbums,
});
Future<void> syncAlbumDeletes(
String albumId,
Iterable<String> assetIdsToKeep,
);
}
enum SortLocalAlbumsBy { id }

View File

@ -0,0 +1,61 @@
part of 'base_asset.model.dart';
enum AssetVisibility {
timeline,
hidden,
archive,
locked,
}
// Model for an asset stored in the server
class Asset extends BaseAsset {
final String id;
final String? localId;
final AssetVisibility visibility;
const Asset({
required this.id,
this.localId,
required super.name,
required super.checksum,
required super.type,
required super.createdAt,
required super.updatedAt,
super.width,
super.height,
super.durationInSeconds,
super.isFavorite = false,
this.visibility = AssetVisibility.timeline,
});
@override
String toString() {
return '''Asset {
id: $id,
name: $name,
type: $type,
createdAt: $createdAt,
updatedAt: $updatedAt,
width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"},
localId: ${localId ?? "<NA>"},
isFavorite: $isFavorite,
visibility: $visibility,
}''';
}
@override
bool operator ==(Object other) {
if (other is! Asset) return false;
if (identical(this, other)) return true;
return super == other &&
id == other.id &&
localId == other.localId &&
visibility == other.visibility;
}
@override
int get hashCode =>
super.hashCode ^ id.hashCode ^ localId.hashCode ^ visibility.hashCode;
}

View File

@ -0,0 +1,76 @@
part 'asset.model.dart';
part 'local_asset.model.dart';
enum AssetType {
// do not change this order!
other,
image,
video,
audio,
}
sealed class BaseAsset {
final String name;
final String? checksum;
final AssetType type;
final DateTime createdAt;
final DateTime updatedAt;
final int? width;
final int? height;
final int? durationInSeconds;
final bool isFavorite;
const BaseAsset({
required this.name,
required this.checksum,
required this.type,
required this.createdAt,
required this.updatedAt,
this.width,
this.height,
this.durationInSeconds,
this.isFavorite = false,
});
@override
String toString() {
return '''BaseAsset {
name: $name,
type: $type,
createdAt: $createdAt,
updatedAt: $updatedAt,
width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"},
isFavorite: $isFavorite,
}''';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is BaseAsset) {
return name == other.name &&
type == other.type &&
createdAt == other.createdAt &&
updatedAt == other.updatedAt &&
width == other.width &&
height == other.height &&
durationInSeconds == other.durationInSeconds &&
isFavorite == other.isFavorite;
}
return false;
}
@override
int get hashCode {
return name.hashCode ^
type.hashCode ^
createdAt.hashCode ^
updatedAt.hashCode ^
width.hashCode ^
height.hashCode ^
durationInSeconds.hashCode ^
isFavorite.hashCode;
}
}

View File

@ -0,0 +1,74 @@
part of 'base_asset.model.dart';
class LocalAsset extends BaseAsset {
final String id;
final String? remoteId;
const LocalAsset({
required this.id,
this.remoteId,
required super.name,
super.checksum,
required super.type,
required super.createdAt,
required super.updatedAt,
super.width,
super.height,
super.durationInSeconds,
super.isFavorite = false,
});
@override
String toString() {
return '''LocalAsset {
id: $id,
name: $name,
type: $type,
createdAt: $createdAt,
updatedAt: $updatedAt,
width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"},
remoteId: ${remoteId ?? "<NA>"}
isFavorite: $isFavorite,
}''';
}
@override
bool operator ==(Object other) {
if (other is! LocalAsset) return false;
if (identical(this, other)) return true;
return super == other && id == other.id && remoteId == other.remoteId;
}
@override
int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode;
LocalAsset copyWith({
String? id,
String? remoteId,
String? name,
String? checksum,
AssetType? type,
DateTime? createdAt,
DateTime? updatedAt,
int? width,
int? height,
int? durationInSeconds,
bool? isFavorite,
}) {
return LocalAsset(
id: id ?? this.id,
remoteId: remoteId ?? this.remoteId,
name: name ?? this.name,
checksum: checksum ?? this.checksum,
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
width: width ?? this.width,
height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
isFavorite: isFavorite ?? this.isFavorite,
);
}
}

View File

@ -0,0 +1,70 @@
enum BackupSelection {
none,
selected,
excluded,
}
class LocalAlbum {
final String id;
final String name;
final DateTime updatedAt;
final int assetCount;
final BackupSelection backupSelection;
const LocalAlbum({
required this.id,
required this.name,
required this.updatedAt,
this.assetCount = 0,
this.backupSelection = BackupSelection.none,
});
LocalAlbum copyWith({
String? id,
String? name,
DateTime? updatedAt,
int? assetCount,
BackupSelection? backupSelection,
}) {
return LocalAlbum(
id: id ?? this.id,
name: name ?? this.name,
updatedAt: updatedAt ?? this.updatedAt,
assetCount: assetCount ?? this.assetCount,
backupSelection: backupSelection ?? this.backupSelection,
);
}
@override
bool operator ==(Object other) {
if (other is! LocalAlbum) return false;
if (identical(this, other)) return true;
return other.id == id &&
other.name == name &&
other.updatedAt == updatedAt &&
other.assetCount == assetCount &&
other.backupSelection == backupSelection;
}
@override
int get hashCode {
return id.hashCode ^
name.hashCode ^
updatedAt.hashCode ^
assetCount.hashCode ^
backupSelection.hashCode;
}
@override
String toString() {
return '''LocalAlbum: {
id: $id,
name: $name,
updatedAt: $updatedAt,
assetCount: $assetCount,
backupSelection: $backupSelection,
}''';
}
}

View File

@ -0,0 +1,379 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:logging/logging.dart';
import 'package:platform/platform.dart';
class LocalSyncService {
final ILocalAlbumRepository _localAlbumRepository;
final NativeSyncApi _nativeSyncApi;
final Platform _platform;
final StoreService _storeService;
final Logger _log = Logger("DeviceSyncService");
LocalSyncService({
required ILocalAlbumRepository localAlbumRepository,
required NativeSyncApi nativeSyncApi,
required StoreService storeService,
Platform? platform,
}) : _localAlbumRepository = localAlbumRepository,
_nativeSyncApi = nativeSyncApi,
_storeService = storeService,
_platform = platform ?? const LocalPlatform();
bool get _ignoreIcloudAssets =>
_storeService.get(StoreKey.ignoreIcloudAssets, false) == true;
Future<void> sync({bool full = false}) async {
final Stopwatch stopwatch = Stopwatch()..start();
try {
if (full || await _nativeSyncApi.shouldFullSync()) {
_log.fine("Full sync request from ${full ? "user" : "native"}");
DLog.log("Full sync request from ${full ? "user" : "native"}");
return await fullSync();
}
final delta = await _nativeSyncApi.getMediaChanges();
if (!delta.hasChanges) {
_log.fine("No media changes detected. Skipping sync");
DLog.log("No media changes detected. Skipping sync");
return;
}
DLog.log("Delta updated: ${delta.updates.length}");
DLog.log("Delta deleted: ${delta.deletes.length}");
final deviceAlbums = await _nativeSyncApi.getAlbums();
await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums());
await _localAlbumRepository.processDelta(
updates: delta.updates.toLocalAssets(),
deletes: delta.deletes,
assetAlbums: delta.assetAlbums,
);
final dbAlbums = await _localAlbumRepository.getAll();
// On Android, we need to sync all albums since it is not possible to
// detect album deletions from the native side
if (_platform.isAndroid) {
for (final album in dbAlbums) {
final deviceIds = await _nativeSyncApi.getAssetIdsForAlbum(album.id);
await _localAlbumRepository.syncAlbumDeletes(album.id, deviceIds);
}
}
if (_platform.isIOS) {
// On iOS, we need to full sync albums that are marked as cloud as the delta sync
// does not include changes for cloud albums. If ignoreIcloudAssets is enabled,
// remove the albums from the local database from the previous sync
final cloudAlbums =
deviceAlbums.where((a) => a.isCloud).toLocalAlbums();
for (final album in cloudAlbums) {
final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id);
if (dbAlbum == null) {
_log.warning(
"Cloud album ${album.name} not found in local database. Skipping sync.",
);
continue;
}
if (_ignoreIcloudAssets) {
await removeAlbum(dbAlbum);
} else {
await updateAlbum(dbAlbum, album);
}
}
}
await _nativeSyncApi.checkpointSync();
} catch (e, s) {
_log.severe("Error performing device sync", e, s);
} finally {
stopwatch.stop();
_log.info("Device sync took - ${stopwatch.elapsedMilliseconds}ms");
DLog.log("Device sync took - ${stopwatch.elapsedMilliseconds}ms");
}
}
Future<void> fullSync() async {
try {
final Stopwatch stopwatch = Stopwatch()..start();
List<PlatformAlbum> deviceAlbums =
List.of(await _nativeSyncApi.getAlbums());
if (_platform.isIOS && _ignoreIcloudAssets) {
deviceAlbums.removeWhere((album) => album.isCloud);
}
final dbAlbums =
await _localAlbumRepository.getAll(sortBy: SortLocalAlbumsBy.id);
await diffSortedLists(
dbAlbums,
deviceAlbums.toLocalAlbums(),
compare: (a, b) => a.id.compareTo(b.id),
both: updateAlbum,
onlyFirst: removeAlbum,
onlySecond: addAlbum,
);
await _nativeSyncApi.checkpointSync();
stopwatch.stop();
_log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms");
DLog.log("Full device sync took - ${stopwatch.elapsedMilliseconds}ms");
} catch (e, s) {
_log.severe("Error performing full device sync", e, s);
}
}
Future<void> addAlbum(LocalAlbum album) async {
try {
_log.fine("Adding device album ${album.name}");
final assets = album.assetCount > 0
? await _nativeSyncApi.getAssetsForAlbum(album.id)
: <PlatformAsset>[];
await _localAlbumRepository.upsert(
album,
toUpsert: assets.toLocalAssets(),
);
_log.fine("Successfully added device album ${album.name}");
} catch (e, s) {
_log.warning("Error while adding device album", e, s);
}
}
Future<void> removeAlbum(LocalAlbum a) async {
_log.fine("Removing device album ${a.name}");
try {
// Asset deletion is handled in the repository
await _localAlbumRepository.delete(a.id);
} catch (e, s) {
_log.warning("Error while removing device album", e, s);
}
}
// The deviceAlbum is ignored since we are going to refresh it anyways
FutureOr<bool> updateAlbum(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async {
try {
_log.fine("Syncing device album ${dbAlbum.name}");
if (_albumsEqual(deviceAlbum, dbAlbum)) {
_log.fine(
"Device album ${dbAlbum.name} has not changed. Skipping sync.",
);
return false;
}
_log.fine("Device album ${dbAlbum.name} has changed. Syncing...");
// Faster path - only new assets added
if (await checkAddition(dbAlbum, deviceAlbum)) {
_log.fine("Fast synced device album ${dbAlbum.name}");
DLog.log("Fast synced device album ${dbAlbum.name}");
return true;
}
// Slower path - full sync
return await fullDiff(dbAlbum, deviceAlbum);
} catch (e, s) {
_log.warning("Error while diff device album", e, s);
}
return true;
}
@visibleForTesting
// The [deviceAlbum] is expected to be refreshed before calling this method
// with modified time and asset count
Future<bool> checkAddition(
LocalAlbum dbAlbum,
LocalAlbum deviceAlbum,
) async {
try {
_log.fine("Fast syncing device album ${dbAlbum.name}");
// Assets has been modified
if (deviceAlbum.assetCount <= dbAlbum.assetCount) {
_log.fine("Local album has modifications. Proceeding to full sync");
return false;
}
final updatedTime =
(dbAlbum.updatedAt.millisecondsSinceEpoch ~/ 1000) + 1;
final newAssetsCount =
await _nativeSyncApi.getAssetsCountSince(deviceAlbum.id, updatedTime);
// Early return if no new assets were found
if (newAssetsCount == 0) {
_log.fine(
"No new assets found despite album having changes. Proceeding to full sync for ${dbAlbum.name}",
);
return false;
}
// Check whether there is only addition or if there has been deletions
if (deviceAlbum.assetCount != dbAlbum.assetCount + newAssetsCount) {
_log.fine("Local album has modifications. Proceeding to full sync");
return false;
}
final newAssets = await _nativeSyncApi.getAssetsForAlbum(
deviceAlbum.id,
updatedTimeCond: updatedTime,
);
await _localAlbumRepository.upsert(
deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
toUpsert: newAssets.toLocalAssets(),
);
return true;
} catch (e, s) {
_log.warning("Error on fast syncing local album: ${dbAlbum.name}", e, s);
}
return false;
}
@visibleForTesting
// The [deviceAlbum] is expected to be refreshed before calling this method
// with modified time and asset count
Future<bool> fullDiff(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async {
try {
final assetsInDevice = deviceAlbum.assetCount > 0
? await _nativeSyncApi
.getAssetsForAlbum(deviceAlbum.id)
.then((a) => a.toLocalAssets())
: <LocalAsset>[];
final assetsInDb = dbAlbum.assetCount > 0
? await _localAlbumRepository.getAssetsForAlbum(dbAlbum.id)
: <LocalAsset>[];
if (deviceAlbum.assetCount == 0) {
_log.fine(
"Device album ${deviceAlbum.name} is empty. Removing assets from DB.",
);
await _localAlbumRepository.upsert(
deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
toDelete: assetsInDb.map((a) => a.id),
);
return true;
}
final updatedDeviceAlbum = deviceAlbum.copyWith(
backupSelection: dbAlbum.backupSelection,
);
if (dbAlbum.assetCount == 0) {
_log.fine(
"Device album ${deviceAlbum.name} is empty. Adding assets to DB.",
);
await _localAlbumRepository.upsert(
updatedDeviceAlbum,
toUpsert: assetsInDevice,
);
return true;
}
assert(assetsInDb.isSortedBy((a) => a.id));
assetsInDevice.sort((a, b) => a.id.compareTo(b.id));
final assetsToUpsert = <LocalAsset>[];
final assetsToDelete = <String>[];
diffSortedListsSync(
assetsInDb,
assetsInDevice,
compare: (a, b) => a.id.compareTo(b.id),
both: (dbAsset, deviceAsset) {
// Custom comparison to check if the asset has been modified without
// comparing the checksum
if (!_assetsEqual(dbAsset, deviceAsset)) {
assetsToUpsert.add(deviceAsset);
return true;
}
return false;
},
onlyFirst: (dbAsset) => assetsToDelete.add(dbAsset.id),
onlySecond: (deviceAsset) => assetsToUpsert.add(deviceAsset),
);
_log.fine(
"Syncing ${deviceAlbum.name}. ${assetsToUpsert.length} assets to add/update and ${assetsToDelete.length} assets to delete",
);
if (assetsToUpsert.isEmpty && assetsToDelete.isEmpty) {
_log.fine(
"No asset changes detected in album ${deviceAlbum.name}. Updating metadata.",
);
_localAlbumRepository.upsert(updatedDeviceAlbum);
return true;
}
await _localAlbumRepository.upsert(
updatedDeviceAlbum,
toUpsert: assetsToUpsert,
toDelete: assetsToDelete,
);
return true;
} catch (e, s) {
_log.warning("Error on full syncing local album: ${dbAlbum.name}", e, s);
}
return true;
}
bool _assetsEqual(LocalAsset a, LocalAsset b) {
return a.updatedAt.isAtSameMomentAs(b.updatedAt) &&
a.createdAt.isAtSameMomentAs(b.createdAt) &&
a.width == b.width &&
a.height == b.height &&
a.durationInSeconds == b.durationInSeconds;
}
bool _albumsEqual(LocalAlbum a, LocalAlbum b) {
return a.name == b.name &&
a.assetCount == b.assetCount &&
a.updatedAt.isAtSameMomentAs(b.updatedAt);
}
}
extension on Iterable<PlatformAlbum> {
List<LocalAlbum> toLocalAlbums() {
return map(
(e) => LocalAlbum(
id: e.id,
name: e.name,
updatedAt: e.updatedAt == null
? DateTime.now()
: DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000),
assetCount: e.assetCount,
),
).toList();
}
}
extension on Iterable<PlatformAsset> {
List<LocalAsset> toLocalAssets() {
return map(
(e) => LocalAsset(
id: e.id,
name: e.name,
type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other,
createdAt: e.createdAt == null
? DateTime.now()
: DateTime.fromMillisecondsSinceEpoch(e.createdAt! * 1000),
updatedAt: e.updatedAt == null
? DateTime.now()
: DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000),
durationInSeconds: e.durationInSeconds,
),
).toList();
}
}

View File

@ -63,7 +63,6 @@ class SyncStreamService {
Iterable<dynamic> data,
) async {
_logger.fine("Processing sync data for $type of length ${data.length}");
// ignore: prefer-switch-expression
switch (type) {
case SyncEntityType.userV1:
return _syncStreamRepository.updateUsersV1(data.cast());

View File

@ -1,13 +1,12 @@
// ignore_for_file: avoid-passing-async-when-sync-expected
import 'dart:async';
import 'package:immich_mobile/providers/infrastructure/sync_stream.provider.dart';
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
import 'package:immich_mobile/utils/isolate.dart';
import 'package:worker_manager/worker_manager.dart';
class BackgroundSyncManager {
Cancelable<void>? _syncTask;
Cancelable<void>? _deviceAlbumSyncTask;
BackgroundSyncManager();
@ -23,7 +22,30 @@ class BackgroundSyncManager {
return Future.wait(futures);
}
Future<void> sync() {
// No need to cancel the task, as it can also be run when the user logs out
Future<void> syncLocal({bool full = false}) {
if (_deviceAlbumSyncTask != null) {
return _deviceAlbumSyncTask!.future;
}
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
// captured by the closure passed to [runInIsolateGentle].
_deviceAlbumSyncTask = full
? runInIsolateGentle(
computation: (ref) =>
ref.read(localSyncServiceProvider).sync(full: true),
)
: runInIsolateGentle(
computation: (ref) =>
ref.read(localSyncServiceProvider).sync(full: false),
);
return _deviceAlbumSyncTask!.whenComplete(() {
_deviceAlbumSyncTask = null;
});
}
Future<void> syncRemote() {
if (_syncTask != null) {
return _syncTask!.future;
}
@ -31,9 +53,8 @@ class BackgroundSyncManager {
_syncTask = runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
);
_syncTask!.whenComplete(() {
return _syncTask!.whenComplete(() {
_syncTask = null;
});
return _syncTask!.future;
}
}

View File

@ -1,7 +1,3 @@
import 'dart:typed_data';
import 'package:uuid/parsing.dart';
extension StringExtension on String {
String capitalize() {
return split(" ")
@ -33,8 +29,3 @@ extension DurationExtension on String {
return int.parse(this);
}
}
extension UUIDExtension on String {
Uint8List toUuidByte({bool shouldValidate = false}) =>
UuidParsing.parseAsByteList(this, validate: shouldValidate);
}

View File

@ -1,4 +1,7 @@
import 'package:drift/drift.dart' hide Query;
import 'package:immich_mobile/domain/models/exif.model.dart' as domain;
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
import 'package:isar/isar.dart';
@ -90,3 +93,53 @@ class ExifInfo {
exposureSeconds: exposureSeconds,
);
}
class RemoteExifEntity extends Table with DriftDefaultsMixin {
const RemoteExifEntity();
TextColumn get assetId =>
text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get city => text().nullable()();
TextColumn get state => text().nullable()();
TextColumn get country => text().nullable()();
DateTimeColumn get dateTimeOriginal => dateTime().nullable()();
TextColumn get description => text().nullable()();
IntColumn get height => integer().nullable()();
IntColumn get width => integer().nullable()();
TextColumn get exposureTime => text().nullable()();
IntColumn get fNumber => integer().nullable()();
IntColumn get fileSize => integer().nullable()();
IntColumn get focalLength => integer().nullable()();
IntColumn get latitude => integer().nullable()();
IntColumn get longitude => integer().nullable()();
IntColumn get iso => integer().nullable()();
TextColumn get make => text().nullable()();
TextColumn get model => text().nullable()();
TextColumn get orientation => text().nullable()();
TextColumn get timeZone => text().nullable()();
IntColumn get rating => integer().nullable()();
TextColumn get projectionType => text().nullable()();
@override
Set<Column> get primaryKey => {assetId};
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class LocalAlbumEntity extends Table with DriftDefaultsMixin {
const LocalAlbumEntity();
TextColumn get id => text()();
TextColumn get name => text()();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
IntColumn get backupSelection => intEnum<BackupSelection>()();
// Used for mark & sweep
BoolColumn get marker_ => boolean().nullable()();
@override
Set<Column> get primaryKey => {id};
}

View File

@ -0,0 +1,497 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
as i1;
import 'package:immich_mobile/domain/models/local_album.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'
as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
typedef $$LocalAlbumEntityTableCreateCompanionBuilder
= i1.LocalAlbumEntityCompanion Function({
required String id,
required String name,
i0.Value<DateTime> updatedAt,
required i2.BackupSelection backupSelection,
i0.Value<bool?> marker_,
});
typedef $$LocalAlbumEntityTableUpdateCompanionBuilder
= i1.LocalAlbumEntityCompanion Function({
i0.Value<String> id,
i0.Value<String> name,
i0.Value<DateTime> updatedAt,
i0.Value<i2.BackupSelection> backupSelection,
i0.Value<bool?> marker_,
});
class $$LocalAlbumEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable> {
$$LocalAlbumEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get name => $composableBuilder(
column: $table.name, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnWithTypeConverterFilters<i2.BackupSelection, i2.BackupSelection, int>
get backupSelection => $composableBuilder(
column: $table.backupSelection,
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i0.ColumnFilters<bool> get marker_ => $composableBuilder(
column: $table.marker_, builder: (column) => i0.ColumnFilters(column));
}
class $$LocalAlbumEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable> {
$$LocalAlbumEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get name => $composableBuilder(
column: $table.name, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get backupSelection => $composableBuilder(
column: $table.backupSelection,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<bool> get marker_ => $composableBuilder(
column: $table.marker_, builder: (column) => i0.ColumnOrderings(column));
}
class $$LocalAlbumEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable> {
$$LocalAlbumEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column);
i0.GeneratedColumn<DateTime> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.BackupSelection, int>
get backupSelection => $composableBuilder(
column: $table.backupSelection, builder: (column) => column);
i0.GeneratedColumn<bool> get marker_ =>
$composableBuilder(column: $table.marker_, builder: (column) => column);
}
class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager<
i0.GeneratedDatabase,
i1.$LocalAlbumEntityTable,
i1.LocalAlbumEntityData,
i1.$$LocalAlbumEntityTableFilterComposer,
i1.$$LocalAlbumEntityTableOrderingComposer,
i1.$$LocalAlbumEntityTableAnnotationComposer,
$$LocalAlbumEntityTableCreateCompanionBuilder,
$$LocalAlbumEntityTableUpdateCompanionBuilder,
(
i1.LocalAlbumEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable,
i1.LocalAlbumEntityData>
),
i1.LocalAlbumEntityData,
i0.PrefetchHooks Function()> {
$$LocalAlbumEntityTableTableManager(
i0.GeneratedDatabase db, i1.$LocalAlbumEntityTable table)
: super(i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$LocalAlbumEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () => i1
.$$LocalAlbumEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$LocalAlbumEntityTableAnnotationComposer(
$db: db, $table: table),
updateCompanionCallback: ({
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String> name = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<i2.BackupSelection> backupSelection =
const i0.Value.absent(),
i0.Value<bool?> marker_ = const i0.Value.absent(),
}) =>
i1.LocalAlbumEntityCompanion(
id: id,
name: name,
updatedAt: updatedAt,
backupSelection: backupSelection,
marker_: marker_,
),
createCompanionCallback: ({
required String id,
required String name,
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
required i2.BackupSelection backupSelection,
i0.Value<bool?> marker_ = const i0.Value.absent(),
}) =>
i1.LocalAlbumEntityCompanion.insert(
id: id,
name: name,
updatedAt: updatedAt,
backupSelection: backupSelection,
marker_: marker_,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $$LocalAlbumEntityTableProcessedTableManager = i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$LocalAlbumEntityTable,
i1.LocalAlbumEntityData,
i1.$$LocalAlbumEntityTableFilterComposer,
i1.$$LocalAlbumEntityTableOrderingComposer,
i1.$$LocalAlbumEntityTableAnnotationComposer,
$$LocalAlbumEntityTableCreateCompanionBuilder,
$$LocalAlbumEntityTableUpdateCompanionBuilder,
(
i1.LocalAlbumEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable,
i1.LocalAlbumEntityData>
),
i1.LocalAlbumEntityData,
i0.PrefetchHooks Function()>;
class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
with i0.TableInfo<$LocalAlbumEntityTable, i1.LocalAlbumEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$LocalAlbumEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _nameMeta =
const i0.VerificationMeta('name');
@override
late final i0.GeneratedColumn<String> name = i0.GeneratedColumn<String>(
'name', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _updatedAtMeta =
const i0.VerificationMeta('updatedAt');
@override
late final i0.GeneratedColumn<DateTime> updatedAt =
i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i4.currentDateAndTime);
@override
late final i0.GeneratedColumnWithTypeConverter<i2.BackupSelection, int>
backupSelection = i0.GeneratedColumn<int>(
'backup_selection', aliasedName, false,
type: i0.DriftSqlType.int, requiredDuringInsert: true)
.withConverter<i2.BackupSelection>(
i1.$LocalAlbumEntityTable.$converterbackupSelection);
static const i0.VerificationMeta _marker_Meta =
const i0.VerificationMeta('marker_');
@override
late final i0.GeneratedColumn<bool> marker_ = i0.GeneratedColumn<bool>(
'marker', aliasedName, true,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints:
i0.GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))'));
@override
List<i0.GeneratedColumn> get $columns =>
[id, name, updatedAt, backupSelection, marker_];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'local_album_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.LocalAlbumEntityData> instance,
{bool isInserting = false}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('name')) {
context.handle(
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
} else if (isInserting) {
context.missing(_nameMeta);
}
if (data.containsKey('updated_at')) {
context.handle(_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
}
if (data.containsKey('marker')) {
context.handle(_marker_Meta,
marker_.isAcceptableOrUnknown(data['marker']!, _marker_Meta));
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {id};
@override
i1.LocalAlbumEntityData map(Map<String, dynamic> data,
{String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.LocalAlbumEntityData(
id: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
name: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection
.fromSql(attachedDatabase.typeMapping.read(i0.DriftSqlType.int,
data['${effectivePrefix}backup_selection'])!),
marker_: attachedDatabase.typeMapping
.read(i0.DriftSqlType.bool, data['${effectivePrefix}marker']),
);
}
@override
$LocalAlbumEntityTable createAlias(String alias) {
return $LocalAlbumEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i2.BackupSelection, int, int>
$converterbackupSelection =
const i0.EnumIndexConverter<i2.BackupSelection>(
i2.BackupSelection.values);
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class LocalAlbumEntityData extends i0.DataClass
implements i0.Insertable<i1.LocalAlbumEntityData> {
final String id;
final String name;
final DateTime updatedAt;
final i2.BackupSelection backupSelection;
final bool? marker_;
const LocalAlbumEntityData(
{required this.id,
required this.name,
required this.updatedAt,
required this.backupSelection,
this.marker_});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<String>(id);
map['name'] = i0.Variable<String>(name);
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
{
map['backup_selection'] = i0.Variable<int>(i1
.$LocalAlbumEntityTable.$converterbackupSelection
.toSql(backupSelection));
}
if (!nullToAbsent || marker_ != null) {
map['marker'] = i0.Variable<bool>(marker_);
}
return map;
}
factory LocalAlbumEntityData.fromJson(Map<String, dynamic> json,
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return LocalAlbumEntityData(
id: serializer.fromJson<String>(json['id']),
name: serializer.fromJson<String>(json['name']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection
.fromJson(serializer.fromJson<int>(json['backupSelection'])),
marker_: serializer.fromJson<bool?>(json['marker_']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'name': serializer.toJson<String>(name),
'updatedAt': serializer.toJson<DateTime>(updatedAt),
'backupSelection': serializer.toJson<int>(i1
.$LocalAlbumEntityTable.$converterbackupSelection
.toJson(backupSelection)),
'marker_': serializer.toJson<bool?>(marker_),
};
}
i1.LocalAlbumEntityData copyWith(
{String? id,
String? name,
DateTime? updatedAt,
i2.BackupSelection? backupSelection,
i0.Value<bool?> marker_ = const i0.Value.absent()}) =>
i1.LocalAlbumEntityData(
id: id ?? this.id,
name: name ?? this.name,
updatedAt: updatedAt ?? this.updatedAt,
backupSelection: backupSelection ?? this.backupSelection,
marker_: marker_.present ? marker_.value : this.marker_,
);
LocalAlbumEntityData copyWithCompanion(i1.LocalAlbumEntityCompanion data) {
return LocalAlbumEntityData(
id: data.id.present ? data.id.value : this.id,
name: data.name.present ? data.name.value : this.name,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
backupSelection: data.backupSelection.present
? data.backupSelection.value
: this.backupSelection,
marker_: data.marker_.present ? data.marker_.value : this.marker_,
);
}
@override
String toString() {
return (StringBuffer('LocalAlbumEntityData(')
..write('id: $id, ')
..write('name: $name, ')
..write('updatedAt: $updatedAt, ')
..write('backupSelection: $backupSelection, ')
..write('marker_: $marker_')
..write(')'))
.toString();
}
@override
int get hashCode =>
Object.hash(id, name, updatedAt, backupSelection, marker_);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.LocalAlbumEntityData &&
other.id == this.id &&
other.name == this.name &&
other.updatedAt == this.updatedAt &&
other.backupSelection == this.backupSelection &&
other.marker_ == this.marker_);
}
class LocalAlbumEntityCompanion
extends i0.UpdateCompanion<i1.LocalAlbumEntityData> {
final i0.Value<String> id;
final i0.Value<String> name;
final i0.Value<DateTime> updatedAt;
final i0.Value<i2.BackupSelection> backupSelection;
final i0.Value<bool?> marker_;
const LocalAlbumEntityCompanion({
this.id = const i0.Value.absent(),
this.name = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.backupSelection = const i0.Value.absent(),
this.marker_ = const i0.Value.absent(),
});
LocalAlbumEntityCompanion.insert({
required String id,
required String name,
this.updatedAt = const i0.Value.absent(),
required i2.BackupSelection backupSelection,
this.marker_ = const i0.Value.absent(),
}) : id = i0.Value(id),
name = i0.Value(name),
backupSelection = i0.Value(backupSelection);
static i0.Insertable<i1.LocalAlbumEntityData> custom({
i0.Expression<String>? id,
i0.Expression<String>? name,
i0.Expression<DateTime>? updatedAt,
i0.Expression<int>? backupSelection,
i0.Expression<bool>? marker_,
}) {
return i0.RawValuesInsertable({
if (id != null) 'id': id,
if (name != null) 'name': name,
if (updatedAt != null) 'updated_at': updatedAt,
if (backupSelection != null) 'backup_selection': backupSelection,
if (marker_ != null) 'marker': marker_,
});
}
i1.LocalAlbumEntityCompanion copyWith(
{i0.Value<String>? id,
i0.Value<String>? name,
i0.Value<DateTime>? updatedAt,
i0.Value<i2.BackupSelection>? backupSelection,
i0.Value<bool?>? marker_}) {
return i1.LocalAlbumEntityCompanion(
id: id ?? this.id,
name: name ?? this.name,
updatedAt: updatedAt ?? this.updatedAt,
backupSelection: backupSelection ?? this.backupSelection,
marker_: marker_ ?? this.marker_,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
}
if (name.present) {
map['name'] = i0.Variable<String>(name.value);
}
if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
}
if (backupSelection.present) {
map['backup_selection'] = i0.Variable<int>(i1
.$LocalAlbumEntityTable.$converterbackupSelection
.toSql(backupSelection.value));
}
if (marker_.present) {
map['marker'] = i0.Variable<bool>(marker_.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('LocalAlbumEntityCompanion(')
..write('id: $id, ')
..write('name: $name, ')
..write('updatedAt: $updatedAt, ')
..write('backupSelection: $backupSelection, ')
..write('marker_: $marker_')
..write(')'))
.toString();
}
}

View File

@ -0,0 +1,17 @@
import 'package:drift/drift.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/utils/drift_default.mixin.dart';
class LocalAlbumAssetEntity extends Table with DriftDefaultsMixin {
const LocalAlbumAssetEntity();
TextColumn get assetId =>
text().references(LocalAssetEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get albumId =>
text().references(LocalAlbumEntity, #id, onDelete: KeyAction.cascade)();
@override
Set<Column> get primaryKey => {assetId, albumId};
}

View File

@ -0,0 +1,565 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'
as i2;
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
as i3;
import 'package:drift/internal/modular.dart' as i4;
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
as i5;
typedef $$LocalAlbumAssetEntityTableCreateCompanionBuilder
= i1.LocalAlbumAssetEntityCompanion Function({
required String assetId,
required String albumId,
});
typedef $$LocalAlbumAssetEntityTableUpdateCompanionBuilder
= i1.LocalAlbumAssetEntityCompanion Function({
i0.Value<String> assetId,
i0.Value<String> albumId,
});
final class $$LocalAlbumAssetEntityTableReferences extends i0.BaseReferences<
i0.GeneratedDatabase,
i1.$LocalAlbumAssetEntityTable,
i1.LocalAlbumAssetEntityData> {
$$LocalAlbumAssetEntityTableReferences(
super.$_db, super.$_table, super.$_typedResult);
static i3.$LocalAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
i4.ReadDatabaseContainer(db)
.resultSet<i3.$LocalAssetEntityTable>('local_asset_entity')
.createAlias(i0.$_aliasNameGenerator(
i4.ReadDatabaseContainer(db)
.resultSet<i1.$LocalAlbumAssetEntityTable>(
'local_album_asset_entity')
.assetId,
i4.ReadDatabaseContainer(db)
.resultSet<i3.$LocalAssetEntityTable>('local_asset_entity')
.id));
i3.$$LocalAssetEntityTableProcessedTableManager get assetId {
final $_column = $_itemColumn<String>('asset_id')!;
final manager = i3
.$$LocalAssetEntityTableTableManager(
$_db,
i4.ReadDatabaseContainer($_db)
.resultSet<i3.$LocalAssetEntityTable>('local_asset_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
static i5.$LocalAlbumEntityTable _albumIdTable(i0.GeneratedDatabase db) =>
i4.ReadDatabaseContainer(db)
.resultSet<i5.$LocalAlbumEntityTable>('local_album_entity')
.createAlias(i0.$_aliasNameGenerator(
i4.ReadDatabaseContainer(db)
.resultSet<i1.$LocalAlbumAssetEntityTable>(
'local_album_asset_entity')
.albumId,
i4.ReadDatabaseContainer(db)
.resultSet<i5.$LocalAlbumEntityTable>('local_album_entity')
.id));
i5.$$LocalAlbumEntityTableProcessedTableManager get albumId {
final $_column = $_itemColumn<String>('album_id')!;
final manager = i5
.$$LocalAlbumEntityTableTableManager(
$_db,
i4.ReadDatabaseContainer($_db)
.resultSet<i5.$LocalAlbumEntityTable>('local_album_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_albumIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
}
class $$LocalAlbumAssetEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumAssetEntityTable> {
$$LocalAlbumAssetEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i3.$$LocalAssetEntityTableFilterComposer get assetId {
final i3.$$LocalAssetEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i4.ReadDatabaseContainer($db)
.resultSet<i3.$LocalAssetEntityTable>('local_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i3.$$LocalAssetEntityTableFilterComposer(
$db: $db,
$table: i4.ReadDatabaseContainer($db)
.resultSet<i3.$LocalAssetEntityTable>('local_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i5.$$LocalAlbumEntityTableFilterComposer get albumId {
final i5.$$LocalAlbumEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.albumId,
referencedTable: i4.ReadDatabaseContainer($db)
.resultSet<i5.$LocalAlbumEntityTable>('local_album_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$LocalAlbumEntityTableFilterComposer(
$db: $db,
$table: i4.ReadDatabaseContainer($db)
.resultSet<i5.$LocalAlbumEntityTable>('local_album_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$LocalAlbumAssetEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumAssetEntityTable> {
$$LocalAlbumAssetEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i3.$$LocalAssetEntityTableOrderingComposer get assetId {
final i3.$$LocalAssetEntityTableOrderingComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i4.ReadDatabaseContainer($db)
.resultSet<i3.$LocalAssetEntityTable>('local_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i3.$$LocalAssetEntityTableOrderingComposer(
$db: $db,
$table: i4.ReadDatabaseContainer($db)
.resultSet<i3.$LocalAssetEntityTable>(
'local_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i5.$$LocalAlbumEntityTableOrderingComposer get albumId {
final i5.$$LocalAlbumEntityTableOrderingComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.albumId,
referencedTable: i4.ReadDatabaseContainer($db)
.resultSet<i5.$LocalAlbumEntityTable>('local_album_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$LocalAlbumEntityTableOrderingComposer(
$db: $db,
$table: i4.ReadDatabaseContainer($db)
.resultSet<i5.$LocalAlbumEntityTable>(
'local_album_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$LocalAlbumAssetEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumAssetEntityTable> {
$$LocalAlbumAssetEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i3.$$LocalAssetEntityTableAnnotationComposer get assetId {
final i3.$$LocalAssetEntityTableAnnotationComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i4.ReadDatabaseContainer($db)
.resultSet<i3.$LocalAssetEntityTable>('local_asset_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i3.$$LocalAssetEntityTableAnnotationComposer(
$db: $db,
$table: i4.ReadDatabaseContainer($db)
.resultSet<i3.$LocalAssetEntityTable>(
'local_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
i5.$$LocalAlbumEntityTableAnnotationComposer get albumId {
final i5.$$LocalAlbumEntityTableAnnotationComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.albumId,
referencedTable: i4.ReadDatabaseContainer($db)
.resultSet<i5.$LocalAlbumEntityTable>('local_album_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$LocalAlbumEntityTableAnnotationComposer(
$db: $db,
$table: i4.ReadDatabaseContainer($db)
.resultSet<i5.$LocalAlbumEntityTable>(
'local_album_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $$LocalAlbumAssetEntityTableTableManager extends i0.RootTableManager<
i0.GeneratedDatabase,
i1.$LocalAlbumAssetEntityTable,
i1.LocalAlbumAssetEntityData,
i1.$$LocalAlbumAssetEntityTableFilterComposer,
i1.$$LocalAlbumAssetEntityTableOrderingComposer,
i1.$$LocalAlbumAssetEntityTableAnnotationComposer,
$$LocalAlbumAssetEntityTableCreateCompanionBuilder,
$$LocalAlbumAssetEntityTableUpdateCompanionBuilder,
(i1.LocalAlbumAssetEntityData, i1.$$LocalAlbumAssetEntityTableReferences),
i1.LocalAlbumAssetEntityData,
i0.PrefetchHooks Function({bool assetId, bool albumId})> {
$$LocalAlbumAssetEntityTableTableManager(
i0.GeneratedDatabase db, i1.$LocalAlbumAssetEntityTable table)
: super(i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$LocalAlbumAssetEntityTableFilterComposer(
$db: db, $table: table),
createOrderingComposer: () =>
i1.$$LocalAlbumAssetEntityTableOrderingComposer(
$db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$LocalAlbumAssetEntityTableAnnotationComposer(
$db: db, $table: table),
updateCompanionCallback: ({
i0.Value<String> assetId = const i0.Value.absent(),
i0.Value<String> albumId = const i0.Value.absent(),
}) =>
i1.LocalAlbumAssetEntityCompanion(
assetId: assetId,
albumId: albumId,
),
createCompanionCallback: ({
required String assetId,
required String albumId,
}) =>
i1.LocalAlbumAssetEntityCompanion.insert(
assetId: assetId,
albumId: albumId,
),
withReferenceMapper: (p0) => p0
.map((e) => (
e.readTable(table),
i1.$$LocalAlbumAssetEntityTableReferences(db, table, e)
))
.toList(),
prefetchHooksCallback: ({assetId = false, albumId = false}) {
return i0.PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
addJoins: <
T extends i0.TableManagerState<
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic>>(state) {
if (assetId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.assetId,
referencedTable: i1.$$LocalAlbumAssetEntityTableReferences
._assetIdTable(db),
referencedColumn: i1.$$LocalAlbumAssetEntityTableReferences
._assetIdTable(db)
.id,
) as T;
}
if (albumId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.albumId,
referencedTable: i1.$$LocalAlbumAssetEntityTableReferences
._albumIdTable(db),
referencedColumn: i1.$$LocalAlbumAssetEntityTableReferences
._albumIdTable(db)
.id,
) as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
));
}
typedef $$LocalAlbumAssetEntityTableProcessedTableManager
= i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$LocalAlbumAssetEntityTable,
i1.LocalAlbumAssetEntityData,
i1.$$LocalAlbumAssetEntityTableFilterComposer,
i1.$$LocalAlbumAssetEntityTableOrderingComposer,
i1.$$LocalAlbumAssetEntityTableAnnotationComposer,
$$LocalAlbumAssetEntityTableCreateCompanionBuilder,
$$LocalAlbumAssetEntityTableUpdateCompanionBuilder,
(
i1.LocalAlbumAssetEntityData,
i1.$$LocalAlbumAssetEntityTableReferences
),
i1.LocalAlbumAssetEntityData,
i0.PrefetchHooks Function({bool assetId, bool albumId})>;
class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity
with
i0
.TableInfo<$LocalAlbumAssetEntityTable, i1.LocalAlbumAssetEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$LocalAlbumAssetEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _assetIdMeta =
const i0.VerificationMeta('assetId');
@override
late final i0.GeneratedColumn<String> assetId = i0.GeneratedColumn<String>(
'asset_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES local_asset_entity (id) ON DELETE CASCADE'));
static const i0.VerificationMeta _albumIdMeta =
const i0.VerificationMeta('albumId');
@override
late final i0.GeneratedColumn<String> albumId = i0.GeneratedColumn<String>(
'album_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES local_album_entity (id) ON DELETE CASCADE'));
@override
List<i0.GeneratedColumn> get $columns => [assetId, albumId];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'local_album_asset_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.LocalAlbumAssetEntityData> instance,
{bool isInserting = false}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('asset_id')) {
context.handle(_assetIdMeta,
assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta));
} else if (isInserting) {
context.missing(_assetIdMeta);
}
if (data.containsKey('album_id')) {
context.handle(_albumIdMeta,
albumId.isAcceptableOrUnknown(data['album_id']!, _albumIdMeta));
} else if (isInserting) {
context.missing(_albumIdMeta);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {assetId, albumId};
@override
i1.LocalAlbumAssetEntityData map(Map<String, dynamic> data,
{String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.LocalAlbumAssetEntityData(
assetId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!,
albumId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}album_id'])!,
);
}
@override
$LocalAlbumAssetEntityTable createAlias(String alias) {
return $LocalAlbumAssetEntityTable(attachedDatabase, alias);
}
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class LocalAlbumAssetEntityData extends i0.DataClass
implements i0.Insertable<i1.LocalAlbumAssetEntityData> {
final String assetId;
final String albumId;
const LocalAlbumAssetEntityData(
{required this.assetId, required this.albumId});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['asset_id'] = i0.Variable<String>(assetId);
map['album_id'] = i0.Variable<String>(albumId);
return map;
}
factory LocalAlbumAssetEntityData.fromJson(Map<String, dynamic> json,
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return LocalAlbumAssetEntityData(
assetId: serializer.fromJson<String>(json['assetId']),
albumId: serializer.fromJson<String>(json['albumId']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'assetId': serializer.toJson<String>(assetId),
'albumId': serializer.toJson<String>(albumId),
};
}
i1.LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) =>
i1.LocalAlbumAssetEntityData(
assetId: assetId ?? this.assetId,
albumId: albumId ?? this.albumId,
);
LocalAlbumAssetEntityData copyWithCompanion(
i1.LocalAlbumAssetEntityCompanion data) {
return LocalAlbumAssetEntityData(
assetId: data.assetId.present ? data.assetId.value : this.assetId,
albumId: data.albumId.present ? data.albumId.value : this.albumId,
);
}
@override
String toString() {
return (StringBuffer('LocalAlbumAssetEntityData(')
..write('assetId: $assetId, ')
..write('albumId: $albumId')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(assetId, albumId);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.LocalAlbumAssetEntityData &&
other.assetId == this.assetId &&
other.albumId == this.albumId);
}
class LocalAlbumAssetEntityCompanion
extends i0.UpdateCompanion<i1.LocalAlbumAssetEntityData> {
final i0.Value<String> assetId;
final i0.Value<String> albumId;
const LocalAlbumAssetEntityCompanion({
this.assetId = const i0.Value.absent(),
this.albumId = const i0.Value.absent(),
});
LocalAlbumAssetEntityCompanion.insert({
required String assetId,
required String albumId,
}) : assetId = i0.Value(assetId),
albumId = i0.Value(albumId);
static i0.Insertable<i1.LocalAlbumAssetEntityData> custom({
i0.Expression<String>? assetId,
i0.Expression<String>? albumId,
}) {
return i0.RawValuesInsertable({
if (assetId != null) 'asset_id': assetId,
if (albumId != null) 'album_id': albumId,
});
}
i1.LocalAlbumAssetEntityCompanion copyWith(
{i0.Value<String>? assetId, i0.Value<String>? albumId}) {
return i1.LocalAlbumAssetEntityCompanion(
assetId: assetId ?? this.assetId,
albumId: albumId ?? this.albumId,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (assetId.present) {
map['asset_id'] = i0.Variable<String>(assetId.value);
}
if (albumId.present) {
map['album_id'] = i0.Variable<String>(albumId.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('LocalAlbumAssetEntityCompanion(')
..write('assetId: $assetId, ')
..write('albumId: $albumId')
..write(')'))
.toString();
}
}

View File

@ -0,0 +1,17 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex(name: 'idx_local_asset_checksum', columns: {#checksum})
class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
const LocalAssetEntity();
TextColumn get id => text()();
TextColumn get checksum => text().nullable()();
// Only used during backup to mirror the favorite status of the asset in the server
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {id};
}

View File

@ -0,0 +1,658 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
as i1;
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'
as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
typedef $$LocalAssetEntityTableCreateCompanionBuilder
= i1.LocalAssetEntityCompanion Function({
required String name,
required i2.AssetType type,
i0.Value<DateTime> createdAt,
i0.Value<DateTime> updatedAt,
i0.Value<int?> durationInSeconds,
required String id,
i0.Value<String?> checksum,
i0.Value<bool> isFavorite,
});
typedef $$LocalAssetEntityTableUpdateCompanionBuilder
= i1.LocalAssetEntityCompanion Function({
i0.Value<String> name,
i0.Value<i2.AssetType> type,
i0.Value<DateTime> createdAt,
i0.Value<DateTime> updatedAt,
i0.Value<int?> durationInSeconds,
i0.Value<String> id,
i0.Value<String?> checksum,
i0.Value<bool> isFavorite,
});
class $$LocalAssetEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAssetEntityTable> {
$$LocalAssetEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get name => $composableBuilder(
column: $table.name, builder: (column) => i0.ColumnFilters(column));
i0.ColumnWithTypeConverterFilters<i2.AssetType, i2.AssetType, int> get type =>
$composableBuilder(
column: $table.type,
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i0.ColumnFilters<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<int> get durationInSeconds => $composableBuilder(
column: $table.durationInSeconds,
builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get checksum => $composableBuilder(
column: $table.checksum, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<bool> get isFavorite => $composableBuilder(
column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column));
}
class $$LocalAssetEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAssetEntityTable> {
$$LocalAssetEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get name => $composableBuilder(
column: $table.name, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get type => $composableBuilder(
column: $table.type, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get createdAt => $composableBuilder(
column: $table.createdAt,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<int> get durationInSeconds => $composableBuilder(
column: $table.durationInSeconds,
builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get checksum => $composableBuilder(
column: $table.checksum, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<bool> get isFavorite => $composableBuilder(
column: $table.isFavorite,
builder: (column) => i0.ColumnOrderings(column));
}
class $$LocalAssetEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAssetEntityTable> {
$$LocalAssetEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column);
i0.GeneratedColumnWithTypeConverter<i2.AssetType, int> get type =>
$composableBuilder(column: $table.type, builder: (column) => column);
i0.GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
i0.GeneratedColumn<DateTime> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
i0.GeneratedColumn<int> get durationInSeconds => $composableBuilder(
column: $table.durationInSeconds, builder: (column) => column);
i0.GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<String> get checksum =>
$composableBuilder(column: $table.checksum, builder: (column) => column);
i0.GeneratedColumn<bool> get isFavorite => $composableBuilder(
column: $table.isFavorite, builder: (column) => column);
}
class $$LocalAssetEntityTableTableManager extends i0.RootTableManager<
i0.GeneratedDatabase,
i1.$LocalAssetEntityTable,
i1.LocalAssetEntityData,
i1.$$LocalAssetEntityTableFilterComposer,
i1.$$LocalAssetEntityTableOrderingComposer,
i1.$$LocalAssetEntityTableAnnotationComposer,
$$LocalAssetEntityTableCreateCompanionBuilder,
$$LocalAssetEntityTableUpdateCompanionBuilder,
(
i1.LocalAssetEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAssetEntityTable,
i1.LocalAssetEntityData>
),
i1.LocalAssetEntityData,
i0.PrefetchHooks Function()> {
$$LocalAssetEntityTableTableManager(
i0.GeneratedDatabase db, i1.$LocalAssetEntityTable table)
: super(i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$LocalAssetEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () => i1
.$$LocalAssetEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i1.$$LocalAssetEntityTableAnnotationComposer(
$db: db, $table: table),
updateCompanionCallback: ({
i0.Value<String> name = const i0.Value.absent(),
i0.Value<i2.AssetType> type = const i0.Value.absent(),
i0.Value<DateTime> createdAt = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String?> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(),
}) =>
i1.LocalAssetEntityCompanion(
name: name,
type: type,
createdAt: createdAt,
updatedAt: updatedAt,
durationInSeconds: durationInSeconds,
id: id,
checksum: checksum,
isFavorite: isFavorite,
),
createCompanionCallback: ({
required String name,
required i2.AssetType type,
i0.Value<DateTime> createdAt = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
required String id,
i0.Value<String?> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(),
}) =>
i1.LocalAssetEntityCompanion.insert(
name: name,
type: type,
createdAt: createdAt,
updatedAt: updatedAt,
durationInSeconds: durationInSeconds,
id: id,
checksum: checksum,
isFavorite: isFavorite,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $$LocalAssetEntityTableProcessedTableManager = i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$LocalAssetEntityTable,
i1.LocalAssetEntityData,
i1.$$LocalAssetEntityTableFilterComposer,
i1.$$LocalAssetEntityTableOrderingComposer,
i1.$$LocalAssetEntityTableAnnotationComposer,
$$LocalAssetEntityTableCreateCompanionBuilder,
$$LocalAssetEntityTableUpdateCompanionBuilder,
(
i1.LocalAssetEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAssetEntityTable,
i1.LocalAssetEntityData>
),
i1.LocalAssetEntityData,
i0.PrefetchHooks Function()>;
i0.Index get idxLocalAssetChecksum => i0.Index('idx_local_asset_checksum',
'CREATE INDEX idx_local_asset_checksum ON local_asset_entity (checksum)');
class $LocalAssetEntityTable extends i3.LocalAssetEntity
with i0.TableInfo<$LocalAssetEntityTable, i1.LocalAssetEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$LocalAssetEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _nameMeta =
const i0.VerificationMeta('name');
@override
late final i0.GeneratedColumn<String> name = i0.GeneratedColumn<String>(
'name', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
@override
late final i0.GeneratedColumnWithTypeConverter<i2.AssetType, int> type =
i0.GeneratedColumn<int>('type', aliasedName, false,
type: i0.DriftSqlType.int, requiredDuringInsert: true)
.withConverter<i2.AssetType>(
i1.$LocalAssetEntityTable.$convertertype);
static const i0.VerificationMeta _createdAtMeta =
const i0.VerificationMeta('createdAt');
@override
late final i0.GeneratedColumn<DateTime> createdAt =
i0.GeneratedColumn<DateTime>('created_at', aliasedName, false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i4.currentDateAndTime);
static const i0.VerificationMeta _updatedAtMeta =
const i0.VerificationMeta('updatedAt');
@override
late final i0.GeneratedColumn<DateTime> updatedAt =
i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i4.currentDateAndTime);
static const i0.VerificationMeta _durationInSecondsMeta =
const i0.VerificationMeta('durationInSeconds');
@override
late final i0.GeneratedColumn<int> durationInSeconds =
i0.GeneratedColumn<int>('duration_in_seconds', aliasedName, true,
type: i0.DriftSqlType.int, requiredDuringInsert: false);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _checksumMeta =
const i0.VerificationMeta('checksum');
@override
late final i0.GeneratedColumn<String> checksum = i0.GeneratedColumn<String>(
'checksum', aliasedName, true,
type: i0.DriftSqlType.string, requiredDuringInsert: false);
static const i0.VerificationMeta _isFavoriteMeta =
const i0.VerificationMeta('isFavorite');
@override
late final i0.GeneratedColumn<bool> isFavorite = i0.GeneratedColumn<bool>(
'is_favorite', aliasedName, false,
type: i0.DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("is_favorite" IN (0, 1))'),
defaultValue: const i4.Constant(false));
@override
List<i0.GeneratedColumn> get $columns => [
name,
type,
createdAt,
updatedAt,
durationInSeconds,
id,
checksum,
isFavorite
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'local_asset_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.LocalAssetEntityData> instance,
{bool isInserting = false}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('name')) {
context.handle(
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
} else if (isInserting) {
context.missing(_nameMeta);
}
if (data.containsKey('created_at')) {
context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
}
if (data.containsKey('updated_at')) {
context.handle(_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
}
if (data.containsKey('duration_in_seconds')) {
context.handle(
_durationInSecondsMeta,
durationInSeconds.isAcceptableOrUnknown(
data['duration_in_seconds']!, _durationInSecondsMeta));
}
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} else if (isInserting) {
context.missing(_idMeta);
}
if (data.containsKey('checksum')) {
context.handle(_checksumMeta,
checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta));
}
if (data.containsKey('is_favorite')) {
context.handle(
_isFavoriteMeta,
isFavorite.isAcceptableOrUnknown(
data['is_favorite']!, _isFavoriteMeta));
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {id};
@override
i1.LocalAssetEntityData map(Map<String, dynamic> data,
{String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.LocalAssetEntityData(
name: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
type: i1.$LocalAssetEntityTable.$convertertype.fromSql(attachedDatabase
.typeMapping
.read(i0.DriftSqlType.int, data['${effectivePrefix}type'])!),
createdAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
durationInSeconds: attachedDatabase.typeMapping.read(
i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']),
id: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
checksum: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}checksum']),
isFavorite: attachedDatabase.typeMapping
.read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!,
);
}
@override
$LocalAssetEntityTable createAlias(String alias) {
return $LocalAssetEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i2.AssetType, int, int> $convertertype =
const i0.EnumIndexConverter<i2.AssetType>(i2.AssetType.values);
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class LocalAssetEntityData extends i0.DataClass
implements i0.Insertable<i1.LocalAssetEntityData> {
final String name;
final i2.AssetType type;
final DateTime createdAt;
final DateTime updatedAt;
final int? durationInSeconds;
final String id;
final String? checksum;
final bool isFavorite;
const LocalAssetEntityData(
{required this.name,
required this.type,
required this.createdAt,
required this.updatedAt,
this.durationInSeconds,
required this.id,
this.checksum,
required this.isFavorite});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['name'] = i0.Variable<String>(name);
{
map['type'] = i0.Variable<int>(
i1.$LocalAssetEntityTable.$convertertype.toSql(type));
}
map['created_at'] = i0.Variable<DateTime>(createdAt);
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
if (!nullToAbsent || durationInSeconds != null) {
map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds);
}
map['id'] = i0.Variable<String>(id);
if (!nullToAbsent || checksum != null) {
map['checksum'] = i0.Variable<String>(checksum);
}
map['is_favorite'] = i0.Variable<bool>(isFavorite);
return map;
}
factory LocalAssetEntityData.fromJson(Map<String, dynamic> json,
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return LocalAssetEntityData(
name: serializer.fromJson<String>(json['name']),
type: i1.$LocalAssetEntityTable.$convertertype
.fromJson(serializer.fromJson<int>(json['type'])),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
durationInSeconds: serializer.fromJson<int?>(json['durationInSeconds']),
id: serializer.fromJson<String>(json['id']),
checksum: serializer.fromJson<String?>(json['checksum']),
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'name': serializer.toJson<String>(name),
'type': serializer
.toJson<int>(i1.$LocalAssetEntityTable.$convertertype.toJson(type)),
'createdAt': serializer.toJson<DateTime>(createdAt),
'updatedAt': serializer.toJson<DateTime>(updatedAt),
'durationInSeconds': serializer.toJson<int?>(durationInSeconds),
'id': serializer.toJson<String>(id),
'checksum': serializer.toJson<String?>(checksum),
'isFavorite': serializer.toJson<bool>(isFavorite),
};
}
i1.LocalAssetEntityData copyWith(
{String? name,
i2.AssetType? type,
DateTime? createdAt,
DateTime? updatedAt,
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
String? id,
i0.Value<String?> checksum = const i0.Value.absent(),
bool? isFavorite}) =>
i1.LocalAssetEntityData(
name: name ?? this.name,
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
durationInSeconds: durationInSeconds.present
? durationInSeconds.value
: this.durationInSeconds,
id: id ?? this.id,
checksum: checksum.present ? checksum.value : this.checksum,
isFavorite: isFavorite ?? this.isFavorite,
);
LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) {
return LocalAssetEntityData(
name: data.name.present ? data.name.value : this.name,
type: data.type.present ? data.type.value : this.type,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
durationInSeconds: data.durationInSeconds.present
? data.durationInSeconds.value
: this.durationInSeconds,
id: data.id.present ? data.id.value : this.id,
checksum: data.checksum.present ? data.checksum.value : this.checksum,
isFavorite:
data.isFavorite.present ? data.isFavorite.value : this.isFavorite,
);
}
@override
String toString() {
return (StringBuffer('LocalAssetEntityData(')
..write('name: $name, ')
..write('type: $type, ')
..write('createdAt: $createdAt, ')
..write('updatedAt: $updatedAt, ')
..write('durationInSeconds: $durationInSeconds, ')
..write('id: $id, ')
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(name, type, createdAt, updatedAt,
durationInSeconds, id, checksum, isFavorite);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.LocalAssetEntityData &&
other.name == this.name &&
other.type == this.type &&
other.createdAt == this.createdAt &&
other.updatedAt == this.updatedAt &&
other.durationInSeconds == this.durationInSeconds &&
other.id == this.id &&
other.checksum == this.checksum &&
other.isFavorite == this.isFavorite);
}
class LocalAssetEntityCompanion
extends i0.UpdateCompanion<i1.LocalAssetEntityData> {
final i0.Value<String> name;
final i0.Value<i2.AssetType> type;
final i0.Value<DateTime> createdAt;
final i0.Value<DateTime> updatedAt;
final i0.Value<int?> durationInSeconds;
final i0.Value<String> id;
final i0.Value<String?> checksum;
final i0.Value<bool> isFavorite;
const LocalAssetEntityCompanion({
this.name = const i0.Value.absent(),
this.type = const i0.Value.absent(),
this.createdAt = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(),
this.id = const i0.Value.absent(),
this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(),
});
LocalAssetEntityCompanion.insert({
required String name,
required i2.AssetType type,
this.createdAt = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
this.durationInSeconds = const i0.Value.absent(),
required String id,
this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(),
}) : name = i0.Value(name),
type = i0.Value(type),
id = i0.Value(id);
static i0.Insertable<i1.LocalAssetEntityData> custom({
i0.Expression<String>? name,
i0.Expression<int>? type,
i0.Expression<DateTime>? createdAt,
i0.Expression<DateTime>? updatedAt,
i0.Expression<int>? durationInSeconds,
i0.Expression<String>? id,
i0.Expression<String>? checksum,
i0.Expression<bool>? isFavorite,
}) {
return i0.RawValuesInsertable({
if (name != null) 'name': name,
if (type != null) 'type': type,
if (createdAt != null) 'created_at': createdAt,
if (updatedAt != null) 'updated_at': updatedAt,
if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds,
if (id != null) 'id': id,
if (checksum != null) 'checksum': checksum,
if (isFavorite != null) 'is_favorite': isFavorite,
});
}
i1.LocalAssetEntityCompanion copyWith(
{i0.Value<String>? name,
i0.Value<i2.AssetType>? type,
i0.Value<DateTime>? createdAt,
i0.Value<DateTime>? updatedAt,
i0.Value<int?>? durationInSeconds,
i0.Value<String>? id,
i0.Value<String?>? checksum,
i0.Value<bool>? isFavorite}) {
return i1.LocalAssetEntityCompanion(
name: name ?? this.name,
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
id: id ?? this.id,
checksum: checksum ?? this.checksum,
isFavorite: isFavorite ?? this.isFavorite,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (name.present) {
map['name'] = i0.Variable<String>(name.value);
}
if (type.present) {
map['type'] = i0.Variable<int>(
i1.$LocalAssetEntityTable.$convertertype.toSql(type.value));
}
if (createdAt.present) {
map['created_at'] = i0.Variable<DateTime>(createdAt.value);
}
if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
}
if (durationInSeconds.present) {
map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds.value);
}
if (id.present) {
map['id'] = i0.Variable<String>(id.value);
}
if (checksum.present) {
map['checksum'] = i0.Variable<String>(checksum.value);
}
if (isFavorite.present) {
map['is_favorite'] = i0.Variable<bool>(isFavorite.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('LocalAssetEntityCompanion(')
..write('name: $name, ')
..write('type: $type, ')
..write('createdAt: $createdAt, ')
..write('updatedAt: $updatedAt, ')
..write('durationInSeconds: $durationInSeconds, ')
..write('id: $id, ')
..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite')
..write(')'))
.toString();
}
}

View File

@ -5,11 +5,11 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class PartnerEntity extends Table with DriftDefaultsMixin {
const PartnerEntity();
BlobColumn get sharedById =>
blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get sharedById =>
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
BlobColumn get sharedWithId =>
blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get sharedWithId =>
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
BoolColumn get inTimeline => boolean().withDefault(const Constant(false))();

View File

@ -3,24 +3,23 @@
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
as i1;
import 'dart:typed_data' as i2;
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'
as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i5;
import 'package:drift/internal/modular.dart' as i6;
as i4;
import 'package:drift/internal/modular.dart' as i5;
typedef $$PartnerEntityTableCreateCompanionBuilder = i1.PartnerEntityCompanion
Function({
required i2.Uint8List sharedById,
required i2.Uint8List sharedWithId,
required String sharedById,
required String sharedWithId,
i0.Value<bool> inTimeline,
});
typedef $$PartnerEntityTableUpdateCompanionBuilder = i1.PartnerEntityCompanion
Function({
i0.Value<i2.Uint8List> sharedById,
i0.Value<i2.Uint8List> sharedWithId,
i0.Value<String> sharedById,
i0.Value<String> sharedWithId,
i0.Value<bool> inTimeline,
});
@ -29,25 +28,25 @@ final class $$PartnerEntityTableReferences extends i0.BaseReferences<
$$PartnerEntityTableReferences(
super.$_db, super.$_table, super.$_typedResult);
static i5.$UserEntityTable _sharedByIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
static i4.$UserEntityTable _sharedByIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
i5.ReadDatabaseContainer(db)
.resultSet<i1.$PartnerEntityTable>('partner_entity')
.sharedById,
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.id));
i5.$$UserEntityTableProcessedTableManager get sharedById {
final $_column = $_itemColumn<i2.Uint8List>('shared_by_id')!;
i4.$$UserEntityTableProcessedTableManager get sharedById {
final $_column = $_itemColumn<String>('shared_by_id')!;
final manager = i5
final manager = i4
.$$UserEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer($_db)
.resultSet<i5.$UserEntityTable>('user_entity'))
i5.ReadDatabaseContainer($_db)
.resultSet<i4.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_sharedByIdTable($_db));
if (item == null) return manager;
@ -55,25 +54,25 @@ final class $$PartnerEntityTableReferences extends i0.BaseReferences<
manager.$state.copyWith(prefetchedData: [item]));
}
static i5.$UserEntityTable _sharedWithIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
static i4.$UserEntityTable _sharedWithIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
i5.ReadDatabaseContainer(db)
.resultSet<i1.$PartnerEntityTable>('partner_entity')
.sharedWithId,
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.id));
i5.$$UserEntityTableProcessedTableManager get sharedWithId {
final $_column = $_itemColumn<i2.Uint8List>('shared_with_id')!;
i4.$$UserEntityTableProcessedTableManager get sharedWithId {
final $_column = $_itemColumn<String>('shared_with_id')!;
final manager = i5
final manager = i4
.$$UserEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer($_db)
.resultSet<i5.$UserEntityTable>('user_entity'))
i5.ReadDatabaseContainer($_db)
.resultSet<i4.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_sharedWithIdTable($_db));
if (item == null) return manager;
@ -94,20 +93,20 @@ class $$PartnerEntityTableFilterComposer
i0.ColumnFilters<bool> get inTimeline => $composableBuilder(
column: $table.inTimeline, builder: (column) => i0.ColumnFilters(column));
i5.$$UserEntityTableFilterComposer get sharedById {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
i4.$$UserEntityTableFilterComposer get sharedById {
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedById,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableFilterComposer(
i4.$$UserEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -116,20 +115,20 @@ class $$PartnerEntityTableFilterComposer
return composer;
}
i5.$$UserEntityTableFilterComposer get sharedWithId {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
i4.$$UserEntityTableFilterComposer get sharedWithId {
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedWithId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableFilterComposer(
i4.$$UserEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -152,20 +151,20 @@ class $$PartnerEntityTableOrderingComposer
column: $table.inTimeline,
builder: (column) => i0.ColumnOrderings(column));
i5.$$UserEntityTableOrderingComposer get sharedById {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
i4.$$UserEntityTableOrderingComposer get sharedById {
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedById,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableOrderingComposer(
i4.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -174,20 +173,20 @@ class $$PartnerEntityTableOrderingComposer
return composer;
}
i5.$$UserEntityTableOrderingComposer get sharedWithId {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
i4.$$UserEntityTableOrderingComposer get sharedWithId {
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedWithId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableOrderingComposer(
i4.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -209,20 +208,20 @@ class $$PartnerEntityTableAnnotationComposer
i0.GeneratedColumn<bool> get inTimeline => $composableBuilder(
column: $table.inTimeline, builder: (column) => column);
i5.$$UserEntityTableAnnotationComposer get sharedById {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
i4.$$UserEntityTableAnnotationComposer get sharedById {
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedById,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableAnnotationComposer(
i4.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -231,20 +230,20 @@ class $$PartnerEntityTableAnnotationComposer
return composer;
}
i5.$$UserEntityTableAnnotationComposer get sharedWithId {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
i4.$$UserEntityTableAnnotationComposer get sharedWithId {
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.sharedWithId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableAnnotationComposer(
i4.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -278,8 +277,8 @@ class $$PartnerEntityTableTableManager extends i0.RootTableManager<
createComputedFieldComposer: () =>
i1.$$PartnerEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
i0.Value<i2.Uint8List> sharedById = const i0.Value.absent(),
i0.Value<i2.Uint8List> sharedWithId = const i0.Value.absent(),
i0.Value<String> sharedById = const i0.Value.absent(),
i0.Value<String> sharedWithId = const i0.Value.absent(),
i0.Value<bool> inTimeline = const i0.Value.absent(),
}) =>
i1.PartnerEntityCompanion(
@ -288,8 +287,8 @@ class $$PartnerEntityTableTableManager extends i0.RootTableManager<
inTimeline: inTimeline,
),
createCompanionCallback: ({
required i2.Uint8List sharedById,
required i2.Uint8List sharedWithId,
required String sharedById,
required String sharedWithId,
i0.Value<bool> inTimeline = const i0.Value.absent(),
}) =>
i1.PartnerEntityCompanion.insert(
@ -366,7 +365,7 @@ typedef $$PartnerEntityTableProcessedTableManager = i0.ProcessedTableManager<
i1.PartnerEntityData,
i0.PrefetchHooks Function({bool sharedById, bool sharedWithId})>;
class $PartnerEntityTable extends i3.PartnerEntity
class $PartnerEntityTable extends i2.PartnerEntity
with i0.TableInfo<$PartnerEntityTable, i1.PartnerEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
@ -375,18 +374,18 @@ class $PartnerEntityTable extends i3.PartnerEntity
static const i0.VerificationMeta _sharedByIdMeta =
const i0.VerificationMeta('sharedById');
@override
late final i0.GeneratedColumn<i2.Uint8List> sharedById =
i0.GeneratedColumn<i2.Uint8List>('shared_by_id', aliasedName, false,
type: i0.DriftSqlType.blob,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
late final i0.GeneratedColumn<String> sharedById = i0.GeneratedColumn<String>(
'shared_by_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
static const i0.VerificationMeta _sharedWithIdMeta =
const i0.VerificationMeta('sharedWithId');
@override
late final i0.GeneratedColumn<i2.Uint8List> sharedWithId =
i0.GeneratedColumn<i2.Uint8List>('shared_with_id', aliasedName, false,
type: i0.DriftSqlType.blob,
late final i0.GeneratedColumn<String> sharedWithId =
i0.GeneratedColumn<String>('shared_with_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
@ -399,7 +398,7 @@ class $PartnerEntityTable extends i3.PartnerEntity
requiredDuringInsert: false,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'CHECK ("in_timeline" IN (0, 1))'),
defaultValue: const i4.Constant(false));
defaultValue: const i3.Constant(false));
@override
List<i0.GeneratedColumn> get $columns =>
[sharedById, sharedWithId, inTimeline];
@ -445,10 +444,10 @@ class $PartnerEntityTable extends i3.PartnerEntity
i1.PartnerEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.PartnerEntityData(
sharedById: attachedDatabase.typeMapping
.read(i0.DriftSqlType.blob, data['${effectivePrefix}shared_by_id'])!,
sharedById: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, data['${effectivePrefix}shared_by_id'])!,
sharedWithId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.blob, data['${effectivePrefix}shared_with_id'])!,
i0.DriftSqlType.string, data['${effectivePrefix}shared_with_id'])!,
inTimeline: attachedDatabase.typeMapping
.read(i0.DriftSqlType.bool, data['${effectivePrefix}in_timeline'])!,
);
@ -467,8 +466,8 @@ class $PartnerEntityTable extends i3.PartnerEntity
class PartnerEntityData extends i0.DataClass
implements i0.Insertable<i1.PartnerEntityData> {
final i2.Uint8List sharedById;
final i2.Uint8List sharedWithId;
final String sharedById;
final String sharedWithId;
final bool inTimeline;
const PartnerEntityData(
{required this.sharedById,
@ -477,8 +476,8 @@ class PartnerEntityData extends i0.DataClass
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['shared_by_id'] = i0.Variable<i2.Uint8List>(sharedById);
map['shared_with_id'] = i0.Variable<i2.Uint8List>(sharedWithId);
map['shared_by_id'] = i0.Variable<String>(sharedById);
map['shared_with_id'] = i0.Variable<String>(sharedWithId);
map['in_timeline'] = i0.Variable<bool>(inTimeline);
return map;
}
@ -487,8 +486,8 @@ class PartnerEntityData extends i0.DataClass
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return PartnerEntityData(
sharedById: serializer.fromJson<i2.Uint8List>(json['sharedById']),
sharedWithId: serializer.fromJson<i2.Uint8List>(json['sharedWithId']),
sharedById: serializer.fromJson<String>(json['sharedById']),
sharedWithId: serializer.fromJson<String>(json['sharedWithId']),
inTimeline: serializer.fromJson<bool>(json['inTimeline']),
);
}
@ -496,16 +495,14 @@ class PartnerEntityData extends i0.DataClass
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'sharedById': serializer.toJson<i2.Uint8List>(sharedById),
'sharedWithId': serializer.toJson<i2.Uint8List>(sharedWithId),
'sharedById': serializer.toJson<String>(sharedById),
'sharedWithId': serializer.toJson<String>(sharedWithId),
'inTimeline': serializer.toJson<bool>(inTimeline),
};
}
i1.PartnerEntityData copyWith(
{i2.Uint8List? sharedById,
i2.Uint8List? sharedWithId,
bool? inTimeline}) =>
{String? sharedById, String? sharedWithId, bool? inTimeline}) =>
i1.PartnerEntityData(
sharedById: sharedById ?? this.sharedById,
sharedWithId: sharedWithId ?? this.sharedWithId,
@ -534,20 +531,19 @@ class PartnerEntityData extends i0.DataClass
}
@override
int get hashCode => Object.hash(i0.$driftBlobEquality.hash(sharedById),
i0.$driftBlobEquality.hash(sharedWithId), inTimeline);
int get hashCode => Object.hash(sharedById, sharedWithId, inTimeline);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.PartnerEntityData &&
i0.$driftBlobEquality.equals(other.sharedById, this.sharedById) &&
i0.$driftBlobEquality.equals(other.sharedWithId, this.sharedWithId) &&
other.sharedById == this.sharedById &&
other.sharedWithId == this.sharedWithId &&
other.inTimeline == this.inTimeline);
}
class PartnerEntityCompanion extends i0.UpdateCompanion<i1.PartnerEntityData> {
final i0.Value<i2.Uint8List> sharedById;
final i0.Value<i2.Uint8List> sharedWithId;
final i0.Value<String> sharedById;
final i0.Value<String> sharedWithId;
final i0.Value<bool> inTimeline;
const PartnerEntityCompanion({
this.sharedById = const i0.Value.absent(),
@ -555,14 +551,14 @@ class PartnerEntityCompanion extends i0.UpdateCompanion<i1.PartnerEntityData> {
this.inTimeline = const i0.Value.absent(),
});
PartnerEntityCompanion.insert({
required i2.Uint8List sharedById,
required i2.Uint8List sharedWithId,
required String sharedById,
required String sharedWithId,
this.inTimeline = const i0.Value.absent(),
}) : sharedById = i0.Value(sharedById),
sharedWithId = i0.Value(sharedWithId);
static i0.Insertable<i1.PartnerEntityData> custom({
i0.Expression<i2.Uint8List>? sharedById,
i0.Expression<i2.Uint8List>? sharedWithId,
i0.Expression<String>? sharedById,
i0.Expression<String>? sharedWithId,
i0.Expression<bool>? inTimeline,
}) {
return i0.RawValuesInsertable({
@ -573,8 +569,8 @@ class PartnerEntityCompanion extends i0.UpdateCompanion<i1.PartnerEntityData> {
}
i1.PartnerEntityCompanion copyWith(
{i0.Value<i2.Uint8List>? sharedById,
i0.Value<i2.Uint8List>? sharedWithId,
{i0.Value<String>? sharedById,
i0.Value<String>? sharedWithId,
i0.Value<bool>? inTimeline}) {
return i1.PartnerEntityCompanion(
sharedById: sharedById ?? this.sharedById,
@ -587,10 +583,10 @@ class PartnerEntityCompanion extends i0.UpdateCompanion<i1.PartnerEntityData> {
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (sharedById.present) {
map['shared_by_id'] = i0.Variable<i2.Uint8List>(sharedById.value);
map['shared_by_id'] = i0.Variable<String>(sharedById.value);
}
if (sharedWithId.present) {
map['shared_with_id'] = i0.Variable<i2.Uint8List>(sharedWithId.value);
map['shared_with_id'] = i0.Variable<String>(sharedWithId.value);
}
if (inTimeline.present) {
map['in_timeline'] = i0.Variable<bool>(inTimeline.value);

View File

@ -0,0 +1,35 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex(
name: 'UQ_remote_asset_owner_checksum',
columns: {#checksum, #ownerId},
unique: true,
)
class RemoteAssetEntity extends Table
with DriftDefaultsMixin, AssetEntityMixin {
const RemoteAssetEntity();
TextColumn get id => text()();
TextColumn get checksum => text()();
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
TextColumn get ownerId =>
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
DateTimeColumn get localDateTime => dateTime().nullable()();
TextColumn get thumbHash => text().nullable()();
DateTimeColumn get deletedAt => dateTime().nullable()();
IntColumn get visibility => intEnum<AssetVisibility>()();
@override
Set<Column> get primaryKey => {id};
}

File diff suppressed because it is too large Load Diff

View File

@ -78,7 +78,7 @@ class User {
class UserEntity extends Table with DriftDefaultsMixin {
const UserEntity();
BlobColumn get id => blob()();
TextColumn get id => text()();
TextColumn get name => text()();
BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
TextColumn get email => text()();

View File

@ -3,13 +3,12 @@
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i1;
import 'dart:typed_data' as i2;
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
typedef $$UserEntityTableCreateCompanionBuilder = i1.UserEntityCompanion
Function({
required i2.Uint8List id,
required String id,
required String name,
i0.Value<bool> isAdmin,
required String email,
@ -20,7 +19,7 @@ typedef $$UserEntityTableCreateCompanionBuilder = i1.UserEntityCompanion
});
typedef $$UserEntityTableUpdateCompanionBuilder = i1.UserEntityCompanion
Function({
i0.Value<i2.Uint8List> id,
i0.Value<String> id,
i0.Value<String> name,
i0.Value<bool> isAdmin,
i0.Value<String> email,
@ -39,7 +38,7 @@ class $$UserEntityTableFilterComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<i2.Uint8List> get id => $composableBuilder(
i0.ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnFilters(column));
i0.ColumnFilters<String> get name => $composableBuilder(
@ -76,7 +75,7 @@ class $$UserEntityTableOrderingComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<i2.Uint8List> get id => $composableBuilder(
i0.ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
i0.ColumnOrderings<String> get name => $composableBuilder(
@ -114,7 +113,7 @@ class $$UserEntityTableAnnotationComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<i2.Uint8List> get id =>
i0.GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
i0.GeneratedColumn<String> get name =>
@ -167,7 +166,7 @@ class $$UserEntityTableTableManager extends i0.RootTableManager<
createComputedFieldComposer: () =>
i1.$$UserEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
i0.Value<i2.Uint8List> id = const i0.Value.absent(),
i0.Value<String> id = const i0.Value.absent(),
i0.Value<String> name = const i0.Value.absent(),
i0.Value<bool> isAdmin = const i0.Value.absent(),
i0.Value<String> email = const i0.Value.absent(),
@ -187,7 +186,7 @@ class $$UserEntityTableTableManager extends i0.RootTableManager<
quotaUsageInBytes: quotaUsageInBytes,
),
createCompanionCallback: ({
required i2.Uint8List id,
required String id,
required String name,
i0.Value<bool> isAdmin = const i0.Value.absent(),
required String email,
@ -230,7 +229,7 @@ typedef $$UserEntityTableProcessedTableManager = i0.ProcessedTableManager<
i1.UserEntityData,
i0.PrefetchHooks Function()>;
class $UserEntityTable extends i3.UserEntity
class $UserEntityTable extends i2.UserEntity
with i0.TableInfo<$UserEntityTable, i1.UserEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
@ -238,9 +237,9 @@ class $UserEntityTable extends i3.UserEntity
$UserEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
@override
late final i0.GeneratedColumn<i2.Uint8List> id =
i0.GeneratedColumn<i2.Uint8List>('id', aliasedName, false,
type: i0.DriftSqlType.blob, requiredDuringInsert: true);
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
'id', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true);
static const i0.VerificationMeta _nameMeta =
const i0.VerificationMeta('name');
@override
@ -256,7 +255,7 @@ class $UserEntityTable extends i3.UserEntity
requiredDuringInsert: false,
defaultConstraints:
i0.GeneratedColumn.constraintIsAlways('CHECK ("is_admin" IN (0, 1))'),
defaultValue: const i4.Constant(false));
defaultValue: const i3.Constant(false));
static const i0.VerificationMeta _emailMeta =
const i0.VerificationMeta('email');
@override
@ -276,7 +275,7 @@ class $UserEntityTable extends i3.UserEntity
i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i4.currentDateAndTime);
defaultValue: i3.currentDateAndTime);
static const i0.VerificationMeta _quotaSizeInBytesMeta =
const i0.VerificationMeta('quotaSizeInBytes');
@override
@ -290,7 +289,7 @@ class $UserEntityTable extends i3.UserEntity
i0.GeneratedColumn<int>('quota_usage_in_bytes', aliasedName, false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const i4.Constant(0));
defaultValue: const i3.Constant(0));
@override
List<i0.GeneratedColumn> get $columns => [
id,
@ -366,7 +365,7 @@ class $UserEntityTable extends i3.UserEntity
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.UserEntityData(
id: attachedDatabase.typeMapping
.read(i0.DriftSqlType.blob, data['${effectivePrefix}id'])!,
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
name: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
isAdmin: attachedDatabase.typeMapping
@ -397,7 +396,7 @@ class $UserEntityTable extends i3.UserEntity
class UserEntityData extends i0.DataClass
implements i0.Insertable<i1.UserEntityData> {
final i2.Uint8List id;
final String id;
final String name;
final bool isAdmin;
final String email;
@ -417,7 +416,7 @@ class UserEntityData extends i0.DataClass
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['id'] = i0.Variable<i2.Uint8List>(id);
map['id'] = i0.Variable<String>(id);
map['name'] = i0.Variable<String>(name);
map['is_admin'] = i0.Variable<bool>(isAdmin);
map['email'] = i0.Variable<String>(email);
@ -436,7 +435,7 @@ class UserEntityData extends i0.DataClass
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return UserEntityData(
id: serializer.fromJson<i2.Uint8List>(json['id']),
id: serializer.fromJson<String>(json['id']),
name: serializer.fromJson<String>(json['name']),
isAdmin: serializer.fromJson<bool>(json['isAdmin']),
email: serializer.fromJson<String>(json['email']),
@ -450,7 +449,7 @@ class UserEntityData extends i0.DataClass
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<i2.Uint8List>(id),
'id': serializer.toJson<String>(id),
'name': serializer.toJson<String>(name),
'isAdmin': serializer.toJson<bool>(isAdmin),
'email': serializer.toJson<String>(email),
@ -462,7 +461,7 @@ class UserEntityData extends i0.DataClass
}
i1.UserEntityData copyWith(
{i2.Uint8List? id,
{String? id,
String? name,
bool? isAdmin,
String? email,
@ -519,13 +518,13 @@ class UserEntityData extends i0.DataClass
}
@override
int get hashCode => Object.hash(i0.$driftBlobEquality.hash(id), name, isAdmin,
email, profileImagePath, updatedAt, quotaSizeInBytes, quotaUsageInBytes);
int get hashCode => Object.hash(id, name, isAdmin, email, profileImagePath,
updatedAt, quotaSizeInBytes, quotaUsageInBytes);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.UserEntityData &&
i0.$driftBlobEquality.equals(other.id, this.id) &&
other.id == this.id &&
other.name == this.name &&
other.isAdmin == this.isAdmin &&
other.email == this.email &&
@ -536,7 +535,7 @@ class UserEntityData extends i0.DataClass
}
class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
final i0.Value<i2.Uint8List> id;
final i0.Value<String> id;
final i0.Value<String> name;
final i0.Value<bool> isAdmin;
final i0.Value<String> email;
@ -555,7 +554,7 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
this.quotaUsageInBytes = const i0.Value.absent(),
});
UserEntityCompanion.insert({
required i2.Uint8List id,
required String id,
required String name,
this.isAdmin = const i0.Value.absent(),
required String email,
@ -567,7 +566,7 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
name = i0.Value(name),
email = i0.Value(email);
static i0.Insertable<i1.UserEntityData> custom({
i0.Expression<i2.Uint8List>? id,
i0.Expression<String>? id,
i0.Expression<String>? name,
i0.Expression<bool>? isAdmin,
i0.Expression<String>? email,
@ -589,7 +588,7 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
}
i1.UserEntityCompanion copyWith(
{i0.Value<i2.Uint8List>? id,
{i0.Value<String>? id,
i0.Value<String>? name,
i0.Value<bool>? isAdmin,
i0.Value<String>? email,
@ -613,7 +612,7 @@ class UserEntityCompanion extends i0.UpdateCompanion<i1.UserEntityData> {
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (id.present) {
map['id'] = i0.Variable<i2.Uint8List>(id.value);
map['id'] = i0.Variable<String>(id.value);
}
if (name.present) {
map['name'] = i0.Variable<String>(name.value);

View File

@ -6,8 +6,8 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class UserMetadataEntity extends Table with DriftDefaultsMixin {
const UserMetadataEntity();
BlobColumn get userId =>
blob().references(UserEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get userId =>
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
TextColumn get preferences => text().map(userPreferenceConverter)();
@override

View File

@ -3,23 +3,22 @@
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
as i1;
import 'dart:typed_data' as i2;
import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i3;
import 'package:immich_mobile/domain/models/user_metadata.model.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'
as i4;
as i3;
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
as i5;
import 'package:drift/internal/modular.dart' as i6;
as i4;
import 'package:drift/internal/modular.dart' as i5;
typedef $$UserMetadataEntityTableCreateCompanionBuilder
= i1.UserMetadataEntityCompanion Function({
required i2.Uint8List userId,
required i3.UserPreferences preferences,
required String userId,
required i2.UserPreferences preferences,
});
typedef $$UserMetadataEntityTableUpdateCompanionBuilder
= i1.UserMetadataEntityCompanion Function({
i0.Value<i2.Uint8List> userId,
i0.Value<i3.UserPreferences> preferences,
i0.Value<String> userId,
i0.Value<i2.UserPreferences> preferences,
});
final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
@ -29,26 +28,26 @@ final class $$UserMetadataEntityTableReferences extends i0.BaseReferences<
$$UserMetadataEntityTableReferences(
super.$_db, super.$_table, super.$_typedResult);
static i5.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
static i4.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) =>
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.createAlias(i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
i5.ReadDatabaseContainer(db)
.resultSet<i1.$UserMetadataEntityTable>(
'user_metadata_entity')
.userId,
i6.ReadDatabaseContainer(db)
.resultSet<i5.$UserEntityTable>('user_entity')
i5.ReadDatabaseContainer(db)
.resultSet<i4.$UserEntityTable>('user_entity')
.id));
i5.$$UserEntityTableProcessedTableManager get userId {
final $_column = $_itemColumn<i2.Uint8List>('user_id')!;
i4.$$UserEntityTableProcessedTableManager get userId {
final $_column = $_itemColumn<String>('user_id')!;
final manager = i5
final manager = i4
.$$UserEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer($_db)
.resultSet<i5.$UserEntityTable>('user_entity'))
i5.ReadDatabaseContainer($_db)
.resultSet<i4.$UserEntityTable>('user_entity'))
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_userIdTable($_db));
if (item == null) return manager;
@ -66,26 +65,26 @@ class $$UserMetadataEntityTableFilterComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnWithTypeConverterFilters<i3.UserPreferences, i3.UserPreferences,
i0.ColumnWithTypeConverterFilters<i2.UserPreferences, i2.UserPreferences,
String>
get preferences => $composableBuilder(
column: $table.preferences,
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i5.$$UserEntityTableFilterComposer get userId {
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
i4.$$UserEntityTableFilterComposer get userId {
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableFilterComposer(
i4.$$UserEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -108,20 +107,20 @@ class $$UserMetadataEntityTableOrderingComposer
column: $table.preferences,
builder: (column) => i0.ColumnOrderings(column));
i5.$$UserEntityTableOrderingComposer get userId {
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
i4.$$UserEntityTableOrderingComposer get userId {
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableOrderingComposer(
i4.$$UserEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -140,24 +139,24 @@ class $$UserMetadataEntityTableAnnotationComposer
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumnWithTypeConverter<i3.UserPreferences, String>
i0.GeneratedColumnWithTypeConverter<i2.UserPreferences, String>
get preferences => $composableBuilder(
column: $table.preferences, builder: (column) => column);
i5.$$UserEntityTableAnnotationComposer get userId {
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
i4.$$UserEntityTableAnnotationComposer get userId {
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.userId,
referencedTable: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
referencedTable: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
i5.$$UserEntityTableAnnotationComposer(
i4.$$UserEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer($db)
.resultSet<i5.$UserEntityTable>('user_entity'),
$table: i5.ReadDatabaseContainer($db)
.resultSet<i4.$UserEntityTable>('user_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
@ -193,16 +192,16 @@ class $$UserMetadataEntityTableTableManager extends i0.RootTableManager<
i1.$$UserMetadataEntityTableAnnotationComposer(
$db: db, $table: table),
updateCompanionCallback: ({
i0.Value<i2.Uint8List> userId = const i0.Value.absent(),
i0.Value<i3.UserPreferences> preferences = const i0.Value.absent(),
i0.Value<String> userId = const i0.Value.absent(),
i0.Value<i2.UserPreferences> preferences = const i0.Value.absent(),
}) =>
i1.UserMetadataEntityCompanion(
userId: userId,
preferences: preferences,
),
createCompanionCallback: ({
required i2.Uint8List userId,
required i3.UserPreferences preferences,
required String userId,
required i2.UserPreferences preferences,
}) =>
i1.UserMetadataEntityCompanion.insert(
userId: userId,
@ -267,7 +266,7 @@ typedef $$UserMetadataEntityTableProcessedTableManager
i1.UserMetadataEntityData,
i0.PrefetchHooks Function({bool userId})>;
class $UserMetadataEntityTable extends i4.UserMetadataEntity
class $UserMetadataEntityTable extends i3.UserMetadataEntity
with i0.TableInfo<$UserMetadataEntityTable, i1.UserMetadataEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
@ -276,18 +275,18 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity
static const i0.VerificationMeta _userIdMeta =
const i0.VerificationMeta('userId');
@override
late final i0.GeneratedColumn<i2.Uint8List> userId =
i0.GeneratedColumn<i2.Uint8List>('user_id', aliasedName, false,
type: i0.DriftSqlType.blob,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
late final i0.GeneratedColumn<String> userId = i0.GeneratedColumn<String>(
'user_id', aliasedName, false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES user_entity (id) ON DELETE CASCADE'));
@override
late final i0.GeneratedColumnWithTypeConverter<i3.UserPreferences, String>
late final i0.GeneratedColumnWithTypeConverter<i2.UserPreferences, String>
preferences = i0.GeneratedColumn<String>(
'preferences', aliasedName, false,
type: i0.DriftSqlType.string, requiredDuringInsert: true)
.withConverter<i3.UserPreferences>(
.withConverter<i2.UserPreferences>(
i1.$UserMetadataEntityTable.$converterpreferences);
@override
List<i0.GeneratedColumn> get $columns => [userId, preferences];
@ -319,7 +318,7 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.UserMetadataEntityData(
userId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.blob, data['${effectivePrefix}user_id'])!,
.read(i0.DriftSqlType.string, data['${effectivePrefix}user_id'])!,
preferences: i1.$UserMetadataEntityTable.$converterpreferences.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, data['${effectivePrefix}preferences'])!),
@ -331,8 +330,8 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity
return $UserMetadataEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i3.UserPreferences, String, Object?>
$converterpreferences = i4.userPreferenceConverter;
static i0.JsonTypeConverter2<i2.UserPreferences, String, Object?>
$converterpreferences = i3.userPreferenceConverter;
@override
bool get withoutRowId => true;
@override
@ -341,14 +340,14 @@ class $UserMetadataEntityTable extends i4.UserMetadataEntity
class UserMetadataEntityData extends i0.DataClass
implements i0.Insertable<i1.UserMetadataEntityData> {
final i2.Uint8List userId;
final i3.UserPreferences preferences;
final String userId;
final i2.UserPreferences preferences;
const UserMetadataEntityData(
{required this.userId, required this.preferences});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['user_id'] = i0.Variable<i2.Uint8List>(userId);
map['user_id'] = i0.Variable<String>(userId);
{
map['preferences'] = i0.Variable<String>(
i1.$UserMetadataEntityTable.$converterpreferences.toSql(preferences));
@ -360,7 +359,7 @@ class UserMetadataEntityData extends i0.DataClass
{i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return UserMetadataEntityData(
userId: serializer.fromJson<i2.Uint8List>(json['userId']),
userId: serializer.fromJson<String>(json['userId']),
preferences: i1.$UserMetadataEntityTable.$converterpreferences
.fromJson(serializer.fromJson<Object?>(json['preferences'])),
);
@ -369,7 +368,7 @@ class UserMetadataEntityData extends i0.DataClass
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'userId': serializer.toJson<i2.Uint8List>(userId),
'userId': serializer.toJson<String>(userId),
'preferences': serializer.toJson<Object?>(i1
.$UserMetadataEntityTable.$converterpreferences
.toJson(preferences)),
@ -377,7 +376,7 @@ class UserMetadataEntityData extends i0.DataClass
}
i1.UserMetadataEntityData copyWith(
{i2.Uint8List? userId, i3.UserPreferences? preferences}) =>
{String? userId, i2.UserPreferences? preferences}) =>
i1.UserMetadataEntityData(
userId: userId ?? this.userId,
preferences: preferences ?? this.preferences,
@ -401,31 +400,30 @@ class UserMetadataEntityData extends i0.DataClass
}
@override
int get hashCode =>
Object.hash(i0.$driftBlobEquality.hash(userId), preferences);
int get hashCode => Object.hash(userId, preferences);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.UserMetadataEntityData &&
i0.$driftBlobEquality.equals(other.userId, this.userId) &&
other.userId == this.userId &&
other.preferences == this.preferences);
}
class UserMetadataEntityCompanion
extends i0.UpdateCompanion<i1.UserMetadataEntityData> {
final i0.Value<i2.Uint8List> userId;
final i0.Value<i3.UserPreferences> preferences;
final i0.Value<String> userId;
final i0.Value<i2.UserPreferences> preferences;
const UserMetadataEntityCompanion({
this.userId = const i0.Value.absent(),
this.preferences = const i0.Value.absent(),
});
UserMetadataEntityCompanion.insert({
required i2.Uint8List userId,
required i3.UserPreferences preferences,
required String userId,
required i2.UserPreferences preferences,
}) : userId = i0.Value(userId),
preferences = i0.Value(preferences);
static i0.Insertable<i1.UserMetadataEntityData> custom({
i0.Expression<i2.Uint8List>? userId,
i0.Expression<String>? userId,
i0.Expression<String>? preferences,
}) {
return i0.RawValuesInsertable({
@ -435,8 +433,7 @@ class UserMetadataEntityCompanion
}
i1.UserMetadataEntityCompanion copyWith(
{i0.Value<i2.Uint8List>? userId,
i0.Value<i3.UserPreferences>? preferences}) {
{i0.Value<String>? userId, i0.Value<i2.UserPreferences>? preferences}) {
return i1.UserMetadataEntityCompanion(
userId: userId ?? this.userId,
preferences: preferences ?? this.preferences,
@ -447,7 +444,7 @@ class UserMetadataEntityCompanion
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (userId.present) {
map['user_id'] = i0.Variable<i2.Uint8List>(userId.value);
map['user_id'] = i0.Variable<String>(userId.value);
}
if (preferences.present) {
map['preferences'] = i0.Variable<String>(i1

View File

@ -3,7 +3,12 @@ import 'dart:async';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
import 'package:isar/isar.dart';
@ -25,7 +30,18 @@ class IsarDatabaseRepository implements IDatabaseRepository {
Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback();
}
@DriftDatabase(tables: [UserEntity, UserMetadataEntity, PartnerEntity])
@DriftDatabase(
tables: [
UserEntity,
UserMetadataEntity,
PartnerEntity,
LocalAlbumEntity,
LocalAssetEntity,
LocalAlbumAssetEntity,
RemoteAssetEntity,
RemoteExifEntity,
],
)
class Drift extends $Drift implements IDatabaseRepository {
Drift([QueryExecutor? executor])
: super(
@ -42,8 +58,9 @@ class Drift extends $Drift implements IDatabaseRepository {
@override
MigrationStrategy get migration => MigrationStrategy(
beforeOpen: (details) async {
await customStatement('PRAGMA journal_mode = WAL');
await customStatement('PRAGMA foreign_keys = ON');
await customStatement('PRAGMA synchronous = NORMAL');
await customStatement('PRAGMA journal_mode = WAL');
},
);
}

View File

@ -7,6 +7,16 @@ import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift
as i2;
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
as i3;
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
as i4;
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
as i5;
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
as i6;
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
as i7;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
as i8;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
@ -16,12 +26,32 @@ abstract class $Drift extends i0.GeneratedDatabase {
i2.$UserMetadataEntityTable(this);
late final i3.$PartnerEntityTable partnerEntity =
i3.$PartnerEntityTable(this);
late final i4.$LocalAlbumEntityTable localAlbumEntity =
i4.$LocalAlbumEntityTable(this);
late final i5.$LocalAssetEntityTable localAssetEntity =
i5.$LocalAssetEntityTable(this);
late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
i6.$LocalAlbumAssetEntityTable(this);
late final i7.$RemoteAssetEntityTable remoteAssetEntity =
i7.$RemoteAssetEntityTable(this);
late final i8.$RemoteExifEntityTable remoteExifEntity =
i8.$RemoteExifEntityTable(this);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@override
List<i0.DatabaseSchemaEntity> get allSchemaEntities =>
[userEntity, userMetadataEntity, partnerEntity];
List<i0.DatabaseSchemaEntity> get allSchemaEntities => [
userEntity,
userMetadataEntity,
partnerEntity,
localAlbumEntity,
localAssetEntity,
localAlbumAssetEntity,
remoteAssetEntity,
remoteExifEntity,
i5.idxLocalAssetChecksum,
i7.uQRemoteAssetOwnerChecksum
];
@override
i0.StreamQueryUpdateRules get streamUpdateRules =>
const i0.StreamQueryUpdateRules(
@ -48,6 +78,36 @@ abstract class $Drift extends i0.GeneratedDatabase {
i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('local_asset_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('local_album_asset_entity',
kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('local_album_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('local_album_asset_entity',
kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('user_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('remote_asset_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete),
],
),
],
);
@override
@ -64,4 +124,14 @@ class $DriftManager {
i2.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
i3.$$PartnerEntityTableTableManager get partnerEntity =>
i3.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
i4.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
i4.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
i5.$$LocalAssetEntityTableTableManager get localAssetEntity =>
i5.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
i7.$$RemoteAssetEntityTableTableManager get remoteAssetEntity =>
i7.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity);
i8.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
}

View File

@ -0,0 +1,382 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:platform/platform.dart';
class DriftLocalAlbumRepository extends DriftDatabaseRepository
implements ILocalAlbumRepository {
final Drift _db;
final Platform _platform;
const DriftLocalAlbumRepository(this._db, {Platform? platform})
: _platform = platform ?? const LocalPlatform(),
super(_db);
@override
Future<List<LocalAlbum>> getAll({SortLocalAlbumsBy? sortBy}) {
final assetCount = _db.localAlbumAssetEntity.assetId.count();
final query = _db.localAlbumEntity.select().join([
leftOuterJoin(
_db.localAlbumAssetEntity,
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
useColumns: false,
),
]);
query
..addColumns([assetCount])
..groupBy([_db.localAlbumEntity.id]);
if (sortBy == SortLocalAlbumsBy.id) {
query.orderBy([OrderingTerm.asc(_db.localAlbumEntity.id)]);
}
return query
.map(
(row) => row
.readTable(_db.localAlbumEntity)
.toDto(assetCount: row.read(assetCount) ?? 0),
)
.get();
}
@override
Future<void> delete(String albumId) => transaction(() async {
// Remove all assets that are only in this particular album
// We cannot remove all assets in the album because they might be in other albums in iOS
// That is not the case on Android since asset <-> album has one:one mapping
final assetsToDelete = _platform.isIOS
? await _getUniqueAssetsInAlbum(albumId)
: await getAssetIdsForAlbum(albumId);
await _deleteAssets(assetsToDelete);
// All the other assets that are still associated will be unlinked automatically on-cascade
await _db.managers.localAlbumEntity
.filter((a) => a.id.equals(albumId))
.delete();
});
@override
Future<void> syncAlbumDeletes(
String albumId,
Iterable<String> assetIdsToKeep,
) async {
if (assetIdsToKeep.isEmpty) {
return Future.value();
}
final deleteSmt = _db.localAssetEntity.delete();
deleteSmt.where((localAsset) {
final subQuery = _db.localAlbumAssetEntity.selectOnly()
..addColumns([_db.localAlbumAssetEntity.assetId])
..join([
innerJoin(
_db.localAlbumEntity,
_db.localAlbumAssetEntity.albumId
.equalsExp(_db.localAlbumEntity.id),
),
]);
subQuery.where(
_db.localAlbumEntity.id.equals(albumId) &
_db.localAlbumAssetEntity.assetId.isNotIn(assetIdsToKeep),
);
return localAsset.id.isInQuery(subQuery);
});
await deleteSmt.go();
}
@override
Future<void> upsert(
LocalAlbum localAlbum, {
Iterable<LocalAsset> toUpsert = const [],
Iterable<String> toDelete = const [],
}) {
final companion = LocalAlbumEntityCompanion.insert(
id: localAlbum.id,
name: localAlbum.name,
updatedAt: Value(localAlbum.updatedAt),
backupSelection: localAlbum.backupSelection,
);
return _db.transaction(() async {
await _db.localAlbumEntity
.insertOne(companion, onConflict: DoUpdate((_) => companion));
await _addAssets(localAlbum.id, toUpsert);
await _removeAssets(localAlbum.id, toDelete);
});
}
@override
Future<void> updateAll(Iterable<LocalAlbum> albums) {
return _db.transaction(() async {
await _db.localAlbumEntity
.update()
.write(const LocalAlbumEntityCompanion(marker_: Value(true)));
await _db.batch((batch) {
for (final album in albums) {
final companion = LocalAlbumEntityCompanion.insert(
id: album.id,
name: album.name,
updatedAt: Value(album.updatedAt),
backupSelection: album.backupSelection,
marker_: const Value(null),
);
batch.insert(
_db.localAlbumEntity,
companion,
onConflict: DoUpdate((_) => companion),
);
}
});
if (_platform.isAndroid) {
// On Android, an asset can only be in one album
// So, get the albums that are marked for deletion
// and delete all the assets that are in those albums
final deleteSmt = _db.localAssetEntity.delete();
deleteSmt.where((localAsset) {
final subQuery = _db.localAlbumAssetEntity.selectOnly()
..addColumns([_db.localAlbumAssetEntity.assetId])
..join([
innerJoin(
_db.localAlbumEntity,
_db.localAlbumAssetEntity.albumId
.equalsExp(_db.localAlbumEntity.id),
),
]);
subQuery.where(_db.localAlbumEntity.marker_.isNotNull());
return localAsset.id.isInQuery(subQuery);
});
await deleteSmt.go();
}
await _db.localAlbumEntity.deleteWhere((f) => f.marker_.isNotNull());
});
}
@override
Future<List<LocalAsset>> getAssetsForAlbum(String albumId) {
final query = _db.localAlbumAssetEntity.select().join(
[
innerJoin(
_db.localAssetEntity,
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
),
],
)
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]);
return query
.map((row) => row.readTable(_db.localAssetEntity).toDto())
.get();
}
@override
Future<List<String>> getAssetIdsForAlbum(String albumId) {
final query = _db.localAlbumAssetEntity.selectOnly()
..addColumns([_db.localAlbumAssetEntity.assetId])
..where(_db.localAlbumAssetEntity.albumId.equals(albumId));
return query
.map((row) => row.read(_db.localAlbumAssetEntity.assetId)!)
.get();
}
@override
Future<void> processDelta({
required List<LocalAsset> updates,
required List<String> deletes,
required Map<String, List<String>> assetAlbums,
}) {
return _db.transaction(() async {
await _deleteAssets(deletes);
await _upsertAssets(updates);
// The ugly casting below is required for now because the generated code
// casts the returned values from the platform during decoding them
// and iterating over them causes the type to be List<Object?> instead of
// List<String>
await _db.batch((batch) async {
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
batch.deleteWhere(
_db.localAlbumAssetEntity,
(f) =>
f.albumId.isNotIn(albumIds.cast<String?>().nonNulls) &
f.assetId.equals(assetId),
);
});
});
await _db.batch((batch) async {
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
batch.insertAll(
_db.localAlbumAssetEntity,
albumIds.cast<String?>().nonNulls.map(
(albumId) => LocalAlbumAssetEntityCompanion.insert(
assetId: assetId,
albumId: albumId,
),
),
onConflict: DoNothing(),
);
});
});
});
}
Future<void> _addAssets(String albumId, Iterable<LocalAsset> assets) {
if (assets.isEmpty) {
return Future.value();
}
return transaction(() async {
await _upsertAssets(assets);
await _db.localAlbumAssetEntity.insertAll(
assets.map(
(a) => LocalAlbumAssetEntityCompanion.insert(
assetId: a.id,
albumId: albumId,
),
),
mode: InsertMode.insertOrIgnore,
);
});
}
Future<void> _removeAssets(String albumId, Iterable<String> assetIds) async {
if (assetIds.isEmpty) {
return Future.value();
}
if (_platform.isAndroid) {
return _deleteAssets(assetIds);
}
List<String> assetsToDelete = [];
List<String> assetsToUnLink = [];
final uniqueAssets = await _getUniqueAssetsInAlbum(albumId);
if (uniqueAssets.isEmpty) {
assetsToUnLink = assetIds.toList();
} else {
// Delete unique assets and unlink others
final uniqueSet = uniqueAssets.toSet();
for (final assetId in assetIds) {
if (uniqueSet.contains(assetId)) {
assetsToDelete.add(assetId);
} else {
assetsToUnLink.add(assetId);
}
}
}
return transaction(() async {
if (assetsToUnLink.isNotEmpty) {
await _db.batch(
(batch) => batch.deleteWhere(
_db.localAlbumAssetEntity,
(f) => f.assetId.isIn(assetsToUnLink) & f.albumId.equals(albumId),
),
);
}
await _deleteAssets(assetsToDelete);
});
}
/// Get all asset ids that are only in this album and not in other albums.
/// This is useful in cases where the album is a smart album or a user-created album, especially on iOS
Future<List<String>> _getUniqueAssetsInAlbum(String albumId) {
final assetId = _db.localAlbumAssetEntity.assetId;
final query = _db.localAlbumAssetEntity.selectOnly()
..addColumns([assetId])
..groupBy(
[assetId],
having: _db.localAlbumAssetEntity.albumId.count().equals(1) &
_db.localAlbumAssetEntity.albumId.equals(albumId),
);
return query.map((row) => row.read(assetId)!).get();
}
Future<void> _upsertAssets(Iterable<LocalAsset> localAssets) {
if (localAssets.isEmpty) {
return Future.value();
}
return _db.batch((batch) async {
batch.insertAllOnConflictUpdate(
_db.localAssetEntity,
localAssets.map(
(a) => LocalAssetEntityCompanion.insert(
name: a.name,
type: a.type,
createdAt: Value(a.createdAt),
updatedAt: Value(a.updatedAt),
durationInSeconds: Value.absentIfNull(a.durationInSeconds),
id: a.id,
checksum: Value.absentIfNull(a.checksum),
),
),
);
});
}
Future<void> _deleteAssets(Iterable<String> ids) {
if (ids.isEmpty) {
return Future.value();
}
return _db.batch(
(batch) => batch.deleteWhere(
_db.localAssetEntity,
(f) => f.id.isIn(ids),
),
);
}
@override
Future<void> update(LocalAlbum album) {
return (_db.update(_db.localAlbumEntity)
..where(
(a) => a.id.equals(album.id),
))
.write(
LocalAlbumEntityCompanion.insert(
id: album.id,
name: album.name,
updatedAt: Value(album.updatedAt),
backupSelection: album.backupSelection,
),
);
}
}
extension on LocalAlbumEntityData {
LocalAlbum toDto({int assetCount = 0}) {
return LocalAlbum(
id: id,
name: name,
updatedAt: updatedAt,
assetCount: assetCount,
backupSelection: backupSelection,
);
}
}
extension on LocalAssetEntityData {
LocalAsset toDto() {
return LocalAsset(
id: id,
name: name,
checksum: checksum,
type: type,
createdAt: createdAt,
updatedAt: updatedAt,
durationInSeconds: durationInSeconds,
isFavorite: isFavorite,
);
}
}

View File

@ -5,6 +5,7 @@ import 'package:http/http.dart' as http;
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@ -105,6 +106,7 @@ class SyncApiRepository implements ISyncApiRepository {
stopwatch.stop();
_logger
.info("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms");
DLog.log("Remote Sync completed in ${stopwatch.elapsed.inMilliseconds}ms");
}
List<SyncEvent> _parseLines(List<String> lines) {

View File

@ -1,12 +1,13 @@
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:openapi/api.dart' hide AssetVisibility;
class DriftSyncStreamRepository extends DriftDatabaseRepository
implements ISyncStreamRepository {
@ -22,7 +23,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository
for (final user in data) {
batch.delete(
_db.userEntity,
UserEntityCompanion(id: Value(user.userId.toUuidByte())),
UserEntityCompanion(id: Value(user.userId)),
);
}
});
@ -44,7 +45,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository
batch.insert(
_db.userEntity,
companion.copyWith(id: Value(user.id.toUuidByte())),
companion.copyWith(id: Value(user.id)),
onConflict: DoUpdate((_) => companion),
);
}
@ -63,8 +64,8 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository
batch.delete(
_db.partnerEntity,
PartnerEntityCompanion(
sharedById: Value(partner.sharedById.toUuidByte()),
sharedWithId: Value(partner.sharedWithId.toUuidByte()),
sharedById: Value(partner.sharedById),
sharedWithId: Value(partner.sharedWithId),
),
);
}
@ -86,8 +87,8 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository
batch.insert(
_db.partnerEntity,
companion.copyWith(
sharedById: Value(partner.sharedById.toUuidByte()),
sharedWithId: Value(partner.sharedWithId.toUuidByte()),
sharedById: Value(partner.sharedById),
sharedWithId: Value(partner.sharedWithId),
),
onConflict: DoUpdate((_) => companion),
);
@ -99,36 +100,153 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository
}
}
// Assets
@override
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data) async {
debugPrint("updateAssetsV1 - ${data.length}");
}
@override
Future<void> deleteAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
debugPrint("deleteAssetsV1 - ${data.length}");
try {
await _deleteAssetsV1(data);
} catch (e, s) {
_logger.severe('Error while processing deleteAssetsV1', e, s);
rethrow;
}
}
// Partner Assets
@override
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data) async {
debugPrint("updatePartnerAssetsV1 - ${data.length}");
Future<void> updateAssetsV1(Iterable<SyncAssetV1> data) async {
try {
await _updateAssetsV1(data);
} catch (e, s) {
_logger.severe('Error while processing updateAssetsV1', e, s);
rethrow;
}
}
@override
Future<void> deletePartnerAssetsV1(Iterable<SyncAssetDeleteV1> data) async {
debugPrint("deletePartnerAssetsV1 - ${data.length}");
try {
await _deleteAssetsV1(data);
} catch (e, s) {
_logger.severe('Error while processing deletePartnerAssetsV1', e, s);
rethrow;
}
}
@override
Future<void> updatePartnerAssetsV1(Iterable<SyncAssetV1> data) async {
try {
await _updateAssetsV1(data);
} catch (e, s) {
_logger.severe('Error while processing updatePartnerAssetsV1', e, s);
rethrow;
}
}
// EXIF
@override
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
debugPrint("updateAssetsExifV1 - ${data.length}");
try {
await _updateAssetExifV1(data);
} catch (e, s) {
_logger.severe('Error while processing updateAssetsExifV1', e, s);
rethrow;
}
}
@override
Future<void> updatePartnerAssetsExifV1(Iterable<SyncAssetExifV1> data) async {
debugPrint("updatePartnerAssetsExifV1 - ${data.length}");
try {
await _updateAssetExifV1(data);
} catch (e, s) {
_logger.severe('Error while processing updatePartnerAssetsExifV1', e, s);
rethrow;
}
}
Future<void> _updateAssetsV1(Iterable<SyncAssetV1> data) =>
_db.batch((batch) {
for (final asset in data) {
final companion = RemoteAssetEntityCompanion(
name: Value(asset.originalFileName),
type: Value(asset.type.toAssetType()),
createdAt: Value.absentIfNull(asset.fileCreatedAt),
updatedAt: Value.absentIfNull(asset.fileModifiedAt),
durationInSeconds: const Value(0),
checksum: Value(asset.checksum),
isFavorite: Value(asset.isFavorite),
ownerId: Value(asset.ownerId),
localDateTime: Value(asset.localDateTime),
thumbHash: Value(asset.thumbhash),
deletedAt: Value(asset.deletedAt),
visibility: Value(asset.visibility.toAssetVisibility()),
);
batch.insert(
_db.remoteAssetEntity,
companion.copyWith(id: Value(asset.id)),
onConflict: DoUpdate((_) => companion),
);
}
});
Future<void> _deleteAssetsV1(Iterable<SyncAssetDeleteV1> assets) =>
_db.batch((batch) {
for (final asset in assets) {
batch.delete(
_db.remoteAssetEntity,
RemoteAssetEntityCompanion(id: Value(asset.assetId)),
);
}
});
Future<void> _updateAssetExifV1(Iterable<SyncAssetExifV1> data) =>
_db.batch((batch) {
for (final exif in data) {
final companion = RemoteExifEntityCompanion(
city: Value(exif.city),
state: Value(exif.state),
country: Value(exif.country),
dateTimeOriginal: Value(exif.dateTimeOriginal),
description: Value(exif.description),
height: Value(exif.exifImageHeight),
width: Value(exif.exifImageWidth),
exposureTime: Value(exif.exposureTime),
fNumber: Value(exif.fNumber),
fileSize: Value(exif.fileSizeInByte),
focalLength: Value(exif.focalLength),
latitude: Value(exif.latitude),
longitude: Value(exif.longitude),
iso: Value(exif.iso),
make: Value(exif.make),
model: Value(exif.model),
orientation: Value(exif.orientation),
timeZone: Value(exif.timeZone),
rating: Value(exif.rating),
projectionType: Value(exif.projectionType),
);
batch.insert(
_db.remoteExifEntity,
companion.copyWith(assetId: Value(exif.assetId)),
onConflict: DoUpdate((_) => companion),
);
}
});
}
extension on SyncAssetV1TypeEnum {
AssetType toAssetType() => switch (this) {
SyncAssetV1TypeEnum.IMAGE => AssetType.image,
SyncAssetV1TypeEnum.VIDEO => AssetType.video,
SyncAssetV1TypeEnum.AUDIO => AssetType.audio,
SyncAssetV1TypeEnum.OTHER => AssetType.other,
_ => throw Exception('Unknown SyncAssetV1TypeEnum value: $this'),
};
}
extension on SyncAssetV1VisibilityEnum {
AssetVisibility toAssetVisibility() => switch (this) {
SyncAssetV1VisibilityEnum.timeline => AssetVisibility.timeline,
SyncAssetV1VisibilityEnum.hidden => AssetVisibility.hidden,
SyncAssetV1VisibilityEnum.archive => AssetVisibility.archive,
SyncAssetV1VisibilityEnum.locked => AssetVisibility.locked,
_ => throw Exception('Unknown SyncAssetV1VisibilityEnum value: $this'),
};
}

View File

@ -0,0 +1,10 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
mixin AssetEntityMixin on Table {
TextColumn get name => text()();
IntColumn get type => intEnum<AssetType>()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
IntColumn get durationInSeconds => integer().nullable()();
}

View File

@ -14,7 +14,7 @@ abstract class ITimelineRepository {
Album album,
GroupAssetsBy groupAssetsBy,
);
Stream<RenderList> watchAllVideosTimeline();
Stream<RenderList> watchAllVideosTimeline(String userId);
Stream<RenderList> watchHomeTimeline(
String userId,

View File

@ -89,18 +89,6 @@ Future<void> initApp() async {
initializeTimeZones();
FileDownloader().configureNotification(
running: TaskNotification(
'downloading_media'.tr(),
'file: {filename}',
),
complete: TaskNotification(
'download_finished'.tr(),
'file: {filename}',
),
progressBar: true,
);
await FileDownloader().trackTasksInGroup(
downloadGroupLivePhoto,
markDownloadedComplete: false,
@ -167,10 +155,27 @@ class ImmichAppState extends ConsumerState<ImmichApp>
await ref.read(localNotificationService).setup();
}
void _configureFileDownloaderNotifications() {
FileDownloader().configureNotification(
running: TaskNotification(
'downloading_media'.tr(),
'${'file_name'.tr()}: {filename}',
),
complete: TaskNotification(
'download_finished'.tr(),
'${'file_name'.tr()}: {filename}',
),
progressBar: true,
);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
Intl.defaultLocale = context.locale.toLanguageTag();
WidgetsBinding.instance.addPostFrameCallback((_) {
_configureFileDownloaderNotifications();
});
}
@override

View File

@ -14,6 +14,7 @@ import 'package:immich_mobile/pages/album/album_shared_user_icons.dart';
import 'package:immich_mobile/pages/album/album_title.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/timeline.provider.dart';
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
import 'package:immich_mobile/providers/multiselect.provider.dart';
@ -93,6 +94,7 @@ class AlbumViewer extends HookConsumerWidget {
onActivitiesPressed() {
if (album.remoteId != null) {
ref.read(currentAssetProvider.notifier).set(null);
context.pushRoute(
const ActivitiesRoute(),
);

View File

@ -14,6 +14,7 @@ import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/translation.dart';
import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart';
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
@ -229,13 +230,11 @@ class AlbumsPage extends HookConsumerWidget {
),
subtitle: sorted[index].ownerId != null
? Text(
'${(sorted[index].assetCount == 1 ? 'album_thumbnail_card_item'.tr() : 'album_thumbnail_card_items'.tr(
namedArgs: {
'count': sorted[index]
.assetCount
.toString(),
},
))} ${sorted[index].ownerId != userId ? 'album_thumbnail_shared_by'.tr(namedArgs: {'user': sorted[index].ownerName!}) : 'owned'.tr()}',
'${t('items_count', {
'count': sorted[index].assetCount,
})} ${sorted[index].ownerId != userId ? t('shared_by_user', {
'user': sorted[index].ownerName!,
}) : 'owned'.tr()}',
overflow: TextOverflow.ellipsis,
style:
context.textTheme.bodyMedium?.copyWith(

View File

@ -3,9 +3,11 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/widgets/backup/album_info_card.dart';
@ -17,12 +19,17 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
const BackupAlbumSelectionPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
final albums = ref.watch(backupAlbumProvider);
final selectedBackupAlbums = albums
.where((album) => album.backupSelection == BackupSelection.selected)
.toList();
final excludedBackupAlbums = albums
.where((album) => album.backupSelection == BackupSelection.excluded)
.toList();
final enableSyncUploadAlbum =
useAppSettingsState(AppSettingsEnum.syncAlbums);
final isDarkTheme = context.isDarkTheme;
final albums = ref.watch(backupProvider).availableAlbums;
useEffect(
() {
@ -85,8 +92,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
buildSelectedAlbumNameChip() {
return selectedBackupAlbums.map((album) {
void removeSelection() =>
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
void removeSelection() {
ref.read(backupAlbumProvider.notifier).deselectAlbum(album);
}
return Padding(
padding: const EdgeInsets.only(right: 8.0),
@ -117,9 +125,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
buildExcludedAlbumNameChip() {
return excludedBackupAlbums.map((album) {
void removeSelection() {
ref
.watch(backupProvider.notifier)
.removeExcludedAlbumForBackup(album);
ref.read(backupAlbumProvider.notifier).deselectAlbum(album);
}
return GestureDetector(
@ -168,129 +174,131 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
).tr(),
elevation: 0,
),
body: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
child: Text(
"backup_album_selection_page_selection_info",
style: context.textTheme.titleSmall,
).tr(),
),
// Selected Album Chips
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Wrap(
children: [
...buildSelectedAlbumNameChip(),
...buildExcludedAlbumNameChip(),
],
),
),
SettingsSwitchListTile(
valueNotifier: enableSyncUploadAlbum,
title: "sync_albums".tr(),
subtitle: "sync_upload_album_setting_subtitle".tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
titleStyle: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
subtitleStyle: context.textTheme.labelLarge?.copyWith(
color: context.colorScheme.primary,
),
onChanged: handleSyncAlbumToggle,
),
ListTile(
title: Text(
"backup_album_selection_page_albums_device".tr(
namedArgs: {
'count': ref
.watch(backupProvider)
.availableAlbums
.length
.toString(),
},
body: SafeArea(
child: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
style: context.textTheme.titleSmall,
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
"backup_album_selection_page_albums_tap",
style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
),
"backup_album_selection_page_selection_info",
style: context.textTheme.titleSmall,
).tr(),
),
trailing: IconButton(
splashRadius: 16,
icon: Icon(
Icons.info,
size: 20,
color: context.primaryColor,
),
onPressed: () {
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 5,
title: Text(
'backup_album_selection_page_selection_info',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: context.primaryColor,
),
).tr(),
content: SingleChildScrollView(
child: ListBody(
children: [
const Text(
'backup_album_selection_page_assets_scatter',
style: TextStyle(
fontSize: 14,
),
).tr(),
],
),
),
);
},
);
},
),
),
// Selected Album Chips
// buildSearchBar(),
],
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Wrap(
children: [
...buildSelectedAlbumNameChip(),
...buildExcludedAlbumNameChip(),
],
),
),
SettingsSwitchListTile(
valueNotifier: enableSyncUploadAlbum,
title: "sync_albums".tr(),
subtitle: "sync_upload_album_setting_subtitle".tr(),
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
titleStyle: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
subtitleStyle: context.textTheme.labelLarge?.copyWith(
color: context.colorScheme.primary,
),
onChanged: handleSyncAlbumToggle,
),
ListTile(
title: Text(
"backup_album_selection_page_albums_device".tr(
namedArgs: {
'count': ref
.watch(backupProvider)
.availableAlbums
.length
.toString(),
},
),
style: context.textTheme.titleSmall,
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
"backup_album_selection_page_albums_tap",
style: context.textTheme.labelLarge?.copyWith(
color: context.primaryColor,
),
).tr(),
),
trailing: IconButton(
splashRadius: 16,
icon: Icon(
Icons.info,
size: 20,
color: context.primaryColor,
),
onPressed: () {
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
elevation: 5,
title: Text(
'backup_album_selection_page_selection_info',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: context.primaryColor,
),
).tr(),
content: SingleChildScrollView(
child: ListBody(
children: [
const Text(
'backup_album_selection_page_assets_scatter',
style: TextStyle(
fontSize: 14,
),
).tr(),
],
),
),
);
},
);
},
),
),
// buildSearchBar(),
],
),
),
),
SliverLayoutBuilder(
builder: (context, constraints) {
if (constraints.crossAxisExtent > 600) {
return buildAlbumSelectionGrid();
} else {
return buildAlbumSelectionList();
}
},
),
],
SliverLayoutBuilder(
builder: (context, constraints) {
if (constraints.crossAxisExtent > 600) {
return buildAlbumSelectionGrid();
} else {
return buildAlbumSelectionList();
}
},
),
],
),
),
);
}

View File

@ -6,11 +6,13 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
@ -86,8 +88,13 @@ class BackupControllerPage extends HookConsumerWidget {
);
Widget buildSelectedAlbumName() {
var text = "backup_controller_page_backup_selected".tr();
var albums = ref.watch(backupProvider).selectedBackupAlbums;
String text = "backup_controller_page_backup_selected".tr();
final albums = ref
.watch(backupAlbumProvider)
.where(
(album) => album.backupSelection == BackupSelection.selected,
)
.toList();
if (albums.isNotEmpty) {
for (var album in albums) {
@ -121,8 +128,13 @@ class BackupControllerPage extends HookConsumerWidget {
}
Widget buildExcludedAlbumName() {
var text = "backup_controller_page_excluded".tr();
var albums = ref.watch(backupProvider).excludedBackupAlbums;
String text = "backup_controller_page_excluded".tr();
final albums = ref
.watch(backupAlbumProvider)
.where(
(album) => album.backupSelection == BackupSelection.excluded,
)
.toList();
if (albums.isNotEmpty) {
for (var album in albums) {

View File

@ -223,7 +223,8 @@ class GalleryViewerPage extends HookConsumerWidget {
heroAttributes: _getHeroAttributes(asset),
filterQuality: FilterQuality.high,
tightMode: true,
minScale: PhotoViewComputedScale.contained,
initialScale: PhotoViewComputedScale.contained * 0.99,
minScale: PhotoViewComputedScale.contained * 0.99,
errorBuilder: (context, error, stackTrace) => ImmichImage(
asset,
fit: BoxFit.contain,
@ -238,9 +239,9 @@ class GalleryViewerPage extends HookConsumerWidget {
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
heroAttributes: _getHeroAttributes(asset),
filterQuality: FilterQuality.high,
initialScale: 1.0,
initialScale: PhotoViewComputedScale.contained * 0.99,
maxScale: 1.0,
minScale: 1.0,
minScale: PhotoViewComputedScale.contained * 0.99,
basePosition: Alignment.center,
child: SizedBox(
width: context.width,

View File

@ -30,7 +30,7 @@ enum SettingSection {
"backup_setting_subtitle",
),
languages(
'setting_languages_title',
'language',
Icons.language,
"setting_languages_subtitle",
),

View File

@ -9,6 +9,7 @@ import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/models/folder/recursive_folder.model.dart';
import 'package:immich_mobile/models/folder/root_folder.model.dart';
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/folder.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
@ -219,12 +220,15 @@ class FolderContent extends HookConsumerWidget {
list.allAssets!.isNotEmpty)
...list.allAssets!.map(
(asset) => LargeLeadingTile(
onTap: () => context.pushRoute(
GalleryViewerRoute(
renderList: list,
initialIndex: list.allAssets!.indexOf(asset),
),
),
onTap: () {
ref.read(currentAssetProvider.notifier).set(asset);
context.pushRoute(
GalleryViewerRoute(
renderList: list,
initialIndex: list.allAssets!.indexOf(asset),
),
);
},
leading: ClipRRect(
borderRadius: const BorderRadius.all(
Radius.circular(15),

View File

@ -133,7 +133,7 @@ class PeopleCollectionPage extends HookConsumerWidget {
);
},
error: (error, stack) => const Text("error"),
loading: () => const CircularProgressIndicator(),
loading: () => const Center(child: CircularProgressIndicator()),
),
);
},

View File

@ -395,6 +395,7 @@ class _MapWithMarker extends StatelessWidget {
children: [
style.widgetWhen(
onData: (style) => MapLibreMap(
attributionButtonMargins: const Point(8, kToolbarHeight),
initialCameraPosition: CameraPosition(
target: initialLocation ?? const LatLng(0, 0),
zoom: initialLocation != null ? 12 : 0,

501
mobile/lib/platform/native_sync_api.g.dart generated Normal file
View File

@ -0,0 +1,501 @@
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
PlatformException _createConnectionError(String channelName) {
return PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel: "$channelName".',
);
}
bool _deepEquals(Object? a, Object? b) {
if (a is List && b is List) {
return a.length == b.length &&
a.indexed
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
}
if (a is Map && b is Map) {
return a.length == b.length &&
a.entries.every((MapEntry<Object?, Object?> entry) =>
(b as Map<Object?, Object?>).containsKey(entry.key) &&
_deepEquals(entry.value, b[entry.key]));
}
return a == b;
}
class PlatformAsset {
PlatformAsset({
required this.id,
required this.name,
required this.type,
this.createdAt,
this.updatedAt,
required this.durationInSeconds,
});
String id;
String name;
int type;
int? createdAt;
int? updatedAt;
int durationInSeconds;
List<Object?> _toList() {
return <Object?>[
id,
name,
type,
createdAt,
updatedAt,
durationInSeconds,
];
}
Object encode() {
return _toList();
}
static PlatformAsset decode(Object result) {
result as List<Object?>;
return PlatformAsset(
id: result[0]! as String,
name: result[1]! as String,
type: result[2]! as int,
createdAt: result[3] as int?,
updatedAt: result[4] as int?,
durationInSeconds: result[5]! as int,
);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! PlatformAsset || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(encode(), other.encode());
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList());
}
class PlatformAlbum {
PlatformAlbum({
required this.id,
required this.name,
this.updatedAt,
required this.isCloud,
required this.assetCount,
});
String id;
String name;
int? updatedAt;
bool isCloud;
int assetCount;
List<Object?> _toList() {
return <Object?>[
id,
name,
updatedAt,
isCloud,
assetCount,
];
}
Object encode() {
return _toList();
}
static PlatformAlbum decode(Object result) {
result as List<Object?>;
return PlatformAlbum(
id: result[0]! as String,
name: result[1]! as String,
updatedAt: result[2] as int?,
isCloud: result[3]! as bool,
assetCount: result[4]! as int,
);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! PlatformAlbum || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(encode(), other.encode());
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList());
}
class SyncDelta {
SyncDelta({
required this.hasChanges,
required this.updates,
required this.deletes,
required this.assetAlbums,
});
bool hasChanges;
List<PlatformAsset> updates;
List<String> deletes;
Map<String, List<String>> assetAlbums;
List<Object?> _toList() {
return <Object?>[
hasChanges,
updates,
deletes,
assetAlbums,
];
}
Object encode() {
return _toList();
}
static SyncDelta decode(Object result) {
result as List<Object?>;
return SyncDelta(
hasChanges: result[0]! as bool,
updates: (result[1] as List<Object?>?)!.cast<PlatformAsset>(),
deletes: (result[2] as List<Object?>?)!.cast<String>(),
assetAlbums:
(result[3] as Map<Object?, Object?>?)!.cast<String, List<String>>(),
);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! SyncDelta || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(encode(), other.encode());
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList());
}
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else if (value is PlatformAsset) {
buffer.putUint8(129);
writeValue(buffer, value.encode());
} else if (value is PlatformAlbum) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else if (value is SyncDelta) {
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 129:
return PlatformAsset.decode(readValue(buffer)!);
case 130:
return PlatformAlbum.decode(readValue(buffer)!);
case 131:
return SyncDelta.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
}
}
class NativeSyncApi {
/// Constructor for [NativeSyncApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
NativeSyncApi(
{BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix =
messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
Future<bool> shouldFullSync() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as bool?)!;
}
}
Future<SyncDelta> getMediaChanges() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as SyncDelta?)!;
}
}
Future<void> checkpointSync() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
Future<void> clearSyncCheckpoint() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
Future<List<String>> getAssetIdsForAlbum(String albumId) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture =
pigeonVar_channel.send(<Object?>[albumId]);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<String>();
}
}
Future<List<PlatformAlbum>> getAlbums() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<PlatformAlbum>();
}
}
Future<int> getAssetsCountSince(String albumId, int timestamp) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture =
pigeonVar_channel.send(<Object?>[albumId, timestamp]);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as int?)!;
}
}
Future<List<PlatformAsset>> getAssetsForAlbum(String albumId,
{int? updatedTimeCond}) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel =
BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture =
pigeonVar_channel.send(<Object?>[albumId, updatedTimeCond]);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<PlatformAsset>();
}
}
}

View File

@ -0,0 +1,68 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
// ignore: import_rule_isar
import 'package:isar/isar.dart';
const kDevLoggerTag = 'DEV';
abstract final class DLog {
const DLog();
static Stream<List<LogMessage>> watchLog() {
final db = Isar.getInstance();
if (db == null) {
debugPrint('Isar is not initialized');
return const Stream.empty();
}
return db.loggerMessages
.filter()
.context1EqualTo(kDevLoggerTag)
.sortByCreatedAtDesc()
.watch(fireImmediately: true)
.map((logs) => logs.map((log) => log.toDto()).toList());
}
static void clearLog() {
final db = Isar.getInstance();
if (db == null) {
debugPrint('Isar is not initialized');
return;
}
db.writeTxnSync(() {
db.loggerMessages.filter().context1EqualTo(kDevLoggerTag).deleteAllSync();
});
}
static void log(String message, [Object? error, StackTrace? stackTrace]) {
debugPrint('[$kDevLoggerTag] [${DateTime.now()}] $message');
if (error != null) {
debugPrint('Error: $error');
}
if (stackTrace != null) {
debugPrint('StackTrace: $stackTrace');
}
final isar = Isar.getInstance();
if (isar == null) {
debugPrint('Isar is not initialized');
return;
}
final record = LogMessage(
message: message,
level: LogLevel.info,
createdAt: DateTime.now(),
logger: kDevLoggerTag,
error: error?.toString(),
stack: stackTrace?.toString(),
);
unawaited(IsarLogRepository(isar).insert(record));
}
}

View File

@ -0,0 +1,201 @@
// ignore_for_file: avoid-local-functions
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:drift/drift.dart' hide Column;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/routing/router.dart';
final _features = [
_Feature(
name: 'Sync Local',
icon: Icons.photo_album_rounded,
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(),
),
_Feature(
name: 'Sync Local Full',
icon: Icons.photo_library_rounded,
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(full: true),
),
_Feature(
name: 'Sync Remote',
icon: Icons.refresh_rounded,
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncRemote(),
),
_Feature(
name: 'WAL Checkpoint',
icon: Icons.save_rounded,
onTap: (_, ref) => ref
.read(driftProvider)
.customStatement("pragma wal_checkpoint(truncate)"),
),
_Feature(
name: 'Clear Delta Checkpoint',
icon: Icons.delete_rounded,
onTap: (_, ref) => ref.read(nativeSyncApiProvider).clearSyncCheckpoint(),
),
_Feature(
name: 'Clear Local Data',
icon: Icons.delete_forever_rounded,
onTap: (_, ref) async {
final db = ref.read(driftProvider);
await db.localAssetEntity.deleteAll();
await db.localAlbumEntity.deleteAll();
await db.localAlbumAssetEntity.deleteAll();
},
),
_Feature(
name: 'Clear Remote Data',
icon: Icons.delete_sweep_rounded,
onTap: (_, ref) async {
final db = ref.read(driftProvider);
await db.remoteAssetEntity.deleteAll();
await db.remoteExifEntity.deleteAll();
},
),
_Feature(
name: 'Local Media Summary',
icon: Icons.table_chart_rounded,
onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()),
),
_Feature(
name: 'Remote Media Summary',
icon: Icons.summarize_rounded,
onTap: (ctx, _) => ctx.pushRoute(const RemoteMediaSummaryRoute()),
),
_Feature(
name: 'Reset Sqlite',
icon: Icons.table_view_rounded,
onTap: (_, ref) async {
final drift = ref.read(driftProvider);
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
final migrator = drift.createMigrator();
for (final entity in drift.allSchemaEntities) {
await migrator.drop(entity);
await migrator.create(entity);
}
},
),
];
@RoutePage()
class FeatInDevPage extends StatelessWidget {
const FeatInDevPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Features in Development'),
centerTitle: true,
),
body: Column(
children: [
Flexible(
flex: 1,
child: ListView.builder(
itemBuilder: (_, index) {
final feat = _features[index];
return Consumer(
builder: (ctx, ref, _) => ListTile(
title: Text(feat.name),
trailing: Icon(feat.icon),
visualDensity: VisualDensity.compact,
onTap: () => unawaited(feat.onTap(ctx, ref)),
),
);
},
itemCount: _features.length,
),
),
const Divider(height: 0),
const Flexible(child: _DevLogs()),
],
),
);
}
}
class _Feature {
const _Feature({
required this.name,
required this.icon,
required this.onTap,
});
final String name;
final IconData icon;
final Future<void> Function(BuildContext, WidgetRef _) onTap;
}
// ignore: prefer-single-widget-per-file
class _DevLogs extends StatelessWidget {
const _DevLogs();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
actions: [
IconButton(
onPressed: DLog.clearLog,
icon: Icon(
Icons.delete_outline_rounded,
size: 20.0,
color: context.primaryColor,
semanticLabel: "Clear logs",
),
),
],
centerTitle: true,
),
body: StreamBuilder(
initialData: [],
stream: DLog.watchLog(),
builder: (_, logMessages) {
return ListView.separated(
itemBuilder: (ctx, index) {
// ignore: avoid-unsafe-collection-methods
final logMessage = logMessages.data![index];
return ListTile(
title: Text(
logMessage.message,
style: TextStyle(
color: ctx.colorScheme.onSurface,
fontSize: 14.0,
overflow: TextOverflow.ellipsis,
),
),
subtitle: Text(
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}",
style: TextStyle(
color: ctx.colorScheme.onSurfaceSecondary,
fontSize: 12.0,
),
),
dense: true,
visualDensity: VisualDensity.compact,
tileColor: Colors.transparent,
minLeadingWidth: 10,
);
},
separatorBuilder: (_, index) {
return const Divider(height: 0);
},
itemCount: logMessages.data?.length ?? 0,
);
},
),
);
}
}

View File

@ -0,0 +1,167 @@
// ignore_for_file: prefer-single-widget-per-file
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
class _Stat {
const _Stat({required this.name, required this.load});
final String name;
final Future<int> Function(Drift _) load;
}
class _Summary extends StatelessWidget {
final String name;
final Future<int> countFuture;
const _Summary({required this.name, required this.countFuture});
@override
Widget build(BuildContext context) {
return FutureBuilder<int>(
future: countFuture,
builder: (ctx, snapshot) {
final Widget subtitle;
if (snapshot.connectionState == ConnectionState.waiting) {
subtitle = const CircularProgressIndicator();
} else if (snapshot.hasError) {
subtitle = const Icon(Icons.error_rounded);
} else {
subtitle = Text('${snapshot.data ?? 0}');
}
return ListTile(title: Text(name), trailing: subtitle);
},
);
}
}
final _localStats = [
_Stat(
name: 'Local Assets',
load: (db) => db.managers.localAssetEntity.count(),
),
_Stat(
name: 'Local Albums',
load: (db) => db.managers.localAlbumEntity.count(),
),
];
@RoutePage()
class LocalMediaSummaryPage extends StatelessWidget {
const LocalMediaSummaryPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Local Media Summary')),
body: Consumer(
builder: (ctx, ref, __) {
final db = ref.watch(driftProvider);
final albumsFuture = ref.watch(localAlbumRepository).getAll();
return CustomScrollView(
slivers: [
SliverList.builder(
itemBuilder: (_, index) {
final stat = _localStats[index];
final countFuture = stat.load(db);
return _Summary(name: stat.name, countFuture: countFuture);
},
itemCount: _localStats.length,
),
SliverToBoxAdapter(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Divider(),
Padding(
padding: const EdgeInsets.only(left: 15),
child: Text(
"Album summary",
style: ctx.textTheme.titleMedium,
),
),
],
),
),
FutureBuilder(
future: albumsFuture,
initialData: <LocalAlbum>[],
builder: (_, snap) {
final albums = snap.data!;
if (albums.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox.shrink());
}
albums.sortBy((a) => a.name);
return SliverList.builder(
itemBuilder: (_, index) {
final album = albums[index];
final countFuture = db.managers.localAlbumAssetEntity
.filter((f) => f.albumId.id.equals(album.id))
.count();
return _Summary(
name: album.name,
countFuture: countFuture,
);
},
itemCount: albums.length,
);
},
),
],
);
},
),
);
}
}
final _remoteStats = [
_Stat(
name: 'Remote Assets',
load: (db) => db.managers.remoteAssetEntity.count(),
),
_Stat(
name: 'Exif Entities',
load: (db) => db.managers.remoteExifEntity.count(),
),
];
@RoutePage()
class RemoteMediaSummaryPage extends StatelessWidget {
const RemoteMediaSummaryPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Remote Media Summary')),
body: Consumer(
builder: (ctx, ref, __) {
final db = ref.watch(driftProvider);
return CustomScrollView(
slivers: [
SliverList.builder(
itemBuilder: (_, index) {
final stat = _remoteStats[index];
final countFuture = stat.load(db);
return _Summary(name: stat.name, countFuture: countFuture);
},
itemCount: _remoteStats.length,
),
],
);
},
),
);
}
}

View File

@ -0,0 +1,62 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
import 'package:immich_mobile/services/local_album.service.dart';
final backupAlbumProvider =
StateNotifierProvider<BackupAlbumNotifier, List<LocalAlbum>>(
(ref) => BackupAlbumNotifier(
ref.watch(localAlbumsServiceProvider),
),
);
class BackupAlbumNotifier extends StateNotifier<List<LocalAlbum>> {
BackupAlbumNotifier(this._localAlbumService) : super([]) {
getAll();
}
final LocalAlbumService _localAlbumService;
Future<void> getAll() async {
state = await _localAlbumService.getAll();
}
Future<void> selectAlbum(LocalAlbum album) async {
album = album.copyWith(backupSelection: BackupSelection.selected);
await _localAlbumService.update(album);
state = state
.map(
(currentAlbum) => currentAlbum.id == album.id
? currentAlbum.copyWith(backupSelection: BackupSelection.selected)
: currentAlbum,
)
.toList();
}
Future<void> deselectAlbum(LocalAlbum album) async {
album = album.copyWith(backupSelection: BackupSelection.none);
await _localAlbumService.update(album);
state = state
.map(
(currentAlbum) => currentAlbum.id == album.id
? currentAlbum.copyWith(backupSelection: BackupSelection.none)
: currentAlbum,
)
.toList();
}
Future<void> excludeAlbum(LocalAlbum album) async {
album = album.copyWith(backupSelection: BackupSelection.excluded);
await _localAlbumService.update(album);
state = state
.map(
(currentAlbum) => currentAlbum.id == album.id
? currentAlbum.copyWith(backupSelection: BackupSelection.excluded)
: currentAlbum,
)
.toList();
}
}

View File

@ -0,0 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
final localAlbumRepository = Provider<ILocalAlbumRepository>(
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
);

View File

@ -0,0 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());

View File

@ -1,10 +1,14 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/local_sync.service.dart';
import 'package:immich_mobile/domain/services/sync_stream.service.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/cancel.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/store.provider.dart';
final syncStreamServiceProvider = Provider(
(ref) => SyncStreamService(
@ -21,3 +25,11 @@ final syncApiRepositoryProvider = Provider(
final syncStreamRepositoryProvider = Provider(
(ref) => DriftSyncStreamRepository(ref.watch(driftProvider)),
);
final localSyncServiceProvider = Provider(
(ref) => LocalSyncService(
localAlbumRepository: ref.watch(localAlbumRepository),
nativeSyncApi: ref.watch(nativeSyncApiProvider),
storeService: ref.watch(storeServiceProvider),
),
);

View File

@ -17,6 +17,30 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
bool get useCustomFilter =>
Store.get(StoreKey.photoManagerCustomFilter, false);
FilterOptionGroup? _getAlbumFilter({
DateTimeCond? updateTimeCond,
bool? containsPathModified,
List<OrderOption>? orderBy,
}) =>
useCustomFilter
? FilterOptionGroup(
imageOption: const FilterOption(
needTitle: true,
sizeConstraint: SizeConstraint(ignoreSize: true),
),
videoOption: const FilterOption(
needTitle: true,
sizeConstraint: SizeConstraint(ignoreSize: true),
durationConstraint: DurationConstraint(allowNullable: true),
),
containsPathModified: containsPathModified ?? false,
createTimeCond: DateTimeCond.def().copyWith(ignore: true),
updateTimeCond:
updateTimeCond ?? DateTimeCond.def().copyWith(ignore: true),
orders: orderBy ?? [],
)
: null;
@override
Future<List<Album>> getAll() async {
final filter = useCustomFilter
@ -30,7 +54,8 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
@override
Future<List<String>> getAssetIds(String albumId) async {
final album = await AssetPathEntity.fromId(albumId);
final album =
await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter());
final List<AssetEntity> assets =
await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
return assets.map((e) => e.id).toList();
@ -38,7 +63,8 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
@override
Future<int> getAssetCount(String albumId) async {
final album = await AssetPathEntity.fromId(albumId);
final album =
await AssetPathEntity.fromId(albumId, filterOption: _getAlbumFilter());
return album.assetCountAsync;
}
@ -53,17 +79,14 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
}) async {
final onDevice = await AssetPathEntity.fromId(
albumId,
filterOption: FilterOptionGroup(
imageOption: const FilterOption(needTitle: true),
videoOption: const FilterOption(needTitle: true),
containsPathModified: true,
filterOption: _getAlbumFilter(
updateTimeCond: modifiedFrom == null && modifiedUntil == null
? null
: DateTimeCond(
min: modifiedFrom ?? DateTime.utc(-271820),
max: modifiedUntil ?? DateTime.utc(275760),
),
orders: orderByModificationDate
orderBy: orderByModificationDate
? [const OrderOption(type: OrderOptionType.updateDate)]
: [],
),
@ -80,7 +103,10 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
DateTime? modifiedFrom,
DateTime? modifiedUntil,
}) async {
final assetPathEntity = await AssetPathEntity.fromId(id);
final assetPathEntity = await AssetPathEntity.fromId(
id,
filterOption: _getAlbumFilter(containsPathModified: true),
);
return _toAlbum(assetPathEntity);
}

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
@ -8,17 +9,22 @@ import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/interfaces/auth.interface.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
final authRepositoryProvider = Provider<IAuthRepository>(
(ref) => AuthRepository(ref.watch(dbProvider)),
(ref) =>
AuthRepository(ref.watch(dbProvider), drift: ref.watch(driftProvider)),
);
class AuthRepository extends DatabaseRepository implements IAuthRepository {
AuthRepository(super.db);
final Drift _drift;
AuthRepository(super.db, {required Drift drift}) : _drift = drift;
@override
Future<void> clearLocalData() {
@ -29,6 +35,8 @@ class AuthRepository extends DatabaseRepository implements IAuthRepository {
db.albums.clear(),
db.eTags.clear(),
db.users.clear(),
_drift.remoteAssetEntity.deleteAll(),
_drift.remoteExifEntity.deleteAll(),
]);
});
}

View File

@ -98,8 +98,10 @@ class TimelineRepository extends DatabaseRepository
}
@override
Stream<RenderList> watchAllVideosTimeline() {
Stream<RenderList> watchAllVideosTimeline(String userId) {
final query = db.assets
.where()
.ownerIdEqualToAnyChecksum(fastHash(userId))
.filter()
.isTrashedEqualTo(false)
.visibilityEqualTo(AssetVisibilityEnum.timeline)

View File

@ -63,6 +63,8 @@ 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/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/media_stat.page.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/routing/auth_guard.dart';
@ -316,5 +318,17 @@ class AppRouter extends RootStackRouter {
page: PinAuthRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: FeatInDevRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: LocalMediaSummaryRoute.page,
guards: [_authGuard, _duplicateGuard],
),
AutoRoute(
page: RemoteMediaSummaryRoute.page,
guards: [_authGuard, _duplicateGuard],
),
];
}

View File

@ -1,3 +1,4 @@
// dart format width=80
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
@ -13,10 +14,7 @@ part of 'router.dart';
/// [ActivitiesPage]
class ActivitiesRoute extends PageRouteInfo<void> {
const ActivitiesRoute({List<PageRouteInfo>? children})
: super(
ActivitiesRoute.name,
initialChildren: children,
);
: super(ActivitiesRoute.name, initialChildren: children);
static const String name = 'ActivitiesRoute';
@ -132,10 +130,7 @@ class AlbumAssetSelectionRouteArgs {
/// [AlbumOptionsPage]
class AlbumOptionsRoute extends PageRouteInfo<void> {
const AlbumOptionsRoute({List<PageRouteInfo>? children})
: super(
AlbumOptionsRoute.name,
initialChildren: children,
);
: super(AlbumOptionsRoute.name, initialChildren: children);
static const String name = 'AlbumOptionsRoute';
@ -156,10 +151,7 @@ class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> {
List<PageRouteInfo>? children,
}) : super(
AlbumPreviewRoute.name,
args: AlbumPreviewRouteArgs(
key: key,
album: album,
),
args: AlbumPreviewRouteArgs(key: key, album: album),
initialChildren: children,
);
@ -169,19 +161,13 @@ class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> {
name,
builder: (data) {
final args = data.argsAs<AlbumPreviewRouteArgs>();
return AlbumPreviewPage(
key: args.key,
album: args.album,
);
return AlbumPreviewPage(key: args.key, album: args.album);
},
);
}
class AlbumPreviewRouteArgs {
const AlbumPreviewRouteArgs({
this.key,
required this.album,
});
const AlbumPreviewRouteArgs({this.key, required this.album});
final Key? key;
@ -203,10 +189,7 @@ class AlbumSharedUserSelectionRoute
List<PageRouteInfo>? children,
}) : super(
AlbumSharedUserSelectionRoute.name,
args: AlbumSharedUserSelectionRouteArgs(
key: key,
assets: assets,
),
args: AlbumSharedUserSelectionRouteArgs(key: key, assets: assets),
initialChildren: children,
);
@ -216,19 +199,13 @@ class AlbumSharedUserSelectionRoute
name,
builder: (data) {
final args = data.argsAs<AlbumSharedUserSelectionRouteArgs>();
return AlbumSharedUserSelectionPage(
key: args.key,
assets: args.assets,
);
return AlbumSharedUserSelectionPage(key: args.key, assets: args.assets);
},
);
}
class AlbumSharedUserSelectionRouteArgs {
const AlbumSharedUserSelectionRouteArgs({
this.key,
required this.assets,
});
const AlbumSharedUserSelectionRouteArgs({this.key, required this.assets});
final Key? key;
@ -249,10 +226,7 @@ class AlbumViewerRoute extends PageRouteInfo<AlbumViewerRouteArgs> {
List<PageRouteInfo>? children,
}) : super(
AlbumViewerRoute.name,
args: AlbumViewerRouteArgs(
key: key,
albumId: albumId,
),
args: AlbumViewerRouteArgs(key: key, albumId: albumId),
initialChildren: children,
);
@ -262,19 +236,13 @@ class AlbumViewerRoute extends PageRouteInfo<AlbumViewerRouteArgs> {
name,
builder: (data) {
final args = data.argsAs<AlbumViewerRouteArgs>();
return AlbumViewerPage(
key: args.key,
albumId: args.albumId,
);
return AlbumViewerPage(key: args.key, albumId: args.albumId);
},
);
}
class AlbumViewerRouteArgs {
const AlbumViewerRouteArgs({
this.key,
required this.albumId,
});
const AlbumViewerRouteArgs({this.key, required this.albumId});
final Key? key;
@ -290,10 +258,7 @@ class AlbumViewerRouteArgs {
/// [AlbumsPage]
class AlbumsRoute extends PageRouteInfo<void> {
const AlbumsRoute({List<PageRouteInfo>? children})
: super(
AlbumsRoute.name,
initialChildren: children,
);
: super(AlbumsRoute.name, initialChildren: children);
static const String name = 'AlbumsRoute';
@ -309,10 +274,7 @@ class AlbumsRoute extends PageRouteInfo<void> {
/// [AllMotionPhotosPage]
class AllMotionPhotosRoute extends PageRouteInfo<void> {
const AllMotionPhotosRoute({List<PageRouteInfo>? children})
: super(
AllMotionPhotosRoute.name,
initialChildren: children,
);
: super(AllMotionPhotosRoute.name, initialChildren: children);
static const String name = 'AllMotionPhotosRoute';
@ -328,10 +290,7 @@ class AllMotionPhotosRoute extends PageRouteInfo<void> {
/// [AllPeoplePage]
class AllPeopleRoute extends PageRouteInfo<void> {
const AllPeopleRoute({List<PageRouteInfo>? children})
: super(
AllPeopleRoute.name,
initialChildren: children,
);
: super(AllPeopleRoute.name, initialChildren: children);
static const String name = 'AllPeopleRoute';
@ -347,10 +306,7 @@ class AllPeopleRoute extends PageRouteInfo<void> {
/// [AllPlacesPage]
class AllPlacesRoute extends PageRouteInfo<void> {
const AllPlacesRoute({List<PageRouteInfo>? children})
: super(
AllPlacesRoute.name,
initialChildren: children,
);
: super(AllPlacesRoute.name, initialChildren: children);
static const String name = 'AllPlacesRoute';
@ -366,10 +322,7 @@ class AllPlacesRoute extends PageRouteInfo<void> {
/// [AllVideosPage]
class AllVideosRoute extends PageRouteInfo<void> {
const AllVideosRoute({List<PageRouteInfo>? children})
: super(
AllVideosRoute.name,
initialChildren: children,
);
: super(AllVideosRoute.name, initialChildren: children);
static const String name = 'AllVideosRoute';
@ -390,10 +343,7 @@ class AppLogDetailRoute extends PageRouteInfo<AppLogDetailRouteArgs> {
List<PageRouteInfo>? children,
}) : super(
AppLogDetailRoute.name,
args: AppLogDetailRouteArgs(
key: key,
logMessage: logMessage,
),
args: AppLogDetailRouteArgs(key: key, logMessage: logMessage),
initialChildren: children,
);
@ -403,19 +353,13 @@ class AppLogDetailRoute extends PageRouteInfo<AppLogDetailRouteArgs> {
name,
builder: (data) {
final args = data.argsAs<AppLogDetailRouteArgs>();
return AppLogDetailPage(
key: args.key,
logMessage: args.logMessage,
);
return AppLogDetailPage(key: args.key, logMessage: args.logMessage);
},
);
}
class AppLogDetailRouteArgs {
const AppLogDetailRouteArgs({
this.key,
required this.logMessage,
});
const AppLogDetailRouteArgs({this.key, required this.logMessage});
final Key? key;
@ -431,10 +375,7 @@ class AppLogDetailRouteArgs {
/// [AppLogPage]
class AppLogRoute extends PageRouteInfo<void> {
const AppLogRoute({List<PageRouteInfo>? children})
: super(
AppLogRoute.name,
initialChildren: children,
);
: super(AppLogRoute.name, initialChildren: children);
static const String name = 'AppLogRoute';
@ -450,10 +391,7 @@ class AppLogRoute extends PageRouteInfo<void> {
/// [ArchivePage]
class ArchiveRoute extends PageRouteInfo<void> {
const ArchiveRoute({List<PageRouteInfo>? children})
: super(
ArchiveRoute.name,
initialChildren: children,
);
: super(ArchiveRoute.name, initialChildren: children);
static const String name = 'ArchiveRoute';
@ -469,10 +407,7 @@ class ArchiveRoute extends PageRouteInfo<void> {
/// [BackupAlbumSelectionPage]
class BackupAlbumSelectionRoute extends PageRouteInfo<void> {
const BackupAlbumSelectionRoute({List<PageRouteInfo>? children})
: super(
BackupAlbumSelectionRoute.name,
initialChildren: children,
);
: super(BackupAlbumSelectionRoute.name, initialChildren: children);
static const String name = 'BackupAlbumSelectionRoute';
@ -488,10 +423,7 @@ class BackupAlbumSelectionRoute extends PageRouteInfo<void> {
/// [BackupControllerPage]
class BackupControllerRoute extends PageRouteInfo<void> {
const BackupControllerRoute({List<PageRouteInfo>? children})
: super(
BackupControllerRoute.name,
initialChildren: children,
);
: super(BackupControllerRoute.name, initialChildren: children);
static const String name = 'BackupControllerRoute';
@ -507,10 +439,7 @@ class BackupControllerRoute extends PageRouteInfo<void> {
/// [BackupOptionsPage]
class BackupOptionsRoute extends PageRouteInfo<void> {
const BackupOptionsRoute({List<PageRouteInfo>? children})
: super(
BackupOptionsRoute.name,
initialChildren: children,
);
: super(BackupOptionsRoute.name, initialChildren: children);
static const String name = 'BackupOptionsRoute';
@ -526,10 +455,7 @@ class BackupOptionsRoute extends PageRouteInfo<void> {
/// [ChangePasswordPage]
class ChangePasswordRoute extends PageRouteInfo<void> {
const ChangePasswordRoute({List<PageRouteInfo>? children})
: super(
ChangePasswordRoute.name,
initialChildren: children,
);
: super(ChangePasswordRoute.name, initialChildren: children);
static const String name = 'ChangePasswordRoute';
@ -550,10 +476,7 @@ class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
List<PageRouteInfo>? children,
}) : super(
CreateAlbumRoute.name,
args: CreateAlbumRouteArgs(
key: key,
assets: assets,
),
args: CreateAlbumRouteArgs(key: key, assets: assets),
initialChildren: children,
);
@ -563,20 +486,15 @@ class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
name,
builder: (data) {
final args = data.argsAs<CreateAlbumRouteArgs>(
orElse: () => const CreateAlbumRouteArgs());
return CreateAlbumPage(
key: args.key,
assets: args.assets,
orElse: () => const CreateAlbumRouteArgs(),
);
return CreateAlbumPage(key: args.key, assets: args.assets);
},
);
}
class CreateAlbumRouteArgs {
const CreateAlbumRouteArgs({
this.key,
this.assets,
});
const CreateAlbumRouteArgs({this.key, this.assets});
final Key? key;
@ -598,11 +516,7 @@ class CropImageRoute extends PageRouteInfo<CropImageRouteArgs> {
List<PageRouteInfo>? children,
}) : super(
CropImageRoute.name,
args: CropImageRouteArgs(
key: key,
image: image,
asset: asset,
),
args: CropImageRouteArgs(key: key, image: image, asset: asset),
initialChildren: children,
);
@ -612,11 +526,7 @@ class CropImageRoute extends PageRouteInfo<CropImageRouteArgs> {
name,
builder: (data) {
final args = data.argsAs<CropImageRouteArgs>();
return CropImagePage(
key: args.key,
image: args.image,
asset: args.asset,
);
return CropImagePage(key: args.key, image: args.image, asset: args.asset);
},
);
}
@ -702,10 +612,7 @@ class EditImageRouteArgs {
/// [FailedBackupStatusPage]
class FailedBackupStatusRoute extends PageRouteInfo<void> {
const FailedBackupStatusRoute({List<PageRouteInfo>? children})
: super(
FailedBackupStatusRoute.name,
initialChildren: children,
);
: super(FailedBackupStatusRoute.name, initialChildren: children);
static const String name = 'FailedBackupStatusRoute';
@ -721,10 +628,7 @@ class FailedBackupStatusRoute extends PageRouteInfo<void> {
/// [FavoritesPage]
class FavoritesRoute extends PageRouteInfo<void> {
const FavoritesRoute({List<PageRouteInfo>? children})
: super(
FavoritesRoute.name,
initialChildren: children,
);
: super(FavoritesRoute.name, initialChildren: children);
static const String name = 'FavoritesRoute';
@ -736,6 +640,22 @@ class FavoritesRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [FeatInDevPage]
class FeatInDevRoute extends PageRouteInfo<void> {
const FeatInDevRoute({List<PageRouteInfo>? children})
: super(FeatInDevRoute.name, initialChildren: children);
static const String name = 'FeatInDevRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const FeatInDevPage();
},
);
}
/// generated route for
/// [FilterImagePage]
class FilterImageRoute extends PageRouteInfo<FilterImageRouteArgs> {
@ -746,11 +666,7 @@ class FilterImageRoute extends PageRouteInfo<FilterImageRouteArgs> {
List<PageRouteInfo>? children,
}) : super(
FilterImageRoute.name,
args: FilterImageRouteArgs(
key: key,
image: image,
asset: asset,
),
args: FilterImageRouteArgs(key: key, image: image, asset: asset),
initialChildren: children,
);
@ -797,10 +713,7 @@ class FolderRoute extends PageRouteInfo<FolderRouteArgs> {
List<PageRouteInfo>? children,
}) : super(
FolderRoute.name,
args: FolderRouteArgs(
key: key,
folder: folder,
),
args: FolderRouteArgs(key: key, folder: folder),
initialChildren: children,
);
@ -809,21 +722,16 @@ class FolderRoute extends PageRouteInfo<FolderRouteArgs> {
static PageInfo page = PageInfo(
name,
builder: (data) {
final args =
data.argsAs<FolderRouteArgs>(orElse: () => const FolderRouteArgs());
return FolderPage(
key: args.key,
folder: args.folder,
final args = data.argsAs<FolderRouteArgs>(
orElse: () => const FolderRouteArgs(),
);
return FolderPage(key: args.key, folder: args.folder);
},
);
}
class FolderRouteArgs {
const FolderRouteArgs({
this.key,
this.folder,
});
const FolderRouteArgs({this.key, this.folder});
final Key? key;
@ -903,10 +811,7 @@ class GalleryViewerRouteArgs {
/// [HeaderSettingsPage]
class HeaderSettingsRoute extends PageRouteInfo<void> {
const HeaderSettingsRoute({List<PageRouteInfo>? children})
: super(
HeaderSettingsRoute.name,
initialChildren: children,
);
: super(HeaderSettingsRoute.name, initialChildren: children);
static const String name = 'HeaderSettingsRoute';
@ -922,10 +827,7 @@ class HeaderSettingsRoute extends PageRouteInfo<void> {
/// [LibraryPage]
class LibraryRoute extends PageRouteInfo<void> {
const LibraryRoute({List<PageRouteInfo>? children})
: super(
LibraryRoute.name,
initialChildren: children,
);
: super(LibraryRoute.name, initialChildren: children);
static const String name = 'LibraryRoute';
@ -941,10 +843,7 @@ class LibraryRoute extends PageRouteInfo<void> {
/// [LocalAlbumsPage]
class LocalAlbumsRoute extends PageRouteInfo<void> {
const LocalAlbumsRoute({List<PageRouteInfo>? children})
: super(
LocalAlbumsRoute.name,
initialChildren: children,
);
: super(LocalAlbumsRoute.name, initialChildren: children);
static const String name = 'LocalAlbumsRoute';
@ -956,14 +855,27 @@ class LocalAlbumsRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [LocalMediaSummaryPage]
class LocalMediaSummaryRoute extends PageRouteInfo<void> {
const LocalMediaSummaryRoute({List<PageRouteInfo>? children})
: super(LocalMediaSummaryRoute.name, initialChildren: children);
static const String name = 'LocalMediaSummaryRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const LocalMediaSummaryPage();
},
);
}
/// generated route for
/// [LockedPage]
class LockedRoute extends PageRouteInfo<void> {
const LockedRoute({List<PageRouteInfo>? children})
: super(
LockedRoute.name,
initialChildren: children,
);
: super(LockedRoute.name, initialChildren: children);
static const String name = 'LockedRoute';
@ -979,10 +891,7 @@ class LockedRoute extends PageRouteInfo<void> {
/// [LoginPage]
class LoginRoute extends PageRouteInfo<void> {
const LoginRoute({List<PageRouteInfo>? children})
: super(
LoginRoute.name,
initialChildren: children,
);
: super(LoginRoute.name, initialChildren: children);
static const String name = 'LoginRoute';
@ -1016,7 +925,8 @@ class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
name,
builder: (data) {
final args = data.argsAs<MapLocationPickerRouteArgs>(
orElse: () => const MapLocationPickerRouteArgs());
orElse: () => const MapLocationPickerRouteArgs(),
);
return MapLocationPickerPage(
key: args.key,
initialLatLng: args.initialLatLng,
@ -1044,16 +954,10 @@ class MapLocationPickerRouteArgs {
/// generated route for
/// [MapPage]
class MapRoute extends PageRouteInfo<MapRouteArgs> {
MapRoute({
Key? key,
LatLng? initialLocation,
List<PageRouteInfo>? children,
}) : super(
MapRoute({Key? key, LatLng? initialLocation, List<PageRouteInfo>? children})
: super(
MapRoute.name,
args: MapRouteArgs(
key: key,
initialLocation: initialLocation,
),
args: MapRouteArgs(key: key, initialLocation: initialLocation),
initialChildren: children,
);
@ -1062,21 +966,16 @@ class MapRoute extends PageRouteInfo<MapRouteArgs> {
static PageInfo page = PageInfo(
name,
builder: (data) {
final args =
data.argsAs<MapRouteArgs>(orElse: () => const MapRouteArgs());
return MapPage(
key: args.key,
initialLocation: args.initialLocation,
final args = data.argsAs<MapRouteArgs>(
orElse: () => const MapRouteArgs(),
);
return MapPage(key: args.key, initialLocation: args.initialLocation);
},
);
}
class MapRouteArgs {
const MapRouteArgs({
this.key,
this.initialLocation,
});
const MapRouteArgs({this.key, this.initialLocation});
final Key? key;
@ -1213,10 +1112,7 @@ class PartnerDetailRoute extends PageRouteInfo<PartnerDetailRouteArgs> {
List<PageRouteInfo>? children,
}) : super(
PartnerDetailRoute.name,
args: PartnerDetailRouteArgs(
key: key,
partner: partner,
),
args: PartnerDetailRouteArgs(key: key, partner: partner),
initialChildren: children,
);
@ -1226,19 +1122,13 @@ class PartnerDetailRoute extends PageRouteInfo<PartnerDetailRouteArgs> {
name,
builder: (data) {
final args = data.argsAs<PartnerDetailRouteArgs>();
return PartnerDetailPage(
key: args.key,
partner: args.partner,
);
return PartnerDetailPage(key: args.key, partner: args.partner);
},
);
}
class PartnerDetailRouteArgs {
const PartnerDetailRouteArgs({
this.key,
required this.partner,
});
const PartnerDetailRouteArgs({this.key, required this.partner});
final Key? key;
@ -1254,10 +1144,7 @@ class PartnerDetailRouteArgs {
/// [PartnerPage]
class PartnerRoute extends PageRouteInfo<void> {
const PartnerRoute({List<PageRouteInfo>? children})
: super(
PartnerRoute.name,
initialChildren: children,
);
: super(PartnerRoute.name, initialChildren: children);
static const String name = 'PartnerRoute';
@ -1273,10 +1160,7 @@ class PartnerRoute extends PageRouteInfo<void> {
/// [PeopleCollectionPage]
class PeopleCollectionRoute extends PageRouteInfo<void> {
const PeopleCollectionRoute({List<PageRouteInfo>? children})
: super(
PeopleCollectionRoute.name,
initialChildren: children,
);
: super(PeopleCollectionRoute.name, initialChildren: children);
static const String name = 'PeopleCollectionRoute';
@ -1292,10 +1176,7 @@ class PeopleCollectionRoute extends PageRouteInfo<void> {
/// [PermissionOnboardingPage]
class PermissionOnboardingRoute extends PageRouteInfo<void> {
const PermissionOnboardingRoute({List<PageRouteInfo>? children})
: super(
PermissionOnboardingRoute.name,
initialChildren: children,
);
: super(PermissionOnboardingRoute.name, initialChildren: children);
static const String name = 'PermissionOnboardingRoute';
@ -1363,10 +1244,7 @@ class PersonResultRouteArgs {
/// [PhotosPage]
class PhotosRoute extends PageRouteInfo<void> {
const PhotosRoute({List<PageRouteInfo>? children})
: super(
PhotosRoute.name,
initialChildren: children,
);
: super(PhotosRoute.name, initialChildren: children);
static const String name = 'PhotosRoute';
@ -1387,10 +1265,7 @@ class PinAuthRoute extends PageRouteInfo<PinAuthRouteArgs> {
List<PageRouteInfo>? children,
}) : super(
PinAuthRoute.name,
args: PinAuthRouteArgs(
key: key,
createPinCode: createPinCode,
),
args: PinAuthRouteArgs(key: key, createPinCode: createPinCode),
initialChildren: children,
);
@ -1399,21 +1274,16 @@ class PinAuthRoute extends PageRouteInfo<PinAuthRouteArgs> {
static PageInfo page = PageInfo(
name,
builder: (data) {
final args =
data.argsAs<PinAuthRouteArgs>(orElse: () => const PinAuthRouteArgs());
return PinAuthPage(
key: args.key,
createPinCode: args.createPinCode,
final args = data.argsAs<PinAuthRouteArgs>(
orElse: () => const PinAuthRouteArgs(),
);
return PinAuthPage(key: args.key, createPinCode: args.createPinCode);
},
);
}
class PinAuthRouteArgs {
const PinAuthRouteArgs({
this.key,
this.createPinCode = false,
});
const PinAuthRouteArgs({this.key, this.createPinCode = false});
final Key? key;
@ -1447,7 +1317,8 @@ class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> {
name,
builder: (data) {
final args = data.argsAs<PlacesCollectionRouteArgs>(
orElse: () => const PlacesCollectionRouteArgs());
orElse: () => const PlacesCollectionRouteArgs(),
);
return PlacesCollectionPage(
key: args.key,
currentLocation: args.currentLocation,
@ -1457,10 +1328,7 @@ class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> {
}
class PlacesCollectionRouteArgs {
const PlacesCollectionRouteArgs({
this.key,
this.currentLocation,
});
const PlacesCollectionRouteArgs({this.key, this.currentLocation});
final Key? key;
@ -1476,10 +1344,7 @@ class PlacesCollectionRouteArgs {
/// [RecentlyTakenPage]
class RecentlyTakenRoute extends PageRouteInfo<void> {
const RecentlyTakenRoute({List<PageRouteInfo>? children})
: super(
RecentlyTakenRoute.name,
initialChildren: children,
);
: super(RecentlyTakenRoute.name, initialChildren: children);
static const String name = 'RecentlyTakenRoute';
@ -1491,6 +1356,22 @@ class RecentlyTakenRoute extends PageRouteInfo<void> {
);
}
/// generated route for
/// [RemoteMediaSummaryPage]
class RemoteMediaSummaryRoute extends PageRouteInfo<void> {
const RemoteMediaSummaryRoute({List<PageRouteInfo>? children})
: super(RemoteMediaSummaryRoute.name, initialChildren: children);
static const String name = 'RemoteMediaSummaryRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const RemoteMediaSummaryPage();
},
);
}
/// generated route for
/// [SearchPage]
class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
@ -1500,10 +1381,7 @@ class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
List<PageRouteInfo>? children,
}) : super(
SearchRoute.name,
args: SearchRouteArgs(
key: key,
prefilter: prefilter,
),
args: SearchRouteArgs(key: key, prefilter: prefilter),
initialChildren: children,
);
@ -1512,21 +1390,16 @@ class SearchRoute extends PageRouteInfo<SearchRouteArgs> {
static PageInfo page = PageInfo(
name,
builder: (data) {
final args =
data.argsAs<SearchRouteArgs>(orElse: () => const SearchRouteArgs());
return SearchPage(
key: args.key,
prefilter: args.prefilter,
final args = data.argsAs<SearchRouteArgs>(
orElse: () => const SearchRouteArgs(),
);
return SearchPage(key: args.key, prefilter: args.prefilter);
},
);
}
class SearchRouteArgs {
const SearchRouteArgs({
this.key,
this.prefilter,
});
const SearchRouteArgs({this.key, this.prefilter});
final Key? key;
@ -1542,10 +1415,7 @@ class SearchRouteArgs {
/// [SettingsPage]
class SettingsRoute extends PageRouteInfo<void> {
const SettingsRoute({List<PageRouteInfo>? children})
: super(
SettingsRoute.name,
initialChildren: children,
);
: super(SettingsRoute.name, initialChildren: children);
static const String name = 'SettingsRoute';
@ -1566,10 +1436,7 @@ class SettingsSubRoute extends PageRouteInfo<SettingsSubRouteArgs> {
List<PageRouteInfo>? children,
}) : super(
SettingsSubRoute.name,
args: SettingsSubRouteArgs(
section: section,
key: key,
),
args: SettingsSubRouteArgs(section: section, key: key),
initialChildren: children,
);
@ -1579,19 +1446,13 @@ class SettingsSubRoute extends PageRouteInfo<SettingsSubRouteArgs> {
name,
builder: (data) {
final args = data.argsAs<SettingsSubRouteArgs>();
return SettingsSubPage(
args.section,
key: args.key,
);
return SettingsSubPage(args.section, key: args.key);
},
);
}
class SettingsSubRouteArgs {
const SettingsSubRouteArgs({
required this.section,
this.key,
});
const SettingsSubRouteArgs({required this.section, this.key});
final SettingSection section;
@ -1612,10 +1473,7 @@ class ShareIntentRoute extends PageRouteInfo<ShareIntentRouteArgs> {
List<PageRouteInfo>? children,
}) : super(
ShareIntentRoute.name,
args: ShareIntentRouteArgs(
key: key,
attachments: attachments,
),
args: ShareIntentRouteArgs(key: key, attachments: attachments),
initialChildren: children,
);
@ -1625,19 +1483,13 @@ class ShareIntentRoute extends PageRouteInfo<ShareIntentRouteArgs> {
name,
builder: (data) {
final args = data.argsAs<ShareIntentRouteArgs>();
return ShareIntentPage(
key: args.key,
attachments: args.attachments,
);
return ShareIntentPage(key: args.key, attachments: args.attachments);
},
);
}
class ShareIntentRouteArgs {
const ShareIntentRouteArgs({
this.key,
required this.attachments,
});
const ShareIntentRouteArgs({this.key, required this.attachments});
final Key? key;
@ -1675,7 +1527,8 @@ class SharedLinkEditRoute extends PageRouteInfo<SharedLinkEditRouteArgs> {
name,
builder: (data) {
final args = data.argsAs<SharedLinkEditRouteArgs>(
orElse: () => const SharedLinkEditRouteArgs());
orElse: () => const SharedLinkEditRouteArgs(),
);
return SharedLinkEditPage(
key: args.key,
existingLink: args.existingLink,
@ -1712,10 +1565,7 @@ class SharedLinkEditRouteArgs {
/// [SharedLinkPage]
class SharedLinkRoute extends PageRouteInfo<void> {
const SharedLinkRoute({List<PageRouteInfo>? children})
: super(
SharedLinkRoute.name,
initialChildren: children,
);
: super(SharedLinkRoute.name, initialChildren: children);
static const String name = 'SharedLinkRoute';
@ -1731,10 +1581,7 @@ class SharedLinkRoute extends PageRouteInfo<void> {
/// [SplashScreenPage]
class SplashScreenRoute extends PageRouteInfo<void> {
const SplashScreenRoute({List<PageRouteInfo>? children})
: super(
SplashScreenRoute.name,
initialChildren: children,
);
: super(SplashScreenRoute.name, initialChildren: children);
static const String name = 'SplashScreenRoute';
@ -1750,10 +1597,7 @@ class SplashScreenRoute extends PageRouteInfo<void> {
/// [TabControllerPage]
class TabControllerRoute extends PageRouteInfo<void> {
const TabControllerRoute({List<PageRouteInfo>? children})
: super(
TabControllerRoute.name,
initialChildren: children,
);
: super(TabControllerRoute.name, initialChildren: children);
static const String name = 'TabControllerRoute';
@ -1769,10 +1613,7 @@ class TabControllerRoute extends PageRouteInfo<void> {
/// [TrashPage]
class TrashRoute extends PageRouteInfo<void> {
const TrashRoute({List<PageRouteInfo>? children})
: super(
TrashRoute.name,
initialChildren: children,
);
: super(TrashRoute.name, initialChildren: children);
static const String name = 'TrashRoute';

View File

@ -104,7 +104,7 @@ class AppSettingsService {
return Store.get(setting.storeKey, setting.defaultValue);
}
void setSetting<T>(AppSettingsEnum<T> setting, T value) {
Store.put(setting.storeKey, value);
Future<void> setSetting<T>(AppSettingsEnum<T> setting, T value) {
return Store.put(setting.storeKey, value);
}
}

View File

@ -0,0 +1,24 @@
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
final localAlbumsServiceProvider = Provider<LocalAlbumService>(
(ref) => LocalAlbumService(
ref.watch(localAlbumRepository),
),
);
class LocalAlbumService {
LocalAlbumService(this._localAlbumRepository);
final ILocalAlbumRepository _localAlbumRepository;
Future<List<LocalAlbum>> getAll() {
return _localAlbumRepository.getAll();
}
Future<void> update(LocalAlbum album) {
return _localAlbumRepository.update(album);
}
}

View File

@ -75,7 +75,9 @@ class TimelineService {
}
Stream<RenderList> watchAllVideosTimeline() {
return _timelineRepository.watchAllVideosTimeline();
final user = _userService.getMyUser();
return _timelineRepository.watchAllVideosTimeline(user.id);
}
Future<RenderList> getTimelineFromAssets(

View File

@ -11,6 +11,7 @@ dynamic upgradeDto(dynamic value, String targetType) {
addDefault(value, 'people', PeopleResponse().toJson());
addDefault(value, 'tags', TagsResponse().toJson());
addDefault(value, 'sharedLinks', SharedLinksResponse().toJson());
addDefault(value, 'cast', CastResponse().toJson());
}
break;
case 'ServerConfigDto':

View File

@ -5,7 +5,8 @@ String t(String key, [Map<String, Object>? args]) {
try {
String message = key.tr();
if (args != null) {
return MessageFormat(message).format(args);
return MessageFormat(message, locale: Intl.defaultLocale ?? 'en')
.format(args);
}
return message;
} catch (e) {

View File

@ -5,6 +5,7 @@ import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/utils/translation.dart';
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
class AlbumThumbnailCard extends ConsumerWidget {
@ -61,28 +62,24 @@ class AlbumThumbnailCard extends ConsumerWidget {
if (album.ownerId == ref.read(currentUserProvider)?.id) {
owner = 'owned'.tr();
} else if (album.ownerName != null) {
owner = 'album_thumbnail_shared_by'
.tr(namedArgs: {'user': album.ownerName!});
owner = t('shared_by_user', {'user': album.ownerName!});
}
}
return RichText(
overflow: TextOverflow.fade,
text: TextSpan(
style: context.textTheme.bodyMedium?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
),
return Text.rich(
TextSpan(
children: [
TextSpan(
text: album.assetCount == 1
? 'album_thumbnail_card_item'.tr()
: 'album_thumbnail_card_items'
.tr(namedArgs: {'count': '${album.assetCount}'}),
text: t('items_count', {'count': album.assetCount}),
),
if (owner != null) const TextSpan(text: ''),
if (owner != null) TextSpan(text: owner),
],
style: context.textTheme.bodyMedium?.copyWith(
color: context.colorScheme.onSurfaceSecondary,
),
),
overflow: TextOverflow.fade,
);
}

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