diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 1d1a6eec16..c9cbf4e7f5 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -75,7 +75,7 @@
{
"label": "Build Immich CLI",
"type": "shell",
- "command": "pnpm --filter cli build:dev"
+ "command": "pnpm --filter @immich/cli build:dev"
}
]
}
diff --git a/.devcontainer/server/container-compose-overrides.yml b/.devcontainer/server/container-compose-overrides.yml
index 5c312efd07..8f9e562e0a 100644
--- a/.devcontainer/server/container-compose-overrides.yml
+++ b/.devcontainer/server/container-compose-overrides.yml
@@ -16,7 +16,7 @@ services:
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
- /etc/localtime:/etc/localtime:ro
- pnpm_store_server:/buildcache/pnpm-store
- - ../plugins:/build/corePlugin
+ - ../packages/plugins:/build/corePlugin
immich-web:
env_file: !reset []
immich-machine-learning:
diff --git a/.dockerignore b/.dockerignore
index f7efb5c56e..4d8e2160c2 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -30,9 +30,7 @@ machine-learning/
misc/
mobile/
-open-api/typescript-sdk/build/
-!open-api/typescript-sdk/package.json
-!open-api/typescript-sdk/package-lock.json
+packages/sdk/build/
server/upload/
server/src/queries
diff --git a/.gitattributes b/.gitattributes
index e1225939b1..f1d1336935 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -24,7 +24,7 @@ mobile/lib/infrastructure/repositories/db.repository.steps.dart linguist-generat
mobile/test/drift/main/generated/** -diff -merge
mobile/test/drift/main/generated/** linguist-generated=true
-open-api/typescript-sdk/fetch-client.ts -diff -merge
-open-api/typescript-sdk/fetch-client.ts linguist-generated=true
+packages/sdk/fetch-client.ts -diff -merge
+packages/sdk/fetch-client.ts linguist-generated=true
*.sh text eol=lf
diff --git a/.github/labeler.yml b/.github/labeler.yml
index d0e4a3097b..824bd5c775 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -1,7 +1,7 @@
cli:
- changed-files:
- any-glob-to-any-file:
- - cli/src/**
+ - packages/cli/src/**
documentation:
- changed-files:
diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml
index 0dfe67401f..f3f254e4be 100644
--- a/.github/workflows/build-mobile.yml
+++ b/.github/workflows/build-mobile.yml
@@ -73,8 +73,8 @@ jobs:
needs: pre-job
permissions:
contents: read
- # Skip when PR from a fork
- if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
+ pull-requests: write
+ if: ${{ github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
runs-on: mich
steps:
@@ -86,11 +86,17 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
- ref: ${{ inputs.ref || github.sha }}
+ ref: ${{ inputs.ref }}
persist-credentials: false
token: ${{ steps.token.outputs.token }}
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
+ with:
+ github_token: ${{ steps.token.outputs.token }}
+
- name: Create the Keystore
+ if: ${{ !github.event.pull_request.head.repo.fork }}
env:
KEY_JKS: ${{ secrets.KEY_JKS }}
working-directory: ./mobile
@@ -113,13 +119,6 @@ jobs:
mobile/.dart_tool
key: build-mobile-gradle-${{ runner.os }}-main
- - name: Setup Flutter SDK
- uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
- with:
- channel: 'stable'
- flutter-version-file: ./mobile/pubspec.yaml
- cache: true
-
- name: Setup Android SDK
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
with:
@@ -130,11 +129,10 @@ jobs:
run: flutter pub get
- name: Generate translation file
- run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
- working-directory: ./mobile
+ run: mise //mobile:codegen:translation
- name: Generate platform APIs
- run: make pigeon
+ run: mise //mobile:codegen:pigeon
working-directory: ./mobile
- name: Build Android App Bundle
@@ -144,20 +142,43 @@ jobs:
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [[ $IS_MAIN == 'true' ]]; then
flutter build apk --release
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
else
- flutter build apk --debug --split-per-abi --target-platform android-arm64
+ flutter build apk --release
fi
- name: Publish Android Artifact
+ id: upload-apk
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-apk-signed
path: mobile/build/app/outputs/flutter-apk/*.apk
+ - name: Comment APK download link on PR
+ if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
+ uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
+ env:
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ APK_URL: ${{ steps.upload-apk.outputs.artifact-url }}
+ with:
+ github-token: ${{ steps.token.outputs.token }}
+ message-id: 'mobile-android-apk'
+ message: |
+ đą **Android release APK (universal)** â `${{ env.HEAD_SHA }}`
+
+ Download: ${{ env.APK_URL }}
+
+
+ QR code
+
+
+
+ Installs as a separate app (applicationId `app.alextran.immich.pr${{ github.event.pull_request.number }}`), so it coexists with the Play Store version and any other PR builds.
+
- name: Save Gradle Cache
id: cache-gradle-save
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -181,6 +202,12 @@ jobs:
runs-on: macos-15
steps:
+ - id: token
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
+ with:
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
+ private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
+
- name: Select Xcode 26
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
@@ -190,24 +217,20 @@ jobs:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
- - name: Setup Flutter SDK
- uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- channel: 'stable'
- flutter-version-file: ./mobile/pubspec.yaml
- cache: true
+ github_token: ${{ steps.token.outputs.token }}
- name: Install Flutter dependencies
working-directory: ./mobile
run: flutter pub get
- name: Generate translation files
- run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
- working-directory: ./mobile
+ run: mise //mobile:codegen:translation
- name: Generate platform APIs
- run: make pigeon
- working-directory: ./mobile
+ run: mise //mobile:codegen:pigeon
- name: Setup Ruby
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml
index 47ac1d3730..07c7505762 100644
--- a/.github/workflows/check-openapi.yml
+++ b/.github/workflows/check-openapi.yml
@@ -24,7 +24,7 @@ jobs:
persist-credentials: false
- name: Check for breaking API changes
- uses: oasdiff/oasdiff-action/breaking@37bf9ff785c7315df88216660826e71be4cc03da # v0.0.44
+ uses: oasdiff/oasdiff-action/breaking@26ccb332c67a45ca649de9faf60552ef1b8260d9 # v0.0.46
with:
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
revision: open-api/immich-openapi-specs.json
diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml
index 8b94ea090b..fd4b7f1abe 100644
--- a/.github/workflows/cli.yml
+++ b/.github/workflows/cli.yml
@@ -3,11 +3,11 @@ on:
push:
branches: [main]
paths:
- - 'cli/**'
+ - 'packages/cli/**'
- '.github/workflows/cli.yml'
pull_request:
paths:
- - 'cli/**'
+ - 'packages/cli/**'
- '.github/workflows/cli.yml'
release:
types: [published]
@@ -28,7 +28,7 @@ jobs:
packages: write
defaults:
run:
- working-directory: ./cli
+ working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
@@ -36,30 +36,20 @@ jobs:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
-
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- node-version-file: './cli/.nvmrc'
- registry-url: 'https://registry.npmjs.org'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
- - name: Setup typescript-sdk
- run: pnpm install && pnpm run build
- working-directory: ./open-api/typescript-sdk
-
- - run: pnpm install --frozen-lockfile
- - run: pnpm build
- - run: pnpm publish --provenance --no-git-checks
+ - name: Publish
if: ${{ github.event_name == 'release' }}
+ run: mise run ci-publish
docker:
name: Docker
@@ -99,7 +89,7 @@ jobs:
- name: Get package version
id: package-version
run: |
- version=$(jq -r '.version' cli/package.json)
+ version=$(jq -r '.version' packages/cli/package.json)
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Generate docker image tags
@@ -117,7 +107,7 @@ jobs:
- name: Build and push image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
- file: cli/Dockerfile
+ file: packages/cli/Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name == 'release' }}
cache-from: type=gha
diff --git a/.github/workflows/close-duplicates.yml b/.github/workflows/close-duplicates.yml
index 55fd5ea0ca..b0b5258048 100644
--- a/.github/workflows/close-duplicates.yml
+++ b/.github/workflows/close-duplicates.yml
@@ -35,7 +35,7 @@ jobs:
needs: [get_body, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' }}
container:
- image: ghcr.io/immich-app/mdq:main@sha256:32abe582452b12dff55055e1d6bc24508a8f17164f9d1831db7bb70953c014c6
+ image: ghcr.io/immich-app/mdq:main@sha256:0a8b8867773a0f8368061f47578603f438349f8f1f28b0e16105f481e5c794e0
outputs:
checked: ${{ steps.get_checkbox.outputs.checked }}
steps:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 827864cec5..f9e6dbfa2d 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -57,7 +57,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
+ uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -70,7 +70,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
+ uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
# âšī¸ Command-line programs to run using the OS shell.
# đ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -83,6 +83,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
+ uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
category: '/language:${{matrix.language}}'
diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml
index 03fae31f11..a85435ea5a 100644
--- a/.github/workflows/docs-build.yml
+++ b/.github/workflows/docs-build.yml
@@ -64,17 +64,11 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- fetch-depth: 0
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
-
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- node-version-file: './docs/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
- name: Run install
run: pnpm install
diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml
index 5204b61d0e..083fa009eb 100644
--- a/.github/workflows/docs-deploy.yml
+++ b/.github/workflows/docs-deploy.yml
@@ -131,7 +131,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
- uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
github_token: ${{ steps.token.outputs.token }}
diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml
index db3d34f22f..4186438d43 100644
--- a/.github/workflows/docs-destroy.yml
+++ b/.github/workflows/docs-destroy.yml
@@ -29,7 +29,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
- uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
github_token: ${{ steps.token.outputs.token }}
diff --git a/.github/workflows/fix-format.yml b/.github/workflows/fix-format.yml
index 661b1e18ad..e718c13792 100644
--- a/.github/workflows/fix-format.yml
+++ b/.github/workflows/fix-format.yml
@@ -14,29 +14,23 @@ jobs:
contents: write
pull-requests: write
steps:
- - name: Generate a token
- id: generate-token
- uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
+ - id: token
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- - name: 'Checkout'
+ - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.ref }}
- token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true
+ token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a # v6.0.4
-
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- node-version-file: './server/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
- name: Fix formatting
run: pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix
diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml
index 5e547fb47a..4df27e581e 100644
--- a/.github/workflows/pr-labeler.yml
+++ b/.github/workflows/pr-labeler.yml
@@ -17,6 +17,6 @@ jobs:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
+ - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with:
repo-token: ${{ steps.token.outputs.token }}
diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml
index 455b9920a3..d4fe794913 100644
--- a/.github/workflows/prepare-release.yml
+++ b/.github/workflows/prepare-release.yml
@@ -48,33 +48,28 @@ jobs:
version: ${{ steps.output.outputs.version }}
permissions: {} # No job-level permissions are needed because it uses the app-token
steps:
- - name: Generate a token
- id: generate-token
- uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
+ - id: token
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- - name: Checkout
+ - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
- token: ${{ steps.generate-token.outputs.token }}
+ token: ${{ steps.token.outputs.token }}
persist-credentials: true
ref: main
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
+ with:
+ github_token: ${{ steps.token.outputs.token }}
+
+ # TODO move to mise
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- - name: Setup pnpm
- uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a # v6.0.4
-
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- with:
- node-version-file: './server/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
-
- name: Bump version
env:
SERVER_BUMP: ${{ inputs.serverBump }}
diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml
index 4b87606bba..9502d940ea 100644
--- a/.github/workflows/sdk.yml
+++ b/.github/workflows/sdk.yml
@@ -14,9 +14,6 @@ jobs:
contents: read
id-token: write
packages: write
- defaults:
- run:
- working-directory: ./open-api/typescript-sdk
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
@@ -24,24 +21,22 @@ jobs:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
-
- # Setup .npmrc file to publish to npm
- - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- node-version-file: './open-api/typescript-sdk/.nvmrc'
- registry-url: 'https://registry.npmjs.org'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
+
- name: Install deps
- run: pnpm install --frozen-lockfile
+ run: pnpm --filter @immich/sdk install --frozen-lockfile
+
- name: Build
- run: pnpm build
+ run: pnpm --filter @immich/sdk build
+
- name: Publish
- run: pnpm publish --provenance --no-git-checks
+ run: pnpm --filter @immich/sdk publish --provenance --no-git-checks
diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml
index 70a7294424..10642fbd11 100644
--- a/.github/workflows/static_analysis.yml
+++ b/.github/workflows/static_analysis.yml
@@ -60,38 +60,30 @@ jobs:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup Flutter SDK
- uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- channel: 'stable'
- flutter-version-file: ./mobile/pubspec.yaml
+ github_token: ${{ steps.token.outputs.token }}
- name: Install dependencies
- run: dart pub get
+ run: flutter pub get
- name: Install dependencies for UI package
- run: dart pub get
+ run: flutter pub get
working-directory: ./mobile/packages/ui
- name: Install dependencies for UI Showcase
- run: dart pub get
+ run: flutter pub get
working-directory: ./mobile/packages/ui/showcase
- - name: Install DCM
- uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1
- with:
- github-token: ${{ steps.token.outputs.token }}
- version: auto
- working-directory: ./mobile
-
- - name: Generate translation file
- run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
+ - name: Generate translation files
+ run: mise //mobile:codegen:translation
- name: Run Build Runner
- run: make build
+ run: mise //mobile:codegen:dart
- name: Generate platform API
- run: make pigeon
+ run: mise //mobile:codegen:pigeon
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
@@ -107,20 +99,16 @@ jobs:
env:
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
run: |
- echo "ERROR: Generated files not up to date! Run 'make build' and 'make pigeon' inside the mobile directory"
+ echo "ERROR: Generated files not up to date! Run 'mise //mobile:codegen:dart' and 'mise //mobile:codegen:pigeon'"
echo "Changed files: ${CHANGED_FILES}"
exit 1
- - name: Run dart analyze
- run: dart analyze --fatal-infos
+ - name: Run analyze
+ run: mise //mobile:analyze
- - name: Run dart format
- run: make format
+ - name: Run format
+ run: mise //mobile:format
# TODO: Re-enable after upgrading custom_lint
# - name: Run dart custom_lint
# run: dart run custom_lint
-
- # TODO: Use https://github.com/CQLabs/dcm-action
- - name: Run DCM
- run: dcm analyze lib --fatal-style --fatal-warnings
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9345f9d0a5..97bccbc9ba 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -33,14 +33,14 @@ jobs:
web:
- 'web/**'
- 'i18n/**'
- - 'open-api/typescript-sdk/**'
+ - 'packages/sdk/**'
- 'pnpm-lock.yaml'
server:
- 'server/**'
- 'pnpm-lock.yaml'
cli:
- - 'cli/**'
- - 'open-api/typescript-sdk/**'
+ - 'packages/cli/**'
+ - 'packages/sdk/**'
- 'pnpm-lock.yaml'
e2e:
- 'e2e/**'
@@ -78,28 +78,14 @@ jobs:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- node-version-file: './server/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
- - name: Run package manager install
- run: pnpm install
- - name: Run linter
- run: pnpm lint
- if: ${{ !cancelled() }}
- - name: Run formatter
- run: pnpm format
- if: ${{ !cancelled() }}
- - name: Run tsc
- run: pnpm check
- if: ${{ !cancelled() }}
- - name: Run small tests & coverage
- run: pnpm test
- if: ${{ !cancelled() }}
+ github_token: ${{ steps.token.outputs.token }}
+
+ - name: Run ci-unit
+ run: mise run ci-unit
+
cli-unit-tests:
name: Unit Test CLI
needs: pre-job
@@ -109,7 +95,7 @@ jobs:
contents: read
defaults:
run:
- working-directory: ./cli
+ working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
@@ -122,31 +108,15 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- node-version-file: './cli/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
- - name: Setup typescript-sdk
- run: pnpm install && pnpm run build
- working-directory: ./open-api/typescript-sdk
- - name: Install deps
- run: pnpm install
- - name: Run linter
- run: pnpm lint
- if: ${{ !cancelled() }}
- - name: Run formatter
- run: pnpm format
- if: ${{ !cancelled() }}
- - name: Run tsc
- run: pnpm check
- if: ${{ !cancelled() }}
- - name: Run unit tests & coverage
- run: pnpm test
- if: ${{ !cancelled() }}
+ github_token: ${{ steps.token.outputs.token }}
+
+ - name: Run ci-unit
+ run: mise run ci-unit
+
cli-unit-tests-win:
name: Unit Test CLI (Windows)
needs: pre-job
@@ -156,7 +126,7 @@ jobs:
contents: read
defaults:
run:
- working-directory: ./cli
+ working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
@@ -169,26 +139,28 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- node-version-file: './cli/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
- - name: Setup typescript-sdk
- run: pnpm install --frozen-lockfile && pnpm build
- working-directory: ./open-api/typescript-sdk
- - name: Install deps
+ github_token: ${{ steps.token.outputs.token }}
+
+ - name: Run setup @immich/sdk
+ run: mise run //:sdk:install && mise run //:sdk:build
+
+ - name: Run pnpm install
run: pnpm install --frozen-lockfile
+
# Skip linter & formatter in Windows test.
+
- name: Run tsc
run: pnpm check
if: ${{ !cancelled() }}
+
- name: Run unit tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
+
web-lint:
name: Lint Web
needs: pre-job
@@ -211,28 +183,22 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- node-version-file: './web/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
- - name: Run setup typescript-sdk
- run: pnpm install --frozen-lockfile && pnpm build
- working-directory: ./open-api/typescript-sdk
+ github_token: ${{ steps.token.outputs.token }}
+
+ - name: Run setup @immich/sdk
+ run: mise run //:sdk:install && mise run //:sdk:build
+
- name: Run pnpm install
- run: pnpm rebuild && pnpm install --frozen-lockfile
+ run: pnpm install --frozen-lockfile
+
- name: Run linter
run: pnpm lint
if: ${{ !cancelled() }}
- - name: Run formatter
- run: pnpm format
- if: ${{ !cancelled() }}
- - name: Run svelte checks
- run: pnpm check:svelte
- if: ${{ !cancelled() }}
+
web-unit-tests:
name: Test Web
needs: pre-job
@@ -255,25 +221,15 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- node-version-file: './web/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
- - name: Run setup typescript-sdk
- run: pnpm install --frozen-lockfile && pnpm build
- working-directory: ./open-api/typescript-sdk
- - name: Run npm install
- run: pnpm install --frozen-lockfile
- - name: Run tsc
- run: pnpm check:typescript
- if: ${{ !cancelled() }}
- - name: Run unit tests & coverage
- run: pnpm test
- if: ${{ !cancelled() }}
+ github_token: ${{ steps.token.outputs.token }}
+
+ - name: Run ci-unit
+ run: mise run ci-unit
+
i18n-tests:
name: Test i18n
needs: pre-job
@@ -293,24 +249,25 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- node-version-file: './web/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
+
- name: Install dependencies
- run: pnpm --filter=immich-i18n install --frozen-lockfile
+ run: pnpm -w install --frozen-lockfile
+
- name: Format
- run: pnpm --filter=immich-i18n format:fix
+ run: pnpm format:fix
+
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
with:
files: |
i18n/**
+
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
@@ -319,6 +276,7 @@ jobs:
echo "ERROR: i18n files not up to date!"
echo "Changed files: ${CHANGED_FILES}"
exit 1
+
e2e-tests-lint:
name: End-to-End Lint
needs: pre-job
@@ -341,30 +299,16 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- node-version-file: './e2e/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
- - name: Run setup typescript-sdk
- run: pnpm install --frozen-lockfile && pnpm build
- working-directory: ./open-api/typescript-sdk
- if: ${{ !cancelled() }}
- - name: Install dependencies
- run: pnpm install --frozen-lockfile
- if: ${{ !cancelled() }}
- - name: Run linter
- run: pnpm lint
- if: ${{ !cancelled() }}
- - name: Run formatter
- run: pnpm format
- if: ${{ !cancelled() }}
- - name: Run tsc
- run: pnpm check
+ github_token: ${{ steps.token.outputs.token }}
+
+ - name: Run ci-unit
+ run: mise run ci-unit
if: ${{ !cancelled() }}
+
server-medium-tests:
name: Medium Tests (Server)
needs: pre-job
@@ -388,23 +332,16 @@ jobs:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- with:
- node-version-file: './server/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+
- name: Setup Mise
- uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
github_token: ${{ steps.token.outputs.token }}
- - name: Run pnpm install
- run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
- - name: Run medium tests
- run: pnpm test:medium
+
+ - name: Run ci-medium
+ run: mise run ci-medium
if: ${{ !cancelled() }}
+
e2e-tests-server-cli:
name: End-to-End Tests (Server & CLI)
needs: pre-job
@@ -431,52 +368,57 @@ jobs:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
+
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
+
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
- node-version-file: './e2e/.nvmrc'
+ node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- - name: Run setup typescript-sdk
- run: pnpm install --frozen-lockfile && pnpm build
- working-directory: ./open-api/typescript-sdk
- if: ${{ !cancelled() }}
+
+ - name: Setup packages
+ run: pnpm --filter "@immich/*" install --frozen-lockfile && pnpm --filter "@immich/*" build
+
- name: Run setup web
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
working-directory: ./web
if: ${{ !cancelled() }}
- - name: Run setup cli
- run: pnpm install --frozen-lockfile && pnpm build
- working-directory: ./cli
- if: ${{ !cancelled() }}
+
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
+
- name: Start Docker Compose
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }}
+
- name: Run e2e tests (api & cli)
env:
VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test
if: ${{ !cancelled() }}
+
- name: Run e2e tests (maintenance)
env:
VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test:maintenance
if: ${{ !cancelled() }}
+
- name: Capture Docker logs
if: always()
run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e
+
- name: Archive Docker logs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: e2e-server-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt
+
e2e-tests-web:
name: End-to-End Tests (Web)
needs: pre-job
@@ -503,70 +445,84 @@ jobs:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
+
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
+
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
- node-version-file: './e2e/.nvmrc'
+ node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- - name: Run setup typescript-sdk
- run: pnpm install --frozen-lockfile && pnpm build
- working-directory: ./open-api/typescript-sdk
+
+ - name: Run setup @immich/sdk
+ run: pnpm --filter @immich/sdk install --frozen-lockfile && pnpm --filter @immich/sdk build
+
if: ${{ !cancelled() }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
+
- name: Install Playwright Browsers
run: pnpm exec playwright install chromium --only-shell
if: ${{ !cancelled() }}
+
- name: Docker build
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }}
+
- name: Run e2e tests (web)
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web
if: ${{ !cancelled() }}
+
- name: Archive e2e test (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure()
with:
name: e2e-web-test-results-${{ matrix.runner }}
path: e2e/playwright-report/
+
- name: Run ui tests (web)
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web:ui
if: ${{ !cancelled() }}
+
- name: Archive ui test (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure()
with:
name: e2e-ui-test-results-${{ matrix.runner }}
path: e2e/playwright-report/
+
- name: Run maintenance tests
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web:maintenance
if: ${{ !cancelled() }}
+
- name: Archive maintenance tests (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure()
with:
name: e2e-maintenance-isolated-test-results-${{ matrix.runner }}
path: e2e/playwright-report/
+
- name: Capture Docker logs
if: always()
run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e
+
- name: Archive Docker logs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: e2e-web-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt
+
success-check-e2e:
name: End-to-End Tests Success
needs: [e2e-tests-server-cli, e2e-tests-web]
@@ -595,17 +551,22 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup Flutter SDK
- uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- channel: 'stable'
- flutter-version-file: ./mobile/pubspec.yaml
- - name: Generate translation file
- run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
+ github_token: ${{ steps.token.outputs.token }}
+
+ - name: Install dependencies
+ run: flutter pub get
working-directory: ./mobile
+
+ - name: Generate translation files
+ run: mise //mobile:codegen:translation
+
- name: Run tests
- working-directory: ./mobile
- run: flutter test -j 1
+ run: mise //mobile:test
+
ml-unit-tests:
name: Unit Test ML
needs: pre-job
@@ -613,9 +574,6 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
- env:
- MISE_CEILING_PATHS: ${{ github.workspace }}
- MISE_TRUSTED_CONFIG_PATHS: ${{ github.workspace }}/machine-learning/mise.toml
defaults:
run:
working-directory: ./machine-learning
@@ -632,21 +590,13 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
- uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
github_token: ${{ steps.token.outputs.token }}
- working_directory: ./machine-learning
- - name: Install dependencies
- run: mise run install --extra cpu
- - name: Lint code
- run: mise run lint --output-format=github
- - name: Format code
- run: mise run format
- - name: Run type checking
- run: mise run check
- - name: Run tests and coverage
- run: mise run test
+ - name: Run ci-unit
+ run: mise run ci-unit
+
github-files-formatting:
name: .github Files Formatting
needs: pre-job
@@ -669,19 +619,19 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- node-version-file: './.github/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
+
- name: Run pnpm install
run: pnpm install --frozen-lockfile
+
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
+
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
@@ -720,29 +670,28 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- node-version-file: './server/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
+
- name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
- - name: Build the app
- run: pnpm --filter immich build
+
- name: Run API generation
- run: ./bin/generate-open-api.sh
+ run: mise //:open-api
working-directory: open-api
+
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
with:
files: |
mobile/openapi
- open-api/typescript-sdk
+ packages/sdk
open-api/immich-openapi-specs.json
+
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
@@ -751,6 +700,7 @@ jobs:
echo "ERROR: Generated files not up to date!"
echo "Changed files: ${CHANGED_FILES}"
exit 1
+
sql-schema-up-to-date:
name: SQL Schema Checks
runs-on: ubuntu-latest
@@ -782,31 +732,35 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
- node-version-file: './server/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
+
- name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
+
- name: Build the app
run: pnpm build
+
- name: Run existing migrations
run: pnpm migrations:run
+
- name: Test npm run schema:reset command works
run: pnpm schema:reset
+
- name: Generate new migrations
continue-on-error: true
run: pnpm migrations:generate src/TestMigration
+
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
with:
files: |
server/src
+
- name: Verify migration files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
@@ -816,16 +770,19 @@ jobs:
echo "Changed files: ${CHANGED_FILES}"
cat ./src/*-TestMigration.ts
exit 1
+
- name: Run SQL generation
- run: pnpm sync:sql
+ run: mise //:sql
env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich
+
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-sql-files
with:
files: |
server/src/queries
+
- name: Verify SQL files have not changed
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
env:
diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml
index b05a20360c..7063820839 100644
--- a/.github/workflows/weblate-lock.yml
+++ b/.github/workflows/weblate-lock.yml
@@ -36,7 +36,7 @@ jobs:
github-token: ${{ steps.token.outputs.token }}
filters: |
i18n:
- - modified: 'i18n/!(en|package)**\.json'
+ - modified: 'i18n/!(en)**\.json'
skip-force-logic: 'true'
enforce-lock:
diff --git a/.gitignore b/.gitignore
index e8fdfa266c..8beeeedfe3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,7 +20,7 @@ mobile/openapi/doc
mobile/openapi/.openapi-generator/FILES
mobile/ios/build
-open-api/typescript-sdk/build
+packages/**/build
mobile/android/fastlane/report.xml
mobile/ios/fastlane/report.xml
diff --git a/.github/.nvmrc b/.nvmrc
similarity index 100%
rename from .github/.nvmrc
rename to .nvmrc
diff --git a/i18n/.prettierrc b/.prettierrc
similarity index 100%
rename from i18n/.prettierrc
rename to .prettierrc
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 9ed2bb77b8..6cdc408fa2 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -23,15 +23,17 @@
"type": "node",
"request": "launch",
"name": "Immich CLI",
- "program": "${workspaceFolder}/cli/dist/index.js",
+ "program": "${workspaceFolder}/packages/cli/dist/index.js",
"args": ["upload", "--help"],
"runtimeArgs": ["--enable-source-maps"],
"console": "integratedTerminal",
- "resolveSourceMapLocations": ["${workspaceFolder}/cli/dist/**/*.js.map"],
+ "resolveSourceMapLocations": [
+ "${workspaceFolder}/packages/cli/dist/**/*.js.map"
+ ],
"sourceMaps": true,
- "outFiles": ["${workspaceFolder}/cli/dist/**/*.js"],
+ "outFiles": ["${workspaceFolder}/packages/cli/dist/**/*.js"],
"skipFiles": ["/**"],
- "preLaunchTask": "Build Immich CLI"
+ "preLaunchTask": "Build @immich/cli"
}
]
}
diff --git a/Makefile b/Makefile
index 4d76913d8f..648aed5120 100644
--- a/Makefile
+++ b/Makefile
@@ -37,105 +37,24 @@ prod-scale:
.PHONY: open-api
open-api:
- cd ./open-api && bash ./bin/generate-open-api.sh
-
-open-api-dart:
- cd ./open-api && bash ./bin/generate-open-api.sh dart
-
-open-api-typescript:
- cd ./open-api && bash ./bin/generate-open-api.sh typescript
+ @printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n"\n\n >&2 && exit 1
sql:
- pnpm --filter immich run sync:sql
+ @printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n"\n\n >&2 && exit 1
-attach-server:
- docker exec -it docker_immich-server_1 sh
renovate:
LOG_LEVEL=debug pnpm exec renovate --platform=local --repository-cache=reset
-# Directories that need to be created for volumes or build output
-VOLUME_DIRS = \
- ./.pnpm-store \
- ./web/.svelte-kit \
- ./web/node_modules \
- ./web/coverage \
- ./e2e/node_modules \
- ./docs/node_modules \
- ./server/node_modules \
- ./open-api/typescript-sdk/node_modules \
- ./.github/node_modules \
- ./node_modules \
- ./cli/node_modules
-
# Include .env file if it exists
-include docker/.env
MODULES = e2e server web cli sdk docs .github
-# directory to package name mapping function
-# cli = @immich/cli
-# docs = documentation
-# e2e = immich-e2e
-# open-api/typescript-sdk = @immich/sdk
-# server = immich
-# web = immich-web
-map-package = $(subst sdk,@immich/sdk,$(subst cli,@immich/cli,$(subst docs,documentation,$(subst e2e,immich-e2e,$(subst server,immich,$(subst web,immich-web,$1))))))
-
-audit-%:
- pnpm --filter $(call map-package,$*) audit fix
-install-%:
- pnpm --filter $(call map-package,$*) install $(if $(FROZEN),--frozen-lockfile) $(if $(OFFLINE),--offline)
-build-cli: build-sdk
-build-web: build-sdk
-build-%: install-%
- pnpm --filter $(call map-package,$*) run build
-format-%:
- pnpm --filter $(call map-package,$*) run format:fix
-lint-%:
- pnpm --filter $(call map-package,$*) run lint:fix
-check-%:
- pnpm --filter $(call map-package,$*) run check
-check-web:
- pnpm --filter immich-web run check:typescript
- pnpm --filter immich-web run check:svelte
-test-%:
- pnpm --filter $(call map-package,$*) run test
test-e2e:
docker compose -f ./e2e/docker-compose.yml build
pnpm --filter immich-e2e run test
pnpm --filter immich-e2e run test:web
-test-medium:
- docker run \
- --rm \
- -v ./server/src:/usr/src/app/src \
- -v ./server/test:/usr/src/app/test \
- -v ./server/vitest.config.medium.mjs:/usr/src/app/vitest.config.medium.mjs \
- -v ./server/tsconfig.json:/usr/src/app/tsconfig.json \
- -e NODE_ENV=development \
- immich-server:latest \
- -c "pnpm test:medium -- --run"
-test-medium-dev:
- docker exec -it immich_server /bin/sh -c "pnpm run test:medium"
-
-install-all:
- pnpm -r --filter '!documentation' install
-
-build-all: $(foreach M,$(filter-out e2e docs .github,$(MODULES)),build-$M) ;
-
-check-all:
- pnpm -r --filter '!documentation' run "/^(check|check\:svelte|check\:typescript)$/"
-lint-all:
- pnpm -r --filter '!documentation' run lint:fix
-format-all:
- pnpm -r --filter '!documentation' run format:fix
-audit-all:
- pnpm -r --filter '!documentation' audit fix
-hygiene-all: audit-all
- pnpm -r --filter '!documentation' run "/(format:fix|check|check:svelte|check:typescript|sql)/"
-
-test-all:
- pnpm -r --filter '!documentation' run "/^test/"
clean:
find . -name "node_modules" -type d -prune -exec rm -rf {} +
@@ -146,7 +65,3 @@ clean:
find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' +
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml down -v --remove-orphans || true
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml down -v --remove-orphans || true
-
-
-setup-server-dev: install-server
-setup-web-dev: install-sdk build-sdk install-web
diff --git a/cli/.nvmrc b/cli/.nvmrc
deleted file mode 100644
index 5bf4400f22..0000000000
--- a/cli/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-24.15.0
diff --git a/cli/Dockerfile b/cli/Dockerfile
deleted file mode 100644
index d56190ee16..0000000000
--- a/cli/Dockerfile
+++ /dev/null
@@ -1,14 +0,0 @@
-FROM node:24.1.0-alpine3.20@sha256:8fe019e0d57dbdce5f5c27c0b63d2775cf34b00e3755a7dea969802d7e0c2b25 AS core
-
-WORKDIR /usr/src/app
-COPY package* pnpm* .pnpmfile.cjs ./
-COPY ./cli ./cli/
-COPY ./open-api/typescript-sdk ./open-api/typescript-sdk/
-RUN corepack enable pnpm && \
- pnpm install --filter @immich/sdk --filter @immich/cli --frozen-lockfile && \
- pnpm --filter @immich/sdk build && \
- pnpm --filter @immich/cli build
-
-WORKDIR /import
-
-ENTRYPOINT ["node", "/usr/src/app/cli/dist"]
diff --git a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl
index 0869dd28bc..7fdd96847c 100644
--- a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl
+++ b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl
@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
- version = "4.52.5"
- constraints = "4.52.5"
+ version = "4.52.7"
+ constraints = "4.52.7"
hashes = [
- "h1:+rfzF+16ZcWZWnTyW/p1HHTzYbPKX8Zt2nIFtR/+f+E=",
- "h1:18bXaaOSq8MWKuMxo/4y7EB7/i7G90y5QsKHZRmkoDo=",
- "h1:4vZVOpKeEQZsF2VrARRZFeL37Ed/gD4rRMtfnvWQres=",
- "h1:BZOsTF83QPKXTAaYqxPKzdl1KRjk/L2qbPpFjM0w28A=",
- "h1:CDuC+HXLvc1z6wkCRsSDcc/+QENIHEtssYshiWg3opA=",
- "h1:DE+YFzLnqSe79pI2R4idRGx5QzLdrA7RXvngTkGfZ30=",
- "h1:DfaJwH3Ml4yrRbdAY4AcDVy0QTQk5T3A622TXzS/u2E=",
- "h1:EIDXP0W3kgIv2pecrFmqtK/DnlqkyckzBzhxKaXU+4A=",
- "h1:EV4kYyaOnwGA0bh/3hU6Ezqnt1PFDxopH7i85e48IzY=",
- "h1:M0iXabfzamU+MPDi0G9XACpbacFKMakmM+Z9HZ8HrsM=",
- "h1:YWmCbGF/KbsrUzcYVBLscwLizidbp95TDQa0N2qpmVo=",
- "h1:cxPcCB5gbrpUO1+IXkQYs1YTY50/0IlApCzGea0cwuQ=",
- "h1:g6DldikTV2HXUu9uoeNY5FuLufgaYWF4ufgZg7wq62s=",
- "h1:oi/Hrx9pwoQ+Z52CBC+rrowVH387EIj0qvnxQgDeI+0=",
- "zh:1a3400cb38863b2585968d1876706bcfc67a148e1318a1d325c6c7704adc999b",
- "zh:4c5062cb9e9da1676f06ae92b8370186d98976cc4c7030d3cd76df12af54282a",
- "zh:52110f493b5f0587ef77a1cfd1a67001fd4c617b14c6502d732ab47352bdc2f7",
- "zh:5aa536f9eaeb43823aaf2aa80e7d39b25ef2b383405ed034aa16a28b446a9238",
- "zh:5cc39459a1c6be8a918f17054e4fbba573825ed5597dcada588fe99614d98a5b",
- "zh:629ae6a7ba298815131da826474d199312d21cec53a4d5ded4fa56a692e6f072",
- "zh:719cc7c75dc1d3eb30c22ff5102a017996d9788b948078c7e1c5b3446aeca661",
- "zh:8698635a3ca04383c1e93b21d6963346bdae54d27177a48e4b1435b7f731731c",
+ "h1:+O72J3QYiZtYmYYZM/Eh0f4NNfl1BvjX1eju43qTQsQ=",
+ "h1:0oqjYIPXcXh7XiDiKI085cHDYQQ5mh8kDl9dmBtvtog=",
+ "h1:4b4ESb87MGv5bnadgYe7sK5rEkKMZhbkQcwPubQTsR4=",
+ "h1:6mTr3eA1Ddb348lLmJuyvn98z4KF+ejqaUEJ76D1rzQ=",
+ "h1:9/3YH+9k9HqsvFtbmBf7SO2+xqZeZrXNKzLkjNuhUEA=",
+ "h1:Jcq4tBWgyH4/2JsojNBSRaN0mcItVMchO+lynonrlqc=",
+ "h1:Y4Vv/2RdP0Q+uxqhOxzOdKxuuEMjXPDcU0vPc5bCQzI=",
+ "h1:a0gW8FBKsbP9Fi0HEDoy49WIbEWVHk9+BR4/iwuBdDQ=",
+ "h1:gElv6iqJtg8OKN77gbw+MjrkrQmJHPkkMEi1J+0xkpU=",
+ "h1:oslXUugD/NQ+duJgT4BhKQyfGbuFOANknMvR73fiOeM=",
+ "h1:pPItIWii5oymR+geZB219ROSPuSODPLTlM4S/u8xLvM=",
+ "h1:u67GWw8GwD9NDlDzp9Y5VRnSQGcCrE8rSpkGPaBpDl0=",
+ "h1:uUUa9dY0XQOycI8pxg16PFFtL0WCTi9uEJz8trTQ7pU=",
+ "h1:y3rV8KF2q6GEMANNlf5EkKJurlfbKlIKpjGcdxoy7pQ=",
+ "zh:0c904ce31a4c6c4a5b3bf7ff1560e77c0cc7e2450c8553ded8e8c90398e1418b",
+ "zh:36183d310c36373fe4cb936b83c595c6fd3b0a94bc7827f28e5789ccbf59752e",
+ "zh:556a568a6f0235e8f41647de9e4d3a1e7b1d6502df8b19b54ec441f1c653ea10",
+ "zh:633ebbd5b0245e75e500ef9be4d9e62288f97e8da3baaa51323892a786d90285",
+ "zh:6acfe60cf52a65ba8f044f748548d2119e7f4fd7f8ebcb14698960d87c68f529",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
- "zh:8a9993f1dcadf1dd6ca43b23348abe374605d29945a2fafc07fb3457644e6a54",
- "zh:b1b9a1e6bcc24d5863a664a411d2dc906373ae7a2399d2d65548ce7377057852",
- "zh:b270184cdeec277218e84b94cb136fead753da717f9b9dc378e51907f3f00bb0",
- "zh:dff2bc10071210181726ce270f954995fe42c696e61e2e8f874021fed02521e5",
- "zh:e8e87b40b6a87dc097b0fdc20d3f725cec0d82abc9cc3755c1f89f8f6e8b0036",
- "zh:ee964a6573d399a5dd22ce328fb38ca1207797a02248f14b2e4913ee390e7803",
+ "zh:904acc31ebb9d6ef68c792074b30532ee61bf515f19e0a3c75b46f126cca1f13",
+ "zh:a1d0a81246afc8750286d3f6fe7a8fbe6460dd2662407b28dbfbabb612e5fa9d",
+ "zh:a41a36fe253fc365fe2b7ffc749624688b2693b4634862fda161179ab100029f",
+ "zh:a7ef269e77ffa8715c8945a2c14322c7ff159ea44c15f62505f3cbb2cae3b32d",
+ "zh:b01aa3bed30610633b762df64332b26f8844a68c3960cebcb30f04918efc67fe",
+ "zh:b069cc2cd18cae10757df3ae030508eac8d55de7e49eda7a5e3e11f2f7fe6455",
+ "zh:b2d2c6313729ebb7465dceece374049e2d08bda34473901be9ff46a8836d42b2",
+ "zh:db0e114edaf4bc2f3d4769958807c83022bfbc619a00bdf4c4bd17faa4ab2d8b",
+ "zh:ecc0aa8b9044f664fd2aaf8fa992d976578f78478980555b4b8f6148e8d1a5fe",
]
}
diff --git a/deployment/modules/cloudflare/docs-release/config.tf b/deployment/modules/cloudflare/docs-release/config.tf
index 63347cf67e..7c59cdd2e3 100644
--- a/deployment/modules/cloudflare/docs-release/config.tf
+++ b/deployment/modules/cloudflare/docs-release/config.tf
@@ -5,7 +5,7 @@ terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
- version = "4.52.5"
+ version = "4.52.7"
}
}
}
diff --git a/deployment/modules/cloudflare/docs/.terraform.lock.hcl b/deployment/modules/cloudflare/docs/.terraform.lock.hcl
index 0869dd28bc..7fdd96847c 100644
--- a/deployment/modules/cloudflare/docs/.terraform.lock.hcl
+++ b/deployment/modules/cloudflare/docs/.terraform.lock.hcl
@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
- version = "4.52.5"
- constraints = "4.52.5"
+ version = "4.52.7"
+ constraints = "4.52.7"
hashes = [
- "h1:+rfzF+16ZcWZWnTyW/p1HHTzYbPKX8Zt2nIFtR/+f+E=",
- "h1:18bXaaOSq8MWKuMxo/4y7EB7/i7G90y5QsKHZRmkoDo=",
- "h1:4vZVOpKeEQZsF2VrARRZFeL37Ed/gD4rRMtfnvWQres=",
- "h1:BZOsTF83QPKXTAaYqxPKzdl1KRjk/L2qbPpFjM0w28A=",
- "h1:CDuC+HXLvc1z6wkCRsSDcc/+QENIHEtssYshiWg3opA=",
- "h1:DE+YFzLnqSe79pI2R4idRGx5QzLdrA7RXvngTkGfZ30=",
- "h1:DfaJwH3Ml4yrRbdAY4AcDVy0QTQk5T3A622TXzS/u2E=",
- "h1:EIDXP0W3kgIv2pecrFmqtK/DnlqkyckzBzhxKaXU+4A=",
- "h1:EV4kYyaOnwGA0bh/3hU6Ezqnt1PFDxopH7i85e48IzY=",
- "h1:M0iXabfzamU+MPDi0G9XACpbacFKMakmM+Z9HZ8HrsM=",
- "h1:YWmCbGF/KbsrUzcYVBLscwLizidbp95TDQa0N2qpmVo=",
- "h1:cxPcCB5gbrpUO1+IXkQYs1YTY50/0IlApCzGea0cwuQ=",
- "h1:g6DldikTV2HXUu9uoeNY5FuLufgaYWF4ufgZg7wq62s=",
- "h1:oi/Hrx9pwoQ+Z52CBC+rrowVH387EIj0qvnxQgDeI+0=",
- "zh:1a3400cb38863b2585968d1876706bcfc67a148e1318a1d325c6c7704adc999b",
- "zh:4c5062cb9e9da1676f06ae92b8370186d98976cc4c7030d3cd76df12af54282a",
- "zh:52110f493b5f0587ef77a1cfd1a67001fd4c617b14c6502d732ab47352bdc2f7",
- "zh:5aa536f9eaeb43823aaf2aa80e7d39b25ef2b383405ed034aa16a28b446a9238",
- "zh:5cc39459a1c6be8a918f17054e4fbba573825ed5597dcada588fe99614d98a5b",
- "zh:629ae6a7ba298815131da826474d199312d21cec53a4d5ded4fa56a692e6f072",
- "zh:719cc7c75dc1d3eb30c22ff5102a017996d9788b948078c7e1c5b3446aeca661",
- "zh:8698635a3ca04383c1e93b21d6963346bdae54d27177a48e4b1435b7f731731c",
+ "h1:+O72J3QYiZtYmYYZM/Eh0f4NNfl1BvjX1eju43qTQsQ=",
+ "h1:0oqjYIPXcXh7XiDiKI085cHDYQQ5mh8kDl9dmBtvtog=",
+ "h1:4b4ESb87MGv5bnadgYe7sK5rEkKMZhbkQcwPubQTsR4=",
+ "h1:6mTr3eA1Ddb348lLmJuyvn98z4KF+ejqaUEJ76D1rzQ=",
+ "h1:9/3YH+9k9HqsvFtbmBf7SO2+xqZeZrXNKzLkjNuhUEA=",
+ "h1:Jcq4tBWgyH4/2JsojNBSRaN0mcItVMchO+lynonrlqc=",
+ "h1:Y4Vv/2RdP0Q+uxqhOxzOdKxuuEMjXPDcU0vPc5bCQzI=",
+ "h1:a0gW8FBKsbP9Fi0HEDoy49WIbEWVHk9+BR4/iwuBdDQ=",
+ "h1:gElv6iqJtg8OKN77gbw+MjrkrQmJHPkkMEi1J+0xkpU=",
+ "h1:oslXUugD/NQ+duJgT4BhKQyfGbuFOANknMvR73fiOeM=",
+ "h1:pPItIWii5oymR+geZB219ROSPuSODPLTlM4S/u8xLvM=",
+ "h1:u67GWw8GwD9NDlDzp9Y5VRnSQGcCrE8rSpkGPaBpDl0=",
+ "h1:uUUa9dY0XQOycI8pxg16PFFtL0WCTi9uEJz8trTQ7pU=",
+ "h1:y3rV8KF2q6GEMANNlf5EkKJurlfbKlIKpjGcdxoy7pQ=",
+ "zh:0c904ce31a4c6c4a5b3bf7ff1560e77c0cc7e2450c8553ded8e8c90398e1418b",
+ "zh:36183d310c36373fe4cb936b83c595c6fd3b0a94bc7827f28e5789ccbf59752e",
+ "zh:556a568a6f0235e8f41647de9e4d3a1e7b1d6502df8b19b54ec441f1c653ea10",
+ "zh:633ebbd5b0245e75e500ef9be4d9e62288f97e8da3baaa51323892a786d90285",
+ "zh:6acfe60cf52a65ba8f044f748548d2119e7f4fd7f8ebcb14698960d87c68f529",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
- "zh:8a9993f1dcadf1dd6ca43b23348abe374605d29945a2fafc07fb3457644e6a54",
- "zh:b1b9a1e6bcc24d5863a664a411d2dc906373ae7a2399d2d65548ce7377057852",
- "zh:b270184cdeec277218e84b94cb136fead753da717f9b9dc378e51907f3f00bb0",
- "zh:dff2bc10071210181726ce270f954995fe42c696e61e2e8f874021fed02521e5",
- "zh:e8e87b40b6a87dc097b0fdc20d3f725cec0d82abc9cc3755c1f89f8f6e8b0036",
- "zh:ee964a6573d399a5dd22ce328fb38ca1207797a02248f14b2e4913ee390e7803",
+ "zh:904acc31ebb9d6ef68c792074b30532ee61bf515f19e0a3c75b46f126cca1f13",
+ "zh:a1d0a81246afc8750286d3f6fe7a8fbe6460dd2662407b28dbfbabb612e5fa9d",
+ "zh:a41a36fe253fc365fe2b7ffc749624688b2693b4634862fda161179ab100029f",
+ "zh:a7ef269e77ffa8715c8945a2c14322c7ff159ea44c15f62505f3cbb2cae3b32d",
+ "zh:b01aa3bed30610633b762df64332b26f8844a68c3960cebcb30f04918efc67fe",
+ "zh:b069cc2cd18cae10757df3ae030508eac8d55de7e49eda7a5e3e11f2f7fe6455",
+ "zh:b2d2c6313729ebb7465dceece374049e2d08bda34473901be9ff46a8836d42b2",
+ "zh:db0e114edaf4bc2f3d4769958807c83022bfbc619a00bdf4c4bd17faa4ab2d8b",
+ "zh:ecc0aa8b9044f664fd2aaf8fa992d976578f78478980555b4b8f6148e8d1a5fe",
]
}
diff --git a/deployment/modules/cloudflare/docs/config.tf b/deployment/modules/cloudflare/docs/config.tf
index 63347cf67e..7c59cdd2e3 100644
--- a/deployment/modules/cloudflare/docs/config.tf
+++ b/deployment/modules/cloudflare/docs/config.tf
@@ -5,7 +5,7 @@ terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
- version = "4.52.5"
+ version = "4.52.7"
}
}
}
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
index 434500b835..dfb876e6bd 100644
--- a/docker/docker-compose.dev.yml
+++ b/docker/docker-compose.dev.yml
@@ -25,10 +25,10 @@ services:
- server_node_modules:/usr/src/app/server/node_modules
- web_node_modules:/usr/src/app/web/node_modules
- github_node_modules:/usr/src/app/.github/node_modules
- - cli_node_modules:/usr/src/app/cli/node_modules
+ - cli_node_modules:/usr/src/app/packages/cli/node_modules
- docs_node_modules:/usr/src/app/docs/node_modules
- e2e_node_modules:/usr/src/app/e2e/node_modules
- - sdk_node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
+ - sdk_node_modules:/usr/src/app/packages/sdk/node_modules
- app_node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage
@@ -74,7 +74,7 @@ services:
- ${UPLOAD_LOCATION}/photos:/data
- /etc/localtime:/etc/localtime:ro
- pnpm_store_server:/buildcache/pnpm-store
- - ../plugins:/build/corePlugin
+ - ../packages/plugins:/build/corePlugin
env_file:
- .env
environment:
@@ -157,7 +157,7 @@ services:
redis:
container_name: immich_redis
- image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
+ image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck:
test: redis-cli ping || exit 1
diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml
index d85186b6f1..24ecb02624 100644
--- a/docker/docker-compose.prod.yml
+++ b/docker/docker-compose.prod.yml
@@ -56,7 +56,7 @@ services:
redis:
container_name: immich_redis
- image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
+ image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck:
test: redis-cli ping || exit 1
restart: always
diff --git a/docker/docker-compose.rootless.yml b/docker/docker-compose.rootless.yml
index c16a623807..3f3e53424b 100644
--- a/docker/docker-compose.rootless.yml
+++ b/docker/docker-compose.rootless.yml
@@ -61,7 +61,7 @@ services:
redis:
container_name: immich_redis
- image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
+ image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
user: '1000:1000'
security_opt:
- no-new-privileges:true
@@ -95,6 +95,3 @@ services:
restart: always
healthcheck:
disable: false
-
-volumes:
- model-cache:
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 610b375011..5f3ad35245 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -49,7 +49,7 @@ services:
redis:
container_name: immich_redis
- image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
+ image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck:
test: redis-cli ping || exit 1
restart: always
diff --git a/docs/.nvmrc b/docs/.nvmrc
deleted file mode 100644
index 5bf4400f22..0000000000
--- a/docs/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-24.15.0
diff --git a/docs/docs/administration/server-commands.md b/docs/docs/administration/server-commands.md
index cbd029296f..6938cfadd6 100644
--- a/docs/docs/administration/server-commands.md
+++ b/docs/docs/administration/server-commands.md
@@ -13,8 +13,11 @@ The `immich-server` docker image comes preinstalled with an administrative CLI (
| `enable-oauth-login` | Enable OAuth login |
| `disable-oauth-login` | Disable OAuth login |
| `list-users` | List Immich users |
+| `grant-admin` | Grant admin privileges to a user (by email) |
+| `revoke-admin` | Revoke admin privileges from a user (by email) |
| `version` | Print Immich version |
| `change-media-location` | Change database file paths to align with a new media location |
+| `schema-check` | Verify database migrations and check for schema drift |
## How to run a command
@@ -102,6 +105,22 @@ immich-admin list-users
]
```
+Grant Admin
+
+```
+immich-admin grant-admin
+? Please enter the user email: user@example.com
+Admin access has been granted to user@example.com
+```
+
+Revoke Admin
+
+```
+immich-admin revoke-admin
+? Please enter the user email: user@example.com
+Admin access has been revoked from user@example.com
+```
+
Print Immich Version
```
@@ -126,3 +145,12 @@ immich-admin change-media-location
Database file paths updated successfully! đ
...
```
+
+Schema Check
+
+```
+immich-admin schema-check
+Migrations are up to date
+
+No schema drift detected
+```
diff --git a/docs/docs/api.md b/docs/docs/api.md
index edf58dc94d..9336fcf40d 100644
--- a/docs/docs/api.md
+++ b/docs/docs/api.md
@@ -10,4 +10,4 @@ OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generato
make open-api
```
-You can find the generated client SDK in the `open-api/typescript-sdk/client` for Typescript SDK and `mobile/openapi` for Dart SDK.
+You can find the generated client SDK in the `packages/sdk/client` for Typescript SDK and `mobile/openapi` for Dart SDK.
diff --git a/docs/docs/developer/devcontainers.md b/docs/docs/developer/devcontainers.md
index 4bd60262ad..99f340c557 100644
--- a/docs/docs/developer/devcontainers.md
+++ b/docs/docs/developer/devcontainers.md
@@ -205,7 +205,7 @@ When the Dev Container starts, it automatically:
1. **Runs post-create script** (`container-server-post-create.sh`):
- Adjusts file permissions for the `node` user
- Installs dependencies: `pnpm install` in all packages
- - Builds TypeScript SDK: `pnpm run build` in `open-api/typescript-sdk`
+ - Builds TypeScript SDK: `pnpm --filter @immich/sdk build`
2. **Starts development servers** via VS Code tasks:
- `Immich API Server (Nest)` - API server with hot-reloading on port 2283
@@ -243,8 +243,8 @@ To connect the mobile app to your Dev Container:
- **Server code** (`/server`): Changes trigger automatic restart
- **Web code** (`/web`): Changes trigger hot module replacement
-- **Database migrations**: Run `pnpm run sync:sql` in the server directory
-- **API changes**: Regenerate TypeScript SDK with `make open-api`
+- **Database migrations**: Run `mise //:sql`
+- **API changes**: Regenerate TypeScript SDK with `mise //:open-api`
## Testing
@@ -252,20 +252,11 @@ To connect the mobile app to your Dev Container:
The Dev Container supports multiple ways to run tests:
-#### Using Make Commands (Recommended)
+#### Using Mise Commands (Recommended)
```bash
# Run tests for specific components
-make test-server # Server unit tests
-make test-web # Web unit tests
-make test-e2e # End-to-end tests
-make test-cli # CLI tests
-
-# Run all tests
-make test-all # Runs tests for all components
-
-# Medium tests (integration tests)
-make test-medium-dev # End-to-end tests
+mise run checklist # in `server/`, `web/`, `packages/cli`
```
#### Using PNPM Directly
@@ -289,48 +280,16 @@ pnpm run test # Run API tests
pnpm run test:web # Run web UI tests
```
-### Code Quality Commands
-
-```bash
-# Linting
-make lint-server # Lint server code
-make lint-web # Lint web code
-make lint-all # Lint all components
-
-# Formatting
-make format-server # Format server code
-make format-web # Format web code
-make format-all # Format all code
-
-# Type checking
-make check-server # Type check server
-make check-web # Type check web
-make check-all # Check all components
-
-# Complete hygiene check
-make hygiene-all # Run lint, format, check, SQL sync, and audit
-```
-
### Additional Make Commands
```bash
-# Build commands
-make build-server # Build server
-make build-web # Build web app
-make build-all # Build everything
-
# API generation
make open-api # Generate OpenAPI specs
make open-api-typescript # Generate TypeScript SDK
make open-api-dart # Generate Dart SDK
# Database
-make sql # Sync database schema
-
-# Dependencies
-make install-server # Install server dependencies
-make install-web # Install web dependencies
-make install-all # Install all dependencies
+mise sql # Sync database schema
```
### Debugging
diff --git a/docs/docs/developer/directories.md b/docs/docs/developer/directories.md
index 409353e2c4..23381946bb 100644
--- a/docs/docs/developer/directories.md
+++ b/docs/docs/developer/directories.md
@@ -10,7 +10,8 @@ Our [GitHub Repository](https://github.com/immich-app/immich) is a [monorepo](ht
| :------------------ | :------------------------------------------------------------------- |
| `.github/` | Github templates and action workflows |
| `.vscode/` | VSCode debug launch profiles |
-| `cli/` | Source code for the work-in-progress CLI rewrite |
+| `packages/cli` | Source code for the CLI |
+| `packages/sdk` | Source code for the generated OpenAPI SDK |
| `docker/` | Docker compose resources for dev, test, production |
| `design/` | Screenshots and logos for the README |
| `docs/` | Source code for the [https://immich.app](https://immich.app) website |
diff --git a/docs/docs/developer/pr-checklist.md b/docs/docs/developer/pr-checklist.md
index e5dc6cc1e5..c4ed44c77b 100644
--- a/docs/docs/developer/pr-checklist.md
+++ b/docs/docs/developer/pr-checklist.md
@@ -34,21 +34,23 @@ Run all web checks with `pnpm run check:all`
Run all server checks with `pnpm run check:all`
:::
-:::info Auto Fix
+:::tip Auto Fix
You can use `pnpm run __:fix` to potentially correct some issues automatically for `pnpm run format` and `lint`.
:::
-## Mobile Checks
+## Mobile Checklist
-The following commands must be executed from within the mobile app directory of the codebase.
+- [ ] `mise //mobile:codegen` (auto-generate files using build_runner)
+- [ ] `mise //mobile:lint` (static analysis via Dart Analyzer and DCM)
+- [ ] `mise //mobile:format` (formatting via Dart Formatter)
+- [ ] `mise //mobile:test` (unit tests)
-- [ ] `make build` (auto-generate files using build_runner)
-- [ ] `make analyze` (static analysis via Dart Analyzer and DCM)
-- [ ] `make format` (formatting via Dart Formatter)
-- [ ] `make test` (unit tests)
+:::tip
+Run all these commands at once with `mise //mobile:checklist`
+:::
-:::info Auto Fix
-You can use `dart fix --apply` and `dcm fix lib` to potentially correct some issues automatically for `make analyze`.
+:::tip Auto Fix
+You can use `mise //mobile:lint-fix` to potentially correct some issues automatically for `mise //mobile:lint`.
:::
## OpenAPI
diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md
index abdb3befbe..c5d782fb52 100644
--- a/docs/docs/developer/setup.md
+++ b/docs/docs/developer/setup.md
@@ -58,7 +58,7 @@ You can access the web from `http://your-machine-ip:3000` or `http://localhost:3
If you only want to do web development connected to an existing, remote backend, follow these steps:
-1. Build the Immich SDK - `cd open-api/typescript-sdk && pnpm i && pnpm run build && cd -`
+1. Build the Immich SDK - `pnpm --filter @immich/sdk install && pnpm --filter @immich/sdk build`
2. Enter the web directory - `cd web/`
3. Install web dependencies - `pnpm i`
4. Start the web development server
diff --git a/docs/docs/developer/testing.md b/docs/docs/developer/testing.md
index d7c9edcd31..219c33d1a1 100644
--- a/docs/docs/developer/testing.md
+++ b/docs/docs/developer/testing.md
@@ -17,15 +17,14 @@ make e2e
Before you can run the tests, you need to run the following commands _once_:
-- `pnpm install` (in `e2e/`)
-- `pnpm run build` (in `cli/`)
-- `make open-api` (in the project root `/`)
+- `pnpm install`
+- `pnpm --filter "@immich/*" build`
+- `mise //:open-api`
Once the test environment is running, the e2e tests can be run via:
```bash
-cd e2e/
-pnpm test
+mise //e2e:test
```
The tests check various things including:
diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md
index 4754497d90..c8ebeffbcd 100644
--- a/docs/docs/install/config-file.md
+++ b/docs/docs/install/config-file.md
@@ -26,7 +26,7 @@ The default configuration looks like this:
},
"ffmpeg": {
"accel": "disabled",
- "accelDecode": false,
+ "accelDecode": true,
"acceptedAudioCodecs": ["aac", "mp3", "opus"],
"acceptedContainers": ["mov", "ogg", "webm"],
"acceptedVideoCodecs": ["h264"],
@@ -264,4 +264,4 @@ volumes:
- ./configuration.yml:${IMMICH_CONFIG_FILE}
```
-::
+:::
diff --git a/docs/package.json b/docs/package.json
index d469d1ffef..e1d26532db 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -56,8 +56,5 @@
},
"engines": {
"node": ">=20"
- },
- "volta": {
- "node": "24.15.0"
}
}
diff --git a/e2e/.nvmrc b/e2e/.nvmrc
deleted file mode 100644
index 5bf4400f22..0000000000
--- a/e2e/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-24.15.0
diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml
index c8a3b975d4..0ccd54cf3f 100644
--- a/e2e/docker-compose.yml
+++ b/e2e/docker-compose.yml
@@ -4,7 +4,7 @@ services:
e2e-auth-server:
container_name: immich-e2e-auth-server
build:
- context: ../e2e-auth-server
+ context: ../packages/e2e-auth-server
ports:
- 2286:2286
@@ -44,7 +44,7 @@ services:
redis:
container_name: immich-e2e-redis
- image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
+ image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck:
test: redis-cli ping || exit 1
diff --git a/e2e/mise.toml b/e2e/mise.toml
index c298115e40..99056f9ead 100644
--- a/e2e/mise.toml
+++ b/e2e/mise.toml
@@ -27,3 +27,18 @@ run = { task = "lint --fix" }
[tasks.check]
env._.path = "./node_modules/.bin"
run = "tsc --noEmit"
+
+
+[tasks.ci-setup]
+depends = ["//:sdk:install", "//:sdk:build", "//cli:install", "//cli:build"]
+run = { task = ":install" }
+
+
+[tasks.ci-unit]
+depends = ["//:sdk:install", "//:sdk:build"]
+run = [
+ { task = ":install" },
+ { task = ":format" },
+ { task = ":lint" },
+ { task = ":check" },
+]
diff --git a/e2e/package.json b/e2e/package.json
index a58c709a6d..00868d001d 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -56,8 +56,5 @@
"utimes": "^5.2.1",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.0"
- },
- "volta": {
- "node": "24.15.0"
}
}
diff --git a/e2e/src/specs/server/api/album.e2e-spec.ts b/e2e/src/specs/server/api/album.e2e-spec.ts
index e1e5178476..55b9c44b70 100644
--- a/e2e/src/specs/server/api/album.e2e-spec.ts
+++ b/e2e/src/specs/server/api/album.e2e-spec.ts
@@ -146,7 +146,7 @@ describe('/albums', () => {
it('should not return shared albums with a deleted owner', async () => {
const { status, body } = await request(app)
- .get('/albums?shared=true')
+ .get('/albums?isShared=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -188,7 +188,7 @@ describe('/albums', () => {
it('should return the album collection including owned and shared', async () => {
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
- expect(body).toHaveLength(4);
+ expect(body).toHaveLength(5);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -219,13 +219,20 @@ describe('/albums', () => {
]),
shared: false,
}),
+ expect.objectContaining({
+ albumName: user2SharedUser,
+ albumUsers: expect.arrayContaining([
+ { role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
+ ]),
+ shared: true,
+ }),
]),
);
});
- it('should return the album collection filtered by shared', async () => {
+ it('should return the album collection filtered by isShared', async () => {
const { status, body } = await request(app)
- .get('/albums?shared=true')
+ .get('/albums?isShared=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
@@ -263,9 +270,9 @@ describe('/albums', () => {
);
});
- it('should return the album collection filtered by NOT shared', async () => {
+ it('should return the album collection filtered by NOT isShared', async () => {
const { status, body } = await request(app)
- .get('/albums?shared=false')
+ .get('/albums?isShared=false')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
@@ -282,6 +289,63 @@ describe('/albums', () => {
);
});
+ it('should return only owned albums when filtered by isOwned=true', async () => {
+ const { status, body } = await request(app)
+ .get('/albums?isOwned=true')
+ .set('Authorization', `Bearer ${user1.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toHaveLength(4);
+ expect(body).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ albumName: user1SharedEditorUser }),
+ expect.objectContaining({ albumName: user1SharedViewerUser }),
+ expect.objectContaining({ albumName: user1SharedLink }),
+ expect.objectContaining({ albumName: user1NotShared }),
+ ]),
+ );
+ });
+
+ it('should return only shared-with-me albums when filtered by isOwned=false', async () => {
+ const { status, body } = await request(app)
+ .get('/albums?isOwned=false')
+ .set('Authorization', `Bearer ${user1.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toHaveLength(1);
+ expect(body).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ albumName: user2SharedUser,
+ albumUsers: expect.arrayContaining([
+ { role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
+ ]),
+ }),
+ ]),
+ );
+ });
+
+ it('should return owned shared-out albums when filtered by isOwned=true&ishared=true', async () => {
+ const { status, body } = await request(app)
+ .get('/albums?isOwned=true&isShared=true')
+ .set('Authorization', `Bearer ${user1.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toHaveLength(3);
+ expect(body).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ albumName: user1SharedEditorUser }),
+ expect.objectContaining({ albumName: user1SharedViewerUser }),
+ expect.objectContaining({ albumName: user1SharedLink }),
+ ]),
+ );
+ });
+
+ it('should return empty list when filtered by isOwned=false&isShared=false', async () => {
+ const { status, body } = await request(app)
+ .get('/albums?isOwned=false&isShared=false')
+ .set('Authorization', `Bearer ${user1.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toHaveLength(0);
+ });
+
it('should return the album collection filtered by assetId', async () => {
const { status, body } = await request(app)
.get(`/albums?assetId=${user1Asset2.id}`)
@@ -290,17 +354,17 @@ describe('/albums', () => {
expect(body).toHaveLength(2);
});
- it('should return the album collection filtered by assetId and ignores shared=true', async () => {
+ it('should return the album collection filtered by assetId and ignores isShared=true', async () => {
const { status, body } = await request(app)
- .get(`/albums?shared=true&assetId=${user1Asset1.id}`)
+ .get(`/albums?isShared=true&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
});
- it('should return the album collection filtered by assetId and ignores shared=false', async () => {
+ it('should return the album collection filtered by assetId and ignores isShared=false', async () => {
const { status, body } = await request(app)
- .get(`/albums?shared=false&assetId=${user1Asset1.id}`)
+ .get(`/albums?isShared=false&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
diff --git a/e2e/src/specs/server/api/asset.e2e-spec.ts b/e2e/src/specs/server/api/asset.e2e-spec.ts
index 3fbacd5bf6..010b096c4d 100644
--- a/e2e/src/specs/server/api/asset.e2e-spec.ts
+++ b/e2e/src/specs/server/api/asset.e2e-spec.ts
@@ -7,7 +7,6 @@ import {
getMyUser,
LoginResponseDto,
SharedLinkType,
- updateConfig,
} from '@immich/sdk';
import { exiftool } from 'exiftool-vendored';
import { DateTime } from 'luxon';
@@ -24,7 +23,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
-const facesAssetDir = `${testAssetDir}/metadata/faces`;
const readTags = async (bytes: Buffer, filename: string) => {
const filepath = join(tempDir, filename);
@@ -185,78 +183,6 @@ describe('/asset', () => {
});
});
- describe('faces', () => {
- const metadataFaceTests = [
- {
- description: 'without orientation',
- filename: 'portrait.jpg',
- },
- {
- description: 'adjusting face regions to orientation',
- filename: 'portrait-orientation-6.jpg',
- },
- ];
- // should produce same resulting face region coordinates for any orientation
- const expectedFaces = [
- {
- name: 'Marie Curie',
- birthDate: null,
- isHidden: false,
- faces: [
- {
- imageHeight: 700,
- imageWidth: 840,
- boundingBoxX1: 261,
- boundingBoxX2: 356,
- boundingBoxY1: 146,
- boundingBoxY2: 284,
- sourceType: 'exif',
- },
- ],
- },
- {
- name: 'Pierre Curie',
- birthDate: null,
- isHidden: false,
- faces: [
- {
- imageHeight: 700,
- imageWidth: 840,
- boundingBoxX1: 536,
- boundingBoxX2: 618,
- boundingBoxY1: 83,
- boundingBoxY2: 252,
- sourceType: 'exif',
- },
- ],
- },
- ];
-
- it.each(metadataFaceTests)('should get the asset faces from $filename $description', async ({ filename }) => {
- const config = await utils.getSystemConfig(admin.accessToken);
- config.metadata.faces.import = true;
- await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
-
- const facesAsset = await utils.createAsset(admin.accessToken, {
- assetData: {
- filename,
- bytes: await readFile(`${facesAssetDir}/${filename}`),
- },
- });
-
- await utils.waitForWebsocketEvent({ event: 'assetUpload', id: facesAsset.id });
-
- const { status, body } = await request(app)
- .get(`/assets/${facesAsset.id}`)
- .set('Authorization', `Bearer ${admin.accessToken}`);
-
- expect(status).toBe(200);
- expect(body.id).toEqual(facesAsset.id);
- const sortedPeople = body.people.toSorted((a: any, b: any) => a.name.localeCompare(b.name));
- expect(sortedPeople).toMatchObject(expectedFaces);
- });
- });
-
it('should work with a shared link', async () => {
const sharedLink = await utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Individual,
diff --git a/e2e/src/specs/server/api/search.e2e-spec.ts b/e2e/src/specs/server/api/search.e2e-spec.ts
index e3e17f67c2..09d33b735b 100644
--- a/e2e/src/specs/server/api/search.e2e-spec.ts
+++ b/e2e/src/specs/server/api/search.e2e-spec.ts
@@ -441,7 +441,18 @@ describe('/search', () => {
.get('/search/explore')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
- expect(body).toEqual([{ fieldName: 'exifInfo.city', items: [] }]);
+ expect(Array.isArray(body)).toBe(true);
+ expect(body).toEqual(expect.arrayContaining([{ fieldName: 'exifInfo.city', items: [] }]));
+ expect(body).toEqual(
+ expect.arrayContaining([
+ {
+ fieldName: 'createdAt',
+ items: expect.arrayContaining([
+ expect.objectContaining({ data: expect.objectContaining({ id: assetLast.id }) }),
+ ]),
+ },
+ ]),
+ );
});
});
diff --git a/e2e/src/specs/server/cli/version.e2e-spec.ts b/e2e/src/specs/server/cli/version.e2e-spec.ts
index 56a0d8b0b1..de03fdf358 100644
--- a/e2e/src/specs/server/cli/version.e2e-spec.ts
+++ b/e2e/src/specs/server/cli/version.e2e-spec.ts
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
import { immichCli } from 'src/utils';
import { describe, expect, it } from 'vitest';
-const pkg = JSON.parse(readFileSync('../cli/package.json', 'utf8'));
+const pkg = JSON.parse(readFileSync('../packages/cli/package.json', 'utf8'));
describe(`immich --version`, () => {
describe('immich --version', () => {
diff --git a/e2e/src/ui/generators/timeline/rest-response.ts b/e2e/src/ui/generators/timeline/rest-response.ts
index 83a60556be..52dfa4c493 100644
--- a/e2e/src/ui/generators/timeline/rest-response.ts
+++ b/e2e/src/ui/generators/timeline/rest-response.ts
@@ -28,6 +28,7 @@ export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetRe
ownerId: [],
ratio: [],
thumbhash: [],
+ createdAt: [],
fileCreatedAt: [],
localOffsetHours: [],
isFavorite: [],
@@ -338,7 +339,6 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
livePhotoVideoId: asset.livePhotoVideoId,
tags: [],
people: [],
- unassignedFaces: [],
stack: asset.stack,
isOffline: false,
hasMetadata: true,
diff --git a/e2e/src/ui/mock-network/base-network.ts b/e2e/src/ui/mock-network/base-network.ts
index 3dc3580396..6680b83dd1 100644
--- a/e2e/src/ui/mock-network/base-network.ts
+++ b/e2e/src/ui/mock-network/base-network.ts
@@ -240,7 +240,8 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
});
});
await context.route('**/api/albums*', async (route, request) => {
- if (request.url().endsWith('albums?shared=true') || request.url().endsWith('albums')) {
+ const url = request.url();
+ if (url.endsWith('albums?isShared=true') || url.endsWith('albums?isOwned=true') || url.endsWith('albums')) {
return route.fulfill({
status: 200,
contentType: 'application/json',
diff --git a/e2e/src/ui/mock-network/broken-asset-network.ts b/e2e/src/ui/mock-network/broken-asset-network.ts
index ce66412e61..2137cdd90f 100644
--- a/e2e/src/ui/mock-network/broken-asset-network.ts
+++ b/e2e/src/ui/mock-network/broken-asset-network.ts
@@ -66,7 +66,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => {
livePhotoVideoId: null,
tags: [],
people: [],
- unassignedFaces: [],
stack: undefined,
isOffline: false,
hasMetadata: true,
diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts
index aa4c3b8499..74c2832c3e 100644
--- a/e2e/src/utils.ts
+++ b/e2e/src/utils.ts
@@ -90,7 +90,7 @@ export const tempDir = tmpdir();
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
export const immichCli = (args: string[]) =>
- executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise;
+ executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../packages/cli' }).promise;
export const dockerExec = (args: string[]) =>
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', args.join(' ')]);
export const immichAdmin = (args: string[]) => dockerExec([`immich-admin ${args.join(' ')}`]);
diff --git a/i18n/en.json b/i18n/en.json
index 76923c6865..ef6bdaf082 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -885,15 +885,13 @@
"cutoff_date_description": "Keep photos from the lastâĻ",
"cutoff_day": "{count, plural, one {day} other {days}}",
"cutoff_year": "{count, plural, one {year} other {years}}",
- "daily_title_text_date": "E, MMM dd",
- "daily_title_text_date_year": "E, MMM dd, yyyy",
"dark": "Dark",
"dark_theme": "Switch to dark theme",
"date": "Date",
"date_after": "Date after",
"date_and_time": "Date and Time",
"date_before": "Date before",
- "date_format": "E, LLL d, y âĸ h:mm a",
+ "date_of_birth": "Date of birth",
"date_of_birth_saved": "Date of birth saved successfully",
"date_range": "Date range",
"day": "Day",
@@ -1403,6 +1401,7 @@
"link_to_oauth": "Link to OAuth",
"linked_oauth_account": "Linked OAuth account",
"list": "List",
+ "live": "Live",
"loading": "Loading",
"loading_search_results_failed": "Loading search results failed",
"local": "Local",
@@ -1582,8 +1581,8 @@
"mobile_app_download_onboarding_note": "Download the companion mobile app using the following options",
"model": "Model",
"month": "Month",
- "monthly_title_text_date_format": "MMMM y",
"more": "More",
+ "motion": "Motion",
"move": "Move",
"move_down": "Move down",
"move_off_locked_folder": "Move out of locked folder",
@@ -1894,6 +1893,7 @@
"remove_assets_title": "Remove assets?",
"remove_custom_date_range": "Remove custom date range",
"remove_deleted_assets": "Remove Deleted Assets",
+ "remove_filter": "Remove filter",
"remove_from_album": "Remove from album",
"remove_from_album_action_prompt": "{count} removed from the album",
"remove_from_favorites": "Remove from favorites",
@@ -2193,6 +2193,7 @@
"show_schema": "Show schema",
"show_search_options": "Show search options",
"show_shared_links": "Show shared links",
+ "show_slideshow_metadata_overlay": "Show image info overlay",
"show_slideshow_transition": "Show slideshow transition",
"show_supporter_badge": "Supporter badge",
"show_supporter_badge_description": "Show a supporter badge",
@@ -2208,6 +2209,9 @@
"skip_to_folders": "Skip to folders",
"skip_to_tags": "Skip to tags",
"slideshow": "Slideshow",
+ "slideshow_metadata_overlay_mode": "Overlay content",
+ "slideshow_metadata_overlay_mode_description_only": "Description only",
+ "slideshow_metadata_overlay_mode_full": "Full",
"slideshow_repeat": "Repeat slideshow",
"slideshow_repeat_description": "Loop back to beginning when slideshow ends",
"slideshow_settings": "Slideshow settings",
diff --git a/i18n/package.json b/i18n/package.json
deleted file mode 100644
index 2b9548ed8b..0000000000
--- a/i18n/package.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "name": "immich-i18n",
- "version": "2.7.5",
- "private": true,
- "scripts": {
- "format": "prettier --cache --check .",
- "format:fix": "prettier --cache --write --list-different ."
- },
- "devDependencies": {
- "prettier": "^3.7.4",
- "prettier-plugin-sort-json": "^4.1.1"
- }
-}
diff --git a/machine-learning/immich_ml/config.py b/machine-learning/immich_ml/config.py
index 8b383f5419..c5ba0bdf0a 100644
--- a/machine-learning/immich_ml/config.py
+++ b/machine-learning/immich_ml/config.py
@@ -32,25 +32,12 @@ class OcrSettings(BaseModel):
class PreloadModelData(BaseModel):
- clip_fallback: str | None = os.getenv("MACHINE_LEARNING_PRELOAD__CLIP", None)
- facial_recognition_fallback: str | None = os.getenv("MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION", None)
- if clip_fallback is not None:
- os.environ["MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL"] = clip_fallback
- os.environ["MACHINE_LEARNING_PRELOAD__CLIP__VISUAL"] = clip_fallback
- del os.environ["MACHINE_LEARNING_PRELOAD__CLIP"]
- if facial_recognition_fallback is not None:
- os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION"] = facial_recognition_fallback
- os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION"] = facial_recognition_fallback
- del os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"]
clip: ClipSettings = ClipSettings()
facial_recognition: FacialRecognitionSettings = FacialRecognitionSettings()
ocr: OcrSettings = OcrSettings()
class MaxBatchSize(BaseModel):
- ocr_fallback: str | None = os.getenv("MACHINE_LEARNING_MAX_BATCH_SIZE__TEXT_RECOGNITION", None)
- if ocr_fallback is not None:
- os.environ["MACHINE_LEARNING_MAX_BATCH_SIZE__OCR"] = ocr_fallback
facial_recognition: int | None = None
ocr: int | None = None
diff --git a/machine-learning/immich_ml/main.py b/machine-learning/immich_ml/main.py
index 4fca7a2e2b..54f9a53930 100644
--- a/machine-learning/immich_ml/main.py
+++ b/machine-learning/immich_ml/main.py
@@ -117,20 +117,6 @@ async def preload_models(preload: PreloadModelData) -> None:
ModelTask.OCR,
)
- if preload.clip_fallback is not None:
- log.warning(
- "Deprecated env variable: 'MACHINE_LEARNING_PRELOAD__CLIP'. "
- "Use 'MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL' and "
- "'MACHINE_LEARNING_PRELOAD__CLIP__VISUAL' instead."
- )
-
- if preload.facial_recognition_fallback is not None:
- log.warning(
- "Deprecated env variable: 'MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION'. "
- "Use 'MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION' and "
- "'MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION' instead."
- )
-
def update_state() -> Iterator[None]:
global active_requests, last_called
diff --git a/machine-learning/mise.toml b/machine-learning/mise.toml
index d83bdcd28b..e5e30c4fc2 100644
--- a/machine-learning/mise.toml
+++ b/machine-learning/mise.toml
@@ -15,4 +15,22 @@ run = "uv run pytest --cov=immich_ml --cov-report term-missing"
run = "uv run ruff format immich_ml"
[tasks.check]
-run = "uv run mypy --strict immich_ml/"
\ No newline at end of file
+run = "uv run mypy --strict immich_ml/"
+
+[tasks.ci-unit]
+run = [
+ { task = ":install --extra cpu" },
+ { task = ":format" },
+ { task = ":lint --output-format=github" },
+ { task = ":check" },
+ { task = ":test" },
+]
+
+[tasks.checklist]
+run = [
+ { task = ":install" },
+ { task = ":format" },
+ { task = ":lint" },
+ { task = ":check" },
+ { task = ":test" },
+]
diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml
index d61df51e38..f706a1f125 100644
--- a/machine-learning/pyproject.toml
+++ b/machine-learning/pyproject.toml
@@ -11,7 +11,7 @@ dependencies = [
"gunicorn>=21.1.0",
"huggingface-hub>=1.0,<2.0",
"insightface>=0.7.3,<1.0",
- "numpy<2.4.0",
+ "numpy>=2.4.0,<3.0",
"opencv-python-headless>=4.7.0.72,<5.0",
"orjson>=3.9.5",
"pillow>=12.2,<13",
diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock
index 2e777db9eb..5623c553df 100644
--- a/machine-learning/uv.lock
+++ b/machine-learning/uv.lock
@@ -1005,7 +1005,7 @@ requires-dist = [
{ name = "gunicorn", specifier = ">=21.1.0" },
{ name = "huggingface-hub", specifier = ">=1.0,<2.0" },
{ name = "insightface", specifier = ">=0.7.3,<1.0" },
- { name = "numpy", specifier = "<2.4.0" },
+ { name = "numpy", specifier = ">=2.4.0,<3.0" },
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.23.2,<2" },
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" },
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.23.2,<2" },
@@ -1558,83 +1558,81 @@ wheels = [
[[package]]
name = "numpy"
-version = "2.3.5"
+version = "2.4.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" },
- { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" },
- { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" },
- { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" },
- { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" },
- { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" },
- { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" },
- { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" },
- { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" },
- { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" },
- { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" },
- { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" },
- { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" },
- { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" },
- { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" },
- { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" },
- { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" },
- { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" },
- { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" },
- { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" },
- { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" },
- { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" },
- { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" },
- { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" },
- { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" },
- { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" },
- { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" },
- { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" },
- { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" },
- { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" },
- { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" },
- { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" },
- { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" },
- { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" },
- { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" },
- { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" },
- { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" },
- { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" },
- { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" },
- { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" },
- { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" },
- { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" },
- { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" },
- { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" },
- { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" },
- { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" },
- { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" },
- { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" },
- { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" },
- { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" },
- { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" },
- { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" },
- { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" },
- { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" },
- { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" },
- { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" },
- { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" },
- { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" },
- { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" },
- { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" },
- { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" },
- { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" },
- { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" },
- { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" },
- { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" },
- { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" },
- { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" },
- { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" },
- { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" },
- { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" },
- { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" },
- { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" },
- { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" },
+ { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" },
+ { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" },
+ { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" },
+ { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" },
+ { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" },
+ { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" },
+ { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" },
+ { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" },
+ { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" },
+ { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" },
+ { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" },
+ { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" },
+ { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" },
+ { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" },
+ { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" },
+ { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" },
+ { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" },
+ { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" },
+ { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" },
+ { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" },
+ { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" },
+ { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" },
+ { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" },
+ { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" },
]
[[package]]
@@ -2314,11 +2312,11 @@ wheels = [
[[package]]
name = "python-multipart"
-version = "0.0.26"
+version = "0.0.27"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/69/9b/f23807317a113dc36e74e75eb265a02dd1a4d9082abc3c1064acd22997c4/python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602", size = 44043, upload-time = "2026-04-27T10:51:26.649Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
+ { url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" },
]
[[package]]
diff --git a/misc/release/pump-version.sh b/misc/release/pump-version.sh
index 6be0ddebb9..39a3364723 100755
--- a/misc/release/pump-version.sh
+++ b/misc/release/pump-version.sh
@@ -64,16 +64,13 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
pnpm version "$NEXT_SERVER" --no-git-tag-version
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix server
- pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix i18n
- pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix cli
+ pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix packages/cli
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix web
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix e2e
- pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix open-api/typescript-sdk
+ pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix packages/sdk
# copy version to open-api spec
- pnpm install --frozen-lockfile --prefix server
- pnpm --prefix server run build
- ( cd ./open-api && bash ./bin/generate-open-api.sh )
+ mise run //:open-api
uv version --directory machine-learning "$NEXT_SERVER"
diff --git a/mise.toml b/mise.toml
index 3fc2d90b5c..f190490f17 100644
--- a/mise.toml
+++ b/mise.toml
@@ -2,9 +2,9 @@ experimental_monorepo_root = true
[monorepo]
config_roots = [
- "plugins",
+ "packages/plugins",
"server",
- "cli",
+ "packages/cli",
"deployment",
"mobile",
"e2e",
@@ -16,16 +16,17 @@ config_roots = [
[tools]
node = "24.15.0"
-flutter = "3.41.7"
+flutter = "3.41.9"
pnpm = "10.33.1"
terragrunt = "1.0.3"
opentofu = "1.11.6"
java = "21.0.2"
+"npm:oazapfts" = "7.5.0"
[tools."github:CQLabs/homebrew-dcm"]
version = "1.37.0"
bin = "dcm"
-postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
+postinstall = "chmod +x \"$MISE_TOOL_INSTALL_PATH/dcm\" || true"
[tools."github:jellyfin/jellyfin-ffmpeg"]
version = "7.1.3-6"
@@ -40,20 +41,43 @@ macos-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
experimental = true
pin = true
+[tasks.open-api-typescript]
+run = [
+ "oazapfts --optimistic --argumentStyle=object --useEnumType --allSchemas open-api/immich-openapi-specs.json packages/sdk/src/fetch-client.ts",
+ { task = "//:sdk:install" },
+ { task = "//:sdk:build" },
+]
+
+[tasks.open-api-dart]
+dir = "open-api"
+run = "bash ./bin/generate-dart-sdk.sh"
+
+[tasks.open-api]
+env = { SHARP_IGNORE_GLOBAL_LIBVIPS = true }
+run = [
+ { task = "//server:install" },
+ { task = "//server:build" },
+ { task = "//server:sync-open-api" },
+ { task = ":open-api-typescript"},
+ { task = ":open-api-dart"},
+]
+
+[tasks.sql]
+dir = "server"
+run = "node ./dist/bin/sync-sql.js"
+
# SDK tasks
[tasks."sdk:install"]
-dir = "open-api/typescript-sdk"
-run = "pnpm install --filter @immich/sdk --frozen-lockfile"
+dir = "packages/sdk"
+run = "pnpm --filter @immich/sdk install --frozen-lockfile"
[tasks."sdk:build"]
-dir = "open-api/typescript-sdk"
-run = "pnpm run build"
+dir = "packages/sdk"
+run = "pnpm build"
# i18n tasks
[tasks."i18n:format"]
-dir = "i18n"
-run = "pnpm run format"
+run = "pnpm format"
[tasks."i18n:format-fix"]
-dir = "i18n"
-run = "pnpm run format:fix"
+run = "pnpm format:fix"
diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml
index fafd1f40ec..7c49052fc2 100644
--- a/mobile/analysis_options.yaml
+++ b/mobile/analysis_options.yaml
@@ -34,6 +34,7 @@ linter:
unrelated_type_equality_checks: true
prefer_const_constructors: true
always_use_package_imports: true
+ always_put_control_body_on_new_line: true
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -50,6 +51,7 @@ analyzer:
# - custom_lint
errors:
unawaited_futures: warning
+ always_put_control_body_on_new_line: warning
custom_lint:
rules:
diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
index e879b54ae5..7e3d67fa81 100644
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -64,8 +64,15 @@ android {
}
release {
- signingConfig signingConfigs.release
+ def hasKeystore = file("../key.jks").exists() && file("../key.jks").length() > 0
+ signingConfig hasKeystore ? signingConfigs.release : signingConfigs.debug
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+
+ def prNumber = System.getenv("PR_NUMBER")
+ if (prNumber) {
+ applicationIdSuffix ".pr${prNumber}"
+ versionNameSuffix "-pr${prNumber}"
+ }
}
}
namespace 'app.alextran.immich'
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt
index 0ae49f87f6..3fcaed34bc 100644
--- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt
@@ -416,12 +416,12 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p
}
}
}
- fun onAndroidUpload(callback: (Result) -> Unit)
+ fun onAndroidUpload(maxMinutesArg: Long?, callback: (Result) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix"
val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
- channel.send(null) {
+ channel.send(listOf(maxMinutesArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt
index 7dce1f6edf..716477904c 100644
--- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt
+++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt
@@ -107,7 +107,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
* This method acts as a bridge between the native Android background task system and Flutter.
*/
override fun onInitialized() {
- flutterApi?.onAndroidUpload { handleHostResult(it) }
+ flutterApi?.onAndroidUpload(maxMinutesArg = 20) { handleHostResult(it) }
}
// TODO: Move this to a separate NotificationManager class
diff --git a/mobile/bin/generate_keys.dart b/mobile/bin/generate_keys.dart
index 3c5c284c3e..a4cf562bcb 100644
--- a/mobile/bin/generate_keys.dart
+++ b/mobile/bin/generate_keys.dart
@@ -217,7 +217,9 @@ List _extractParams(String value) {
final icuType = match.group(2)!;
final icuContent = match.group(3) ?? '';
- if (params.containsKey(name)) continue;
+ if (params.containsKey(name)) {
+ continue;
+ }
String type;
if (icuType == 'plural' || icuType == 'number') {
@@ -238,7 +240,9 @@ List _extractParams(String value) {
for (var i = 0; i < value.length; i++) {
if (value[i] == '{') {
- if (depth == 0) icuStart = i;
+ if (depth == 0) {
+ icuStart = i;
+ }
depth++;
} else if (value[i] == '}') {
depth--;
@@ -256,7 +260,9 @@ List _extractParams(String value) {
for (final match in simpleRegex.allMatches(cleanedValue)) {
final name = match.group(1)!;
- if (params.containsKey(name)) continue;
+ if (params.containsKey(name)) {
+ continue;
+ }
String type;
if (_kIntParamNames.contains(name.toLowerCase())) {
diff --git a/mobile/drift_schemas/main/drift_schema_v25.json b/mobile/drift_schemas/main/drift_schema_v25.json
index 95fa57d4d2..5a3f78aae7 100644
--- a/mobile/drift_schemas/main/drift_schema_v25.json
+++ b/mobile/drift_schemas/main/drift_schema_v25.json
@@ -1003,20 +1003,6 @@
1
],
"type": "index",
- "data": {
- "on": 1,
- "name": "idx_remote_asset_owner_checksum",
- "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)",
- "unique": false,
- "columns": []
- }
- },
- {
- "id": 12,
- "references": [
- 1
- ],
- "type": "index",
"data": {
"on": 1,
"name": "UQ_remote_assets_owner_checksum",
@@ -1026,7 +1012,7 @@
}
},
{
- "id": 13,
+ "id": 12,
"references": [
1
],
@@ -1040,7 +1026,7 @@
}
},
{
- "id": 14,
+ "id": 13,
"references": [
1
],
@@ -1054,7 +1040,7 @@
}
},
{
- "id": 15,
+ "id": 14,
"references": [
1
],
@@ -1067,36 +1053,22 @@
"columns": []
}
},
+ {
+ "id": 15,
+ "references": [
+ 1
+ ],
+ "type": "index",
+ "data": {
+ "on": 1,
+ "name": "idx_remote_asset_owner_visibility_deleted_created",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created\nON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)\n",
+ "unique": false,
+ "columns": []
+ }
+ },
{
"id": 16,
- "references": [
- 1
- ],
- "type": "index",
- "data": {
- "on": 1,
- "name": "idx_remote_asset_local_date_time_day",
- "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))",
- "unique": false,
- "columns": []
- }
- },
- {
- "id": 17,
- "references": [
- 1
- ],
- "type": "index",
- "data": {
- "on": 1,
- "name": "idx_remote_asset_local_date_time_month",
- "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))",
- "unique": false,
- "columns": []
- }
- },
- {
- "id": 18,
"references": [],
"type": "table",
"data": {
@@ -1226,7 +1198,7 @@
}
},
{
- "id": 19,
+ "id": 17,
"references": [
0
],
@@ -1301,7 +1273,7 @@
}
},
{
- "id": 20,
+ "id": 18,
"references": [
0
],
@@ -1388,7 +1360,7 @@
}
},
{
- "id": 21,
+ "id": 19,
"references": [
1
],
@@ -1644,7 +1616,7 @@
}
},
{
- "id": 22,
+ "id": 20,
"references": [
1,
4
@@ -1718,7 +1690,7 @@
}
},
{
- "id": 23,
+ "id": 21,
"references": [
4,
0
@@ -1806,7 +1778,7 @@
}
},
{
- "id": 24,
+ "id": 22,
"references": [
1
],
@@ -1902,7 +1874,7 @@
}
},
{
- "id": 25,
+ "id": 23,
"references": [
0
],
@@ -2066,10 +2038,10 @@
}
},
{
- "id": 26,
+ "id": 24,
"references": [
1,
- 25
+ 23
],
"type": "table",
"data": {
@@ -2140,7 +2112,7 @@
}
},
{
- "id": 27,
+ "id": 25,
"references": [
0
],
@@ -2284,10 +2256,10 @@
}
},
{
- "id": 28,
+ "id": 26,
"references": [
1,
- 27
+ 25
],
"type": "table",
"data": {
@@ -2461,7 +2433,7 @@
}
},
{
- "id": 29,
+ "id": 27,
"references": [],
"type": "table",
"data": {
@@ -2509,7 +2481,7 @@
}
},
{
- "id": 30,
+ "id": 28,
"references": [],
"type": "table",
"data": {
@@ -2684,7 +2656,7 @@
}
},
{
- "id": 31,
+ "id": 29,
"references": [
1
],
@@ -2778,7 +2750,7 @@
}
},
{
- "id": 32,
+ "id": 30,
"references": [],
"type": "table",
"data": {
@@ -2826,13 +2798,13 @@
}
},
{
- "id": 33,
+ "id": 31,
"references": [
- 20
+ 18
],
"type": "index",
"data": {
- "on": 20,
+ "on": 18,
"name": "idx_partner_shared_with_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)",
"unique": false,
@@ -2840,19 +2812,47 @@
}
},
{
- "id": 34,
+ "id": 32,
"references": [
- 21
+ 19
],
"type": "index",
"data": {
- "on": 21,
+ "on": 19,
"name": "idx_lat_lng",
"sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)",
"unique": false,
"columns": []
}
},
+ {
+ "id": 33,
+ "references": [
+ 19
+ ],
+ "type": "index",
+ "data": {
+ "on": 19,
+ "name": "idx_remote_exif_city",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city\nON remote_exif_entity (city) WHERE city IS NOT NULL\n",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 34,
+ "references": [
+ 20
+ ],
+ "type": "index",
+ "data": {
+ "on": 20,
+ "name": "idx_remote_album_asset_album_asset",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
{
"id": 35,
"references": [
@@ -2861,20 +2861,6 @@
"type": "index",
"data": {
"on": 22,
- "name": "idx_remote_album_asset_album_asset",
- "sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)",
- "unique": false,
- "columns": []
- }
- },
- {
- "id": 36,
- "references": [
- 24
- ],
- "type": "index",
- "data": {
- "on": 24,
"name": "idx_remote_asset_cloud_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)",
"unique": false,
@@ -2882,13 +2868,13 @@
}
},
{
- "id": 37,
+ "id": 36,
"references": [
- 27
+ 25
],
"type": "index",
"data": {
- "on": 27,
+ "on": 25,
"name": "idx_person_owner_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)",
"unique": false,
@@ -2896,13 +2882,13 @@
}
},
{
- "id": 38,
+ "id": 37,
"references": [
- 28
+ 26
],
"type": "index",
"data": {
- "on": 28,
+ "on": 26,
"name": "idx_asset_face_person_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)",
"unique": false,
@@ -2910,13 +2896,13 @@
}
},
{
- "id": 39,
+ "id": 38,
"references": [
- 28
+ 26
],
"type": "index",
"data": {
- "on": 28,
+ "on": 26,
"name": "idx_asset_face_asset_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)",
"unique": false,
@@ -2924,13 +2910,27 @@
}
},
{
- "id": 40,
+ "id": 39,
"references": [
- 30
+ 26
],
"type": "index",
"data": {
- "on": 30,
+ "on": 26,
+ "name": "idx_asset_face_visible_person",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person\nON asset_face_entity (person_id, asset_id)\nWHERE is_visible = 1 AND deleted_at IS NULL\n",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 40,
+ "references": [
+ 28
+ ],
+ "type": "index",
+ "data": {
+ "on": 28,
"name": "idx_trashed_local_asset_checksum",
"sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)",
"unique": false,
@@ -2940,11 +2940,11 @@
{
"id": 41,
"references": [
- 30
+ 28
],
"type": "index",
"data": {
- "on": 30,
+ "on": 28,
"name": "idx_trashed_local_asset_album",
"sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)",
"unique": false,
@@ -2954,11 +2954,11 @@
{
"id": 42,
"references": [
- 31
+ 29
],
"type": "index",
"data": {
- "on": 31,
+ "on": 29,
"name": "idx_asset_edit_asset_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)",
"unique": false,
@@ -3066,15 +3066,6 @@
}
]
},
- {
- "name": "idx_remote_asset_owner_checksum",
- "sql": [
- {
- "dialect": "sqlite",
- "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)"
- }
- ]
- },
{
"name": "UQ_remote_assets_owner_checksum",
"sql": [
@@ -3112,20 +3103,11 @@
]
},
{
- "name": "idx_remote_asset_local_date_time_day",
+ "name": "idx_remote_asset_owner_visibility_deleted_created",
"sql": [
{
"dialect": "sqlite",
- "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))"
- }
- ]
- },
- {
- "name": "idx_remote_asset_local_date_time_month",
- "sql": [
- {
- "dialect": "sqlite",
- "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))"
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)"
}
]
},
@@ -3282,6 +3264,15 @@
}
]
},
+ {
+ "name": "idx_remote_exif_city",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL"
+ }
+ ]
+ },
{
"name": "idx_remote_album_asset_album_asset",
"sql": [
@@ -3327,6 +3318,15 @@
}
]
},
+ {
+ "name": "idx_asset_face_visible_person",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL"
+ }
+ ]
+ },
{
"name": "idx_trashed_local_asset_checksum",
"sql": [
diff --git a/mobile/drift_schemas/main/drift_schema_v26.json b/mobile/drift_schemas/main/drift_schema_v26.json
new file mode 100644
index 0000000000..b958bcca43
--- /dev/null
+++ b/mobile/drift_schemas/main/drift_schema_v26.json
@@ -0,0 +1,3368 @@
+{
+ "_meta": {
+ "description": "This file contains a serialized version of schema entities for drift.",
+ "version": "1.3.0"
+ },
+ "options": {
+ "store_date_time_values_as_text": true
+ },
+ "entities": [
+ {
+ "id": 0,
+ "references": [],
+ "type": "table",
+ "data": {
+ "name": "user_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "email",
+ "getter_name": "email",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "has_profile_image",
+ "getter_name": "hasProfileImage",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"has_profile_image\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"has_profile_image\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "profile_changed_at",
+ "getter_name": "profileChangedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "avatar_color",
+ "getter_name": "avatarColor",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AvatarColor.values)",
+ "dart_type_name": "AvatarColor"
+ }
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 1,
+ "references": [
+ 0
+ ],
+ "type": "table",
+ "data": {
+ "name": "remote_asset_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "type",
+ "getter_name": "type",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AssetType.values)",
+ "dart_type_name": "AssetType"
+ }
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "width",
+ "getter_name": "width",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "height",
+ "getter_name": "height",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "duration_ms",
+ "getter_name": "durationMs",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "checksum",
+ "getter_name": "checksum",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_favorite",
+ "getter_name": "isFavorite",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_favorite\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "owner_id",
+ "getter_name": "ownerId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "local_date_time",
+ "getter_name": "localDateTime",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "thumb_hash",
+ "getter_name": "thumbHash",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "deleted_at",
+ "getter_name": "deletedAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "uploaded_at",
+ "getter_name": "uploadedAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "live_photo_video_id",
+ "getter_name": "livePhotoVideoId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "visibility",
+ "getter_name": "visibility",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AssetVisibility.values)",
+ "dart_type_name": "AssetVisibility"
+ }
+ },
+ {
+ "name": "stack_id",
+ "getter_name": "stackId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "library_id",
+ "getter_name": "libraryId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_edited",
+ "getter_name": "isEdited",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_edited\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_edited\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 2,
+ "references": [
+ 0
+ ],
+ "type": "table",
+ "data": {
+ "name": "stack_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "owner_id",
+ "getter_name": "ownerId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "primary_asset_id",
+ "getter_name": "primaryAssetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 3,
+ "references": [],
+ "type": "table",
+ "data": {
+ "name": "local_asset_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "type",
+ "getter_name": "type",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AssetType.values)",
+ "dart_type_name": "AssetType"
+ }
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "width",
+ "getter_name": "width",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "height",
+ "getter_name": "height",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "duration_ms",
+ "getter_name": "durationMs",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "checksum",
+ "getter_name": "checksum",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_favorite",
+ "getter_name": "isFavorite",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_favorite\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "orientation",
+ "getter_name": "orientation",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "i_cloud_id",
+ "getter_name": "iCloudId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "adjustment_time",
+ "getter_name": "adjustmentTime",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "latitude",
+ "getter_name": "latitude",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "longitude",
+ "getter_name": "longitude",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "playback_style",
+ "getter_name": "playbackStyle",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AssetPlaybackStyle.values)",
+ "dart_type_name": "AssetPlaybackStyle"
+ }
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 4,
+ "references": [
+ 1
+ ],
+ "type": "table",
+ "data": {
+ "name": "remote_album_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "description",
+ "getter_name": "description",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('\\'\\'')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "thumbnail_asset_id",
+ "getter_name": "thumbnailAssetId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE SET NULL",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE SET NULL"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "setNull"
+ }
+ }
+ ]
+ },
+ {
+ "name": "is_activity_enabled",
+ "getter_name": "isActivityEnabled",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_activity_enabled\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_activity_enabled\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('1')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "order",
+ "getter_name": "order",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AlbumAssetOrder.values)",
+ "dart_type_name": "AlbumAssetOrder"
+ }
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 5,
+ "references": [
+ 4
+ ],
+ "type": "table",
+ "data": {
+ "name": "local_album_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "backup_selection",
+ "getter_name": "backupSelection",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(BackupSelection.values)",
+ "dart_type_name": "BackupSelection"
+ }
+ },
+ {
+ "name": "is_ios_shared_album",
+ "getter_name": "isIosSharedAlbum",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_ios_shared_album\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_ios_shared_album\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "linked_remote_album_id",
+ "getter_name": "linkedRemoteAlbumId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE SET NULL",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_album_entity (id) ON DELETE SET NULL"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_album_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "setNull"
+ }
+ }
+ ]
+ },
+ {
+ "name": "marker",
+ "getter_name": "marker_",
+ "moor_type": "bool",
+ "nullable": true,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"marker\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"marker\" IN (0, 1))"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 6,
+ "references": [
+ 3,
+ 5
+ ],
+ "type": "table",
+ "data": {
+ "name": "local_album_asset_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "asset_id",
+ "getter_name": "assetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES local_asset_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES local_asset_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "local_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "album_id",
+ "getter_name": "albumId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES local_album_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES local_album_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "local_album_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "marker",
+ "getter_name": "marker_",
+ "moor_type": "bool",
+ "nullable": true,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"marker\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"marker\" IN (0, 1))"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "asset_id",
+ "album_id"
+ ]
+ }
+ },
+ {
+ "id": 7,
+ "references": [
+ 6
+ ],
+ "type": "index",
+ "data": {
+ "on": 6,
+ "name": "idx_local_album_asset_album_asset",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 8,
+ "references": [
+ 3
+ ],
+ "type": "index",
+ "data": {
+ "on": 3,
+ "name": "idx_local_asset_checksum",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 9,
+ "references": [
+ 3
+ ],
+ "type": "index",
+ "data": {
+ "on": 3,
+ "name": "idx_local_asset_cloud_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 10,
+ "references": [
+ 2
+ ],
+ "type": "index",
+ "data": {
+ "on": 2,
+ "name": "idx_stack_primary_asset_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 11,
+ "references": [
+ 1
+ ],
+ "type": "index",
+ "data": {
+ "on": 1,
+ "name": "UQ_remote_assets_owner_checksum",
+ "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n",
+ "unique": true,
+ "columns": []
+ }
+ },
+ {
+ "id": 12,
+ "references": [
+ 1
+ ],
+ "type": "index",
+ "data": {
+ "on": 1,
+ "name": "UQ_remote_assets_owner_library_checksum",
+ "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n",
+ "unique": true,
+ "columns": []
+ }
+ },
+ {
+ "id": 13,
+ "references": [
+ 1
+ ],
+ "type": "index",
+ "data": {
+ "on": 1,
+ "name": "idx_remote_asset_checksum",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 14,
+ "references": [
+ 1
+ ],
+ "type": "index",
+ "data": {
+ "on": 1,
+ "name": "idx_remote_asset_stack_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 15,
+ "references": [
+ 1
+ ],
+ "type": "index",
+ "data": {
+ "on": 1,
+ "name": "idx_remote_asset_owner_visibility_deleted_created",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created\nON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)\n",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 16,
+ "references": [],
+ "type": "table",
+ "data": {
+ "name": "auth_user_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "email",
+ "getter_name": "email",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_admin",
+ "getter_name": "isAdmin",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_admin\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_admin\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "has_profile_image",
+ "getter_name": "hasProfileImage",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"has_profile_image\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"has_profile_image\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "profile_changed_at",
+ "getter_name": "profileChangedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "avatar_color",
+ "getter_name": "avatarColor",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AvatarColor.values)",
+ "dart_type_name": "AvatarColor"
+ }
+ },
+ {
+ "name": "quota_size_in_bytes",
+ "getter_name": "quotaSizeInBytes",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "quota_usage_in_bytes",
+ "getter_name": "quotaUsageInBytes",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "pin_code",
+ "getter_name": "pinCode",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 17,
+ "references": [
+ 0
+ ],
+ "type": "table",
+ "data": {
+ "name": "user_metadata_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "user_id",
+ "getter_name": "userId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "key",
+ "getter_name": "key",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(UserMetadataKey.values)",
+ "dart_type_name": "UserMetadataKey"
+ }
+ },
+ {
+ "name": "value",
+ "getter_name": "value",
+ "moor_type": "blob",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "userMetadataConverter",
+ "dart_type_name": "Map"
+ }
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "user_id",
+ "key"
+ ]
+ }
+ },
+ {
+ "id": 18,
+ "references": [
+ 0
+ ],
+ "type": "table",
+ "data": {
+ "name": "partner_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "shared_by_id",
+ "getter_name": "sharedById",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "shared_with_id",
+ "getter_name": "sharedWithId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "in_timeline",
+ "getter_name": "inTimeline",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"in_timeline\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"in_timeline\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "shared_by_id",
+ "shared_with_id"
+ ]
+ }
+ },
+ {
+ "id": 19,
+ "references": [
+ 1
+ ],
+ "type": "table",
+ "data": {
+ "name": "remote_exif_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "asset_id",
+ "getter_name": "assetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "city",
+ "getter_name": "city",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "state",
+ "getter_name": "state",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "country",
+ "getter_name": "country",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "date_time_original",
+ "getter_name": "dateTimeOriginal",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "description",
+ "getter_name": "description",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "height",
+ "getter_name": "height",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "width",
+ "getter_name": "width",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "exposure_time",
+ "getter_name": "exposureTime",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "f_number",
+ "getter_name": "fNumber",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "file_size",
+ "getter_name": "fileSize",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "focal_length",
+ "getter_name": "focalLength",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "latitude",
+ "getter_name": "latitude",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "longitude",
+ "getter_name": "longitude",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "iso",
+ "getter_name": "iso",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "make",
+ "getter_name": "make",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "model",
+ "getter_name": "model",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "lens",
+ "getter_name": "lens",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "orientation",
+ "getter_name": "orientation",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "time_zone",
+ "getter_name": "timeZone",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "rating",
+ "getter_name": "rating",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "projection_type",
+ "getter_name": "projectionType",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "asset_id"
+ ]
+ }
+ },
+ {
+ "id": 20,
+ "references": [
+ 1,
+ 4
+ ],
+ "type": "table",
+ "data": {
+ "name": "remote_album_asset_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "asset_id",
+ "getter_name": "assetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "album_id",
+ "getter_name": "albumId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_album_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_album_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "asset_id",
+ "album_id"
+ ]
+ }
+ },
+ {
+ "id": 21,
+ "references": [
+ 4,
+ 0
+ ],
+ "type": "table",
+ "data": {
+ "name": "remote_album_user_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "album_id",
+ "getter_name": "albumId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_album_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_album_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "user_id",
+ "getter_name": "userId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "role",
+ "getter_name": "role",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AlbumUserRole.values)",
+ "dart_type_name": "AlbumUserRole"
+ }
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "album_id",
+ "user_id"
+ ]
+ }
+ },
+ {
+ "id": 22,
+ "references": [
+ 1
+ ],
+ "type": "table",
+ "data": {
+ "name": "remote_asset_cloud_id_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "asset_id",
+ "getter_name": "assetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "cloud_id",
+ "getter_name": "cloudId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "adjustment_time",
+ "getter_name": "adjustmentTime",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "latitude",
+ "getter_name": "latitude",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "longitude",
+ "getter_name": "longitude",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "asset_id"
+ ]
+ }
+ },
+ {
+ "id": 23,
+ "references": [
+ 0
+ ],
+ "type": "table",
+ "data": {
+ "name": "memory_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "deleted_at",
+ "getter_name": "deletedAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "owner_id",
+ "getter_name": "ownerId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "type",
+ "getter_name": "type",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(MemoryTypeEnum.values)",
+ "dart_type_name": "MemoryTypeEnum"
+ }
+ },
+ {
+ "name": "data",
+ "getter_name": "data",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_saved",
+ "getter_name": "isSaved",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_saved\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_saved\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "memory_at",
+ "getter_name": "memoryAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "seen_at",
+ "getter_name": "seenAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "show_at",
+ "getter_name": "showAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "hide_at",
+ "getter_name": "hideAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 24,
+ "references": [
+ 1,
+ 23
+ ],
+ "type": "table",
+ "data": {
+ "name": "memory_asset_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "asset_id",
+ "getter_name": "assetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "memory_id",
+ "getter_name": "memoryId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES memory_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES memory_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "memory_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "asset_id",
+ "memory_id"
+ ]
+ }
+ },
+ {
+ "id": 25,
+ "references": [
+ 0
+ ],
+ "type": "table",
+ "data": {
+ "name": "person_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "owner_id",
+ "getter_name": "ownerId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "face_asset_id",
+ "getter_name": "faceAssetId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_favorite",
+ "getter_name": "isFavorite",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_favorite\" IN (0, 1))"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_hidden",
+ "getter_name": "isHidden",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_hidden\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_hidden\" IN (0, 1))"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "color",
+ "getter_name": "color",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "birth_date",
+ "getter_name": "birthDate",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 26,
+ "references": [
+ 1,
+ 25
+ ],
+ "type": "table",
+ "data": {
+ "name": "asset_face_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "asset_id",
+ "getter_name": "assetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "person_id",
+ "getter_name": "personId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES person_entity (id) ON DELETE SET NULL",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES person_entity (id) ON DELETE SET NULL"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "person_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "setNull"
+ }
+ }
+ ]
+ },
+ {
+ "name": "image_width",
+ "getter_name": "imageWidth",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "image_height",
+ "getter_name": "imageHeight",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "bounding_box_x1",
+ "getter_name": "boundingBoxX1",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "bounding_box_y1",
+ "getter_name": "boundingBoxY1",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "bounding_box_x2",
+ "getter_name": "boundingBoxX2",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "bounding_box_y2",
+ "getter_name": "boundingBoxY2",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "source_type",
+ "getter_name": "sourceType",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_visible",
+ "getter_name": "isVisible",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_visible\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_visible\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('1')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "deleted_at",
+ "getter_name": "deletedAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 27,
+ "references": [],
+ "type": "table",
+ "data": {
+ "name": "store_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "string_value",
+ "getter_name": "stringValue",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "int_value",
+ "getter_name": "intValue",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 28,
+ "references": [],
+ "type": "table",
+ "data": {
+ "name": "trashed_local_asset_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "type",
+ "getter_name": "type",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AssetType.values)",
+ "dart_type_name": "AssetType"
+ }
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "width",
+ "getter_name": "width",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "height",
+ "getter_name": "height",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "duration_ms",
+ "getter_name": "durationMs",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "album_id",
+ "getter_name": "albumId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "checksum",
+ "getter_name": "checksum",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_favorite",
+ "getter_name": "isFavorite",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_favorite\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "orientation",
+ "getter_name": "orientation",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "source",
+ "getter_name": "source",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(TrashOrigin.values)",
+ "dart_type_name": "TrashOrigin"
+ }
+ },
+ {
+ "name": "playback_style",
+ "getter_name": "playbackStyle",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AssetPlaybackStyle.values)",
+ "dart_type_name": "AssetPlaybackStyle"
+ }
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id",
+ "album_id"
+ ]
+ }
+ },
+ {
+ "id": 29,
+ "references": [
+ 1
+ ],
+ "type": "table",
+ "data": {
+ "name": "asset_edit_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "asset_id",
+ "getter_name": "assetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "action",
+ "getter_name": "action",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AssetEditAction.values)",
+ "dart_type_name": "AssetEditAction"
+ }
+ },
+ {
+ "name": "parameters",
+ "getter_name": "parameters",
+ "moor_type": "blob",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "editParameterConverter",
+ "dart_type_name": "Map"
+ }
+ },
+ {
+ "name": "sequence",
+ "getter_name": "sequence",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 30,
+ "references": [],
+ "type": "table",
+ "data": {
+ "name": "metadata",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "key",
+ "getter_name": "key",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "value",
+ "getter_name": "value",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "key"
+ ]
+ }
+ },
+ {
+ "id": 31,
+ "references": [
+ 18
+ ],
+ "type": "index",
+ "data": {
+ "on": 18,
+ "name": "idx_partner_shared_with_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 32,
+ "references": [
+ 19
+ ],
+ "type": "index",
+ "data": {
+ "on": 19,
+ "name": "idx_lat_lng",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 33,
+ "references": [
+ 19
+ ],
+ "type": "index",
+ "data": {
+ "on": 19,
+ "name": "idx_remote_exif_city",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city\nON remote_exif_entity (city) WHERE city IS NOT NULL\n",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 34,
+ "references": [
+ 20
+ ],
+ "type": "index",
+ "data": {
+ "on": 20,
+ "name": "idx_remote_album_asset_album_asset",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 35,
+ "references": [
+ 22
+ ],
+ "type": "index",
+ "data": {
+ "on": 22,
+ "name": "idx_remote_asset_cloud_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 36,
+ "references": [
+ 25
+ ],
+ "type": "index",
+ "data": {
+ "on": 25,
+ "name": "idx_person_owner_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 37,
+ "references": [
+ 26
+ ],
+ "type": "index",
+ "data": {
+ "on": 26,
+ "name": "idx_asset_face_person_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 38,
+ "references": [
+ 26
+ ],
+ "type": "index",
+ "data": {
+ "on": 26,
+ "name": "idx_asset_face_asset_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 39,
+ "references": [
+ 26
+ ],
+ "type": "index",
+ "data": {
+ "on": 26,
+ "name": "idx_asset_face_visible_person",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person\nON asset_face_entity (person_id, asset_id)\nWHERE is_visible = 1 AND deleted_at IS NULL\n",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 40,
+ "references": [
+ 28
+ ],
+ "type": "index",
+ "data": {
+ "on": 28,
+ "name": "idx_trashed_local_asset_checksum",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 41,
+ "references": [
+ 28
+ ],
+ "type": "index",
+ "data": {
+ "on": 28,
+ "name": "idx_trashed_local_asset_album",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 42,
+ "references": [
+ 29
+ ],
+ "type": "index",
+ "data": {
+ "on": 29,
+ "name": "idx_asset_edit_asset_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)",
+ "unique": false,
+ "columns": []
+ }
+ }
+ ],
+ "fixed_sql": [
+ {
+ "name": "user_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"user_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"email\" TEXT NOT NULL, \"has_profile_image\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_profile_image\" IN (0, 1)), \"profile_changed_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"avatar_color\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "remote_asset_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"remote_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"checksum\" TEXT NOT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"local_date_time\" TEXT NULL, \"thumb_hash\" TEXT NULL, \"deleted_at\" TEXT NULL, \"uploaded_at\" TEXT NULL, \"live_photo_video_id\" TEXT NULL, \"visibility\" INTEGER NOT NULL, \"stack_id\" TEXT NULL, \"library_id\" TEXT NULL, \"is_edited\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_edited\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "stack_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"stack_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"primary_asset_id\" TEXT NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "local_asset_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"local_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"i_cloud_id\" TEXT NULL, \"adjustment_time\" TEXT NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "remote_album_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"description\" TEXT NOT NULL DEFAULT '', \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"thumbnail_asset_id\" TEXT NULL REFERENCES remote_asset_entity (id) ON DELETE SET NULL, \"is_activity_enabled\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_activity_enabled\" IN (0, 1)), \"order\" INTEGER NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "local_album_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"local_album_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"backup_selection\" INTEGER NOT NULL, \"is_ios_shared_album\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_ios_shared_album\" IN (0, 1)), \"linked_remote_album_id\" TEXT NULL REFERENCES remote_album_entity (id) ON DELETE SET NULL, \"marker\" INTEGER NULL CHECK (\"marker\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "local_album_asset_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"local_album_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES local_asset_entity (id) ON DELETE CASCADE, \"album_id\" TEXT NOT NULL REFERENCES local_album_entity (id) ON DELETE CASCADE, \"marker\" INTEGER NULL CHECK (\"marker\" IN (0, 1)), PRIMARY KEY (\"asset_id\", \"album_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "idx_local_album_asset_album_asset",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_local_asset_checksum",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)"
+ }
+ ]
+ },
+ {
+ "name": "idx_local_asset_cloud_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_stack_primary_asset_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)"
+ }
+ ]
+ },
+ {
+ "name": "UQ_remote_assets_owner_checksum",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)"
+ }
+ ]
+ },
+ {
+ "name": "UQ_remote_assets_owner_library_checksum",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)"
+ }
+ ]
+ },
+ {
+ "name": "idx_remote_asset_checksum",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)"
+ }
+ ]
+ },
+ {
+ "name": "idx_remote_asset_stack_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_remote_asset_owner_visibility_deleted_created",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)"
+ }
+ ]
+ },
+ {
+ "name": "auth_user_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"auth_user_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"email\" TEXT NOT NULL, \"is_admin\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_admin\" IN (0, 1)), \"has_profile_image\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_profile_image\" IN (0, 1)), \"profile_changed_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"avatar_color\" INTEGER NOT NULL, \"quota_size_in_bytes\" INTEGER NOT NULL DEFAULT 0, \"quota_usage_in_bytes\" INTEGER NOT NULL DEFAULT 0, \"pin_code\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "user_metadata_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"user_metadata_entity\" (\"user_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"key\" INTEGER NOT NULL, \"value\" BLOB NOT NULL, PRIMARY KEY (\"user_id\", \"key\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "partner_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"partner_entity\" (\"shared_by_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"shared_with_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"in_timeline\" INTEGER NOT NULL DEFAULT 0 CHECK (\"in_timeline\" IN (0, 1)), PRIMARY KEY (\"shared_by_id\", \"shared_with_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "remote_exif_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"remote_exif_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"city\" TEXT NULL, \"state\" TEXT NULL, \"country\" TEXT NULL, \"date_time_original\" TEXT NULL, \"description\" TEXT NULL, \"height\" INTEGER NULL, \"width\" INTEGER NULL, \"exposure_time\" TEXT NULL, \"f_number\" REAL NULL, \"file_size\" INTEGER NULL, \"focal_length\" REAL NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, \"iso\" INTEGER NULL, \"make\" TEXT NULL, \"model\" TEXT NULL, \"lens\" TEXT NULL, \"orientation\" TEXT NULL, \"time_zone\" TEXT NULL, \"rating\" INTEGER NULL, \"projection_type\" TEXT NULL, PRIMARY KEY (\"asset_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "remote_album_asset_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"album_id\" TEXT NOT NULL REFERENCES remote_album_entity (id) ON DELETE CASCADE, PRIMARY KEY (\"asset_id\", \"album_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "remote_album_user_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_user_entity\" (\"album_id\" TEXT NOT NULL REFERENCES remote_album_entity (id) ON DELETE CASCADE, \"user_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"role\" INTEGER NOT NULL, PRIMARY KEY (\"album_id\", \"user_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "remote_asset_cloud_id_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"remote_asset_cloud_id_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"cloud_id\" TEXT NULL, \"created_at\" TEXT NULL, \"adjustment_time\" TEXT NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, PRIMARY KEY (\"asset_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "memory_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"memory_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"deleted_at\" TEXT NULL, \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"type\" INTEGER NOT NULL, \"data\" TEXT NOT NULL, \"is_saved\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_saved\" IN (0, 1)), \"memory_at\" TEXT NOT NULL, \"seen_at\" TEXT NULL, \"show_at\" TEXT NULL, \"hide_at\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "memory_asset_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"memory_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"memory_id\" TEXT NOT NULL REFERENCES memory_entity (id) ON DELETE CASCADE, PRIMARY KEY (\"asset_id\", \"memory_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "person_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"person_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"name\" TEXT NOT NULL, \"face_asset_id\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL CHECK (\"is_favorite\" IN (0, 1)), \"is_hidden\" INTEGER NOT NULL CHECK (\"is_hidden\" IN (0, 1)), \"color\" TEXT NULL, \"birth_date\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "asset_face_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"asset_face_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"person_id\" TEXT NULL REFERENCES person_entity (id) ON DELETE SET NULL, \"image_width\" INTEGER NOT NULL, \"image_height\" INTEGER NOT NULL, \"bounding_box_x1\" INTEGER NOT NULL, \"bounding_box_y1\" INTEGER NOT NULL, \"bounding_box_x2\" INTEGER NOT NULL, \"bounding_box_y2\" INTEGER NOT NULL, \"source_type\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_visible\" IN (0, 1)), \"deleted_at\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "store_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"store_entity\" (\"id\" INTEGER NOT NULL, \"string_value\" TEXT NULL, \"int_value\" INTEGER NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "trashed_local_asset_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"trashed_local_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"album_id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"source\" INTEGER NOT NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\", \"album_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "asset_edit_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"asset_edit_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"action\" INTEGER NOT NULL, \"parameters\" BLOB NOT NULL, \"sequence\" INTEGER NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "metadata",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"metadata\" (\"key\" TEXT NOT NULL, \"value\" TEXT NOT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), PRIMARY KEY (\"key\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "idx_partner_shared_with_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_lat_lng",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)"
+ }
+ ]
+ },
+ {
+ "name": "idx_remote_exif_city",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL"
+ }
+ ]
+ },
+ {
+ "name": "idx_remote_album_asset_album_asset",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_remote_asset_cloud_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_person_owner_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_asset_face_person_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_asset_face_asset_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_asset_face_visible_person",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL"
+ }
+ ]
+ },
+ {
+ "name": "idx_trashed_local_asset_checksum",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)"
+ }
+ ]
+ },
+ {
+ "name": "idx_trashed_local_asset_album",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_asset_edit_asset_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/mobile/ios/Runner/Background/BackgroundWorker.g.swift b/mobile/ios/Runner/Background/BackgroundWorker.g.swift
index 40553441a6..bd01e953f9 100644
--- a/mobile/ios/Runner/Background/BackgroundWorker.g.swift
+++ b/mobile/ios/Runner/Background/BackgroundWorker.g.swift
@@ -348,7 +348,7 @@ class BackgroundWorkerBgHostApiSetup {
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
protocol BackgroundWorkerFlutterApiProtocol {
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void)
- func onAndroidUpload(completion: @escaping (Result) -> Void)
+ func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result) -> Void)
func cancel(completion: @escaping (Result) -> Void)
}
class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
@@ -379,10 +379,10 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
}
}
}
- func onAndroidUpload(completion: @escaping (Result) -> Void) {
+ func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result) -> Void) {
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)"
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
- channel.sendMessage(nil) { response in
+ channel.sendMessage([maxMinutesArg] as [Any?]) { response in
guard let listResponse = response as? [Any?] else {
completion(.failure(createConnectionError(withChannelName: channelName)))
return
diff --git a/mobile/lib/domain/models/album/album.model.dart b/mobile/lib/domain/models/album/album.model.dart
index ef67d729ec..63f4ed6be3 100644
--- a/mobile/lib/domain/models/album/album.model.dart
+++ b/mobile/lib/domain/models/album/album.model.dart
@@ -61,8 +61,12 @@ class RemoteAlbum {
@override
bool operator ==(Object other) {
- if (other is! RemoteAlbum) return false;
- if (identical(this, other)) return true;
+ if (other is! RemoteAlbum) {
+ return false;
+ }
+ if (identical(this, other)) {
+ return true;
+ }
return id == other.id &&
name == other.name &&
ownerId == other.ownerId &&
diff --git a/mobile/lib/domain/models/album/local_album.model.dart b/mobile/lib/domain/models/album/local_album.model.dart
index ea06118aa1..9e8521fa02 100644
--- a/mobile/lib/domain/models/album/local_album.model.dart
+++ b/mobile/lib/domain/models/album/local_album.model.dart
@@ -49,8 +49,12 @@ class LocalAlbum {
@override
bool operator ==(Object other) {
- if (other is! LocalAlbum) return false;
- if (identical(this, other)) return true;
+ if (other is! LocalAlbum) {
+ return false;
+ }
+ if (identical(this, other)) {
+ return true;
+ }
return other.id == id &&
other.name == name &&
diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart
index 15f705c65b..418b124930 100644
--- a/mobile/lib/domain/models/asset/base_asset.model.dart
+++ b/mobile/lib/domain/models/asset/base_asset.model.dart
@@ -51,12 +51,18 @@ sealed class BaseAsset {
bool get isAnimatedImage => playbackStyle == AssetPlaybackStyle.imageAnimated;
AssetPlaybackStyle get playbackStyle {
- if (isVideo) return AssetPlaybackStyle.video;
- if (isMotionPhoto) return AssetPlaybackStyle.livePhoto;
+ if (isVideo) {
+ return AssetPlaybackStyle.video;
+ }
+ if (isMotionPhoto) {
+ return AssetPlaybackStyle.livePhoto;
+ }
if (isImage && durationMs != null && durationMs! > 0) {
return AssetPlaybackStyle.imageAnimated;
}
- if (isImage) return AssetPlaybackStyle.image;
+ if (isImage) {
+ return AssetPlaybackStyle.image;
+ }
return AssetPlaybackStyle.unknown;
}
@@ -98,7 +104,9 @@ sealed class BaseAsset {
@override
bool operator ==(Object other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
if (other is BaseAsset) {
return name == other.name &&
type == other.type &&
diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart
index 04aa6cd846..04f0ae6c8c 100644
--- a/mobile/lib/domain/models/asset/local_asset.model.dart
+++ b/mobile/lib/domain/models/asset/local_asset.model.dart
@@ -74,8 +74,12 @@ class LocalAsset extends BaseAsset {
// Not checking for remoteId here
@override
bool operator ==(Object other) {
- if (other is! LocalAsset) return false;
- if (identical(this, other)) return true;
+ if (other is! LocalAsset) {
+ return false;
+ }
+ if (identical(this, other)) {
+ return true;
+ }
return super == other &&
id == other.id &&
cloudId == other.cloudId &&
diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart
index 36dc6242e1..a810877dcc 100644
--- a/mobile/lib/domain/models/asset/remote_asset.model.dart
+++ b/mobile/lib/domain/models/asset/remote_asset.model.dart
@@ -10,6 +10,7 @@ class RemoteAsset extends BaseAsset {
final AssetVisibility visibility;
final String ownerId;
final String? stackId;
+ final DateTime? uploadedAt;
const RemoteAsset({
required this.id,
@@ -20,6 +21,7 @@ class RemoteAsset extends BaseAsset {
required super.type,
required super.createdAt,
required super.updatedAt,
+ this.uploadedAt,
super.width,
super.height,
super.durationMs,
@@ -55,6 +57,7 @@ class RemoteAsset extends BaseAsset {
type: $type,
createdAt: $createdAt,
updatedAt: $updatedAt,
+ uploadedAt: ${uploadedAt ?? ""},
width: ${width ?? ""},
height: ${height ?? ""},
durationMs: ${durationMs ?? ""},
@@ -71,14 +74,19 @@ class RemoteAsset extends BaseAsset {
// Not checking for localId here
@override
bool operator ==(Object other) {
- if (other is! RemoteAsset) return false;
- if (identical(this, other)) return true;
+ if (other is! RemoteAsset) {
+ return false;
+ }
+ if (identical(this, other)) {
+ return true;
+ }
return super == other &&
id == other.id &&
ownerId == other.ownerId &&
thumbHash == other.thumbHash &&
visibility == other.visibility &&
- stackId == other.stackId;
+ stackId == other.stackId &&
+ uploadedAt == other.uploadedAt;
}
@override
@@ -89,7 +97,8 @@ class RemoteAsset extends BaseAsset {
localId.hashCode ^
thumbHash.hashCode ^
visibility.hashCode ^
- stackId.hashCode;
+ stackId.hashCode ^
+ uploadedAt.hashCode;
RemoteAsset copyWith({
String? id,
@@ -100,6 +109,7 @@ class RemoteAsset extends BaseAsset {
AssetType? type,
DateTime? createdAt,
DateTime? updatedAt,
+ DateTime? uploadedAt,
int? width,
int? height,
int? durationMs,
@@ -119,6 +129,7 @@ class RemoteAsset extends BaseAsset {
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
+ uploadedAt: uploadedAt ?? this.uploadedAt,
width: width ?? this.width,
height: height ?? this.height,
durationMs: durationMs ?? this.durationMs,
@@ -144,6 +155,7 @@ class RemoteAssetExif extends RemoteAsset {
required super.type,
required super.createdAt,
required super.updatedAt,
+ super.uploadedAt,
super.width,
super.height,
super.durationMs,
@@ -158,8 +170,12 @@ class RemoteAssetExif extends RemoteAsset {
@override
bool operator ==(Object other) {
- if (other is! RemoteAssetExif) return false;
- if (identical(this, other)) return true;
+ if (other is! RemoteAssetExif) {
+ return false;
+ }
+ if (identical(this, other)) {
+ return true;
+ }
return super == other && exifInfo == other.exifInfo;
}
@@ -176,6 +192,7 @@ class RemoteAssetExif extends RemoteAsset {
AssetType? type,
DateTime? createdAt,
DateTime? updatedAt,
+ DateTime? uploadedAt,
int? width,
int? height,
int? durationMs,
@@ -196,6 +213,7 @@ class RemoteAssetExif extends RemoteAsset {
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
+ uploadedAt: uploadedAt ?? this.uploadedAt,
width: width ?? this.width,
height: height ?? this.height,
durationMs: durationMs ?? this.durationMs,
diff --git a/mobile/lib/domain/models/asset_face.model.dart b/mobile/lib/domain/models/asset_face.model.dart
index f432b923e3..1388836946 100644
--- a/mobile/lib/domain/models/asset_face.model.dart
+++ b/mobile/lib/domain/models/asset_face.model.dart
@@ -68,7 +68,9 @@ class AssetFace {
@override
bool operator ==(covariant AssetFace other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.id == id &&
other.assetId == assetId &&
diff --git a/mobile/lib/domain/models/config/app_config.dart b/mobile/lib/domain/models/config/app_config.dart
index 3f0ca91db7..beca1c21e7 100644
--- a/mobile/lib/domain/models/config/app_config.dart
+++ b/mobile/lib/domain/models/config/app_config.dart
@@ -1,18 +1,58 @@
+import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
+import 'package:immich_mobile/domain/models/config/image_config.dart';
+import 'package:immich_mobile/domain/models/config/map_config.dart';
import 'package:immich_mobile/domain/models/config/theme_config.dart';
+import 'package:immich_mobile/domain/models/config/timeline_config.dart';
+import 'package:immich_mobile/domain/models/config/viewer_config.dart';
class AppConfig {
final ThemeConfig theme;
+ final CleanupConfig cleanup;
+ final MapConfig map;
+ final TimelineConfig timeline;
+ final ImageConfig image;
+ final ViewerConfig viewer;
- const AppConfig({this.theme = const ThemeConfig()});
+ const AppConfig({
+ this.theme = const .new(),
+ this.cleanup = const .new(),
+ this.map = const .new(),
+ this.timeline = const .new(),
+ this.image = const .new(),
+ this.viewer = const .new(),
+ });
- AppConfig copyWith({ThemeConfig? theme}) => .new(theme: theme ?? this.theme);
+ AppConfig copyWith({
+ ThemeConfig? theme,
+ CleanupConfig? cleanup,
+ MapConfig? map,
+ TimelineConfig? timeline,
+ ImageConfig? image,
+ ViewerConfig? viewer,
+ }) => .new(
+ theme: theme ?? this.theme,
+ cleanup: cleanup ?? this.cleanup,
+ map: map ?? this.map,
+ timeline: timeline ?? this.timeline,
+ image: image ?? this.image,
+ viewer: viewer ?? this.viewer,
+ );
@override
- bool operator ==(Object other) => identical(this, other) || (other is AppConfig && other.theme == theme);
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is AppConfig &&
+ other.theme == theme &&
+ other.cleanup == cleanup &&
+ other.map == map &&
+ other.timeline == timeline &&
+ other.image == image &&
+ other.viewer == viewer);
@override
- int get hashCode => theme.hashCode;
+ int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer);
@override
- String toString() => 'AppConfig(theme: $theme)';
+ String toString() =>
+ 'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer)';
}
diff --git a/mobile/lib/domain/models/config/cleanup_config.dart b/mobile/lib/domain/models/config/cleanup_config.dart
new file mode 100644
index 0000000000..4b34814492
--- /dev/null
+++ b/mobile/lib/domain/models/config/cleanup_config.dart
@@ -0,0 +1,48 @@
+import 'package:immich_mobile/constants/enums.dart';
+
+class CleanupConfig {
+ final bool keepFavorites;
+ final AssetKeepType keepMediaType;
+ final List keepAlbumIds;
+ final int cutoffDaysAgo;
+ final bool defaultsInitialized;
+
+ const CleanupConfig({
+ this.keepFavorites = true,
+ this.keepMediaType = AssetKeepType.none,
+ this.keepAlbumIds = const [],
+ this.cutoffDaysAgo = -1,
+ this.defaultsInitialized = false,
+ });
+
+ CleanupConfig copyWith({
+ bool? keepFavorites,
+ AssetKeepType? keepMediaType,
+ List? keepAlbumIds,
+ int? cutoffDaysAgo,
+ bool? defaultsInitialized,
+ }) => .new(
+ keepFavorites: keepFavorites ?? this.keepFavorites,
+ keepMediaType: keepMediaType ?? this.keepMediaType,
+ keepAlbumIds: keepAlbumIds ?? this.keepAlbumIds,
+ cutoffDaysAgo: cutoffDaysAgo ?? this.cutoffDaysAgo,
+ defaultsInitialized: defaultsInitialized ?? this.defaultsInitialized,
+ );
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is CleanupConfig &&
+ other.keepFavorites == keepFavorites &&
+ other.keepMediaType == keepMediaType &&
+ other.keepAlbumIds == keepAlbumIds &&
+ other.cutoffDaysAgo == cutoffDaysAgo &&
+ other.defaultsInitialized == defaultsInitialized);
+
+ @override
+ int get hashCode => Object.hash(keepFavorites, keepMediaType, keepAlbumIds, cutoffDaysAgo, defaultsInitialized);
+
+ @override
+ String toString() =>
+ 'CleanupConfig(keepFavorites: $keepFavorites, keepMediaType: $keepMediaType, keepAlbumIds: $keepAlbumIds, cutoffDaysAgo: $cutoffDaysAgo, defaultsInitialized: $defaultsInitialized)';
+}
diff --git a/mobile/lib/domain/models/config/image_config.dart b/mobile/lib/domain/models/config/image_config.dart
new file mode 100644
index 0000000000..8410a9010b
--- /dev/null
+++ b/mobile/lib/domain/models/config/image_config.dart
@@ -0,0 +1,20 @@
+class ImageConfig {
+ final bool preferRemote;
+ final bool loadOriginal;
+
+ const ImageConfig({this.preferRemote = false, this.loadOriginal = false});
+
+ ImageConfig copyWith({bool? preferRemote, bool? loadOriginal}) =>
+ ImageConfig(preferRemote: preferRemote ?? this.preferRemote, loadOriginal: loadOriginal ?? this.loadOriginal);
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is ImageConfig && other.preferRemote == preferRemote && other.loadOriginal == loadOriginal);
+
+ @override
+ int get hashCode => Object.hash(preferRemote, loadOriginal);
+
+ @override
+ String toString() => 'ImageConfig(preferRemoteImage: $preferRemote, loadOriginal: $loadOriginal)';
+}
diff --git a/mobile/lib/domain/models/config/map_config.dart b/mobile/lib/domain/models/config/map_config.dart
new file mode 100644
index 0000000000..e37ab0f431
--- /dev/null
+++ b/mobile/lib/domain/models/config/map_config.dart
@@ -0,0 +1,48 @@
+import 'package:flutter/material.dart';
+
+class MapConfig {
+ final int relativeDays;
+ final bool favoritesOnly;
+ final bool includeArchived;
+ final ThemeMode themeMode;
+ final bool withPartners;
+
+ const MapConfig({
+ this.relativeDays = 0,
+ this.favoritesOnly = false,
+ this.includeArchived = false,
+ this.themeMode = ThemeMode.system,
+ this.withPartners = false,
+ });
+
+ MapConfig copyWith({
+ int? relativeDays,
+ bool? favoritesOnly,
+ bool? includeArchived,
+ ThemeMode? themeMode,
+ bool? withPartners,
+ }) => MapConfig(
+ relativeDays: relativeDays ?? this.relativeDays,
+ favoritesOnly: favoritesOnly ?? this.favoritesOnly,
+ includeArchived: includeArchived ?? this.includeArchived,
+ themeMode: themeMode ?? this.themeMode,
+ withPartners: withPartners ?? this.withPartners,
+ );
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is MapConfig &&
+ other.relativeDays == relativeDays &&
+ other.favoritesOnly == favoritesOnly &&
+ other.includeArchived == includeArchived &&
+ other.themeMode == themeMode &&
+ other.withPartners == withPartners);
+
+ @override
+ int get hashCode => Object.hash(relativeDays, favoritesOnly, includeArchived, themeMode, withPartners);
+
+ @override
+ String toString() =>
+ 'MapConfig(relativeDays: $relativeDays, favoritesOnly: $favoritesOnly, includeArchived: $includeArchived, themeMode: $themeMode, withPartners: $withPartners)';
+}
diff --git a/mobile/lib/domain/models/config/timeline_config.dart b/mobile/lib/domain/models/config/timeline_config.dart
new file mode 100644
index 0000000000..4b6b9d5625
--- /dev/null
+++ b/mobile/lib/domain/models/config/timeline_config.dart
@@ -0,0 +1,30 @@
+import 'package:immich_mobile/domain/models/timeline.model.dart';
+
+class TimelineConfig {
+ final int tilesPerRow;
+ final GroupAssetsBy groupAssetsBy;
+ final bool storageIndicator;
+
+ const TimelineConfig({this.tilesPerRow = 4, this.groupAssetsBy = GroupAssetsBy.day, this.storageIndicator = true});
+
+ TimelineConfig copyWith({int? tilesPerRow, GroupAssetsBy? groupAssetsBy, bool? storageIndicator}) => TimelineConfig(
+ tilesPerRow: tilesPerRow ?? this.tilesPerRow,
+ groupAssetsBy: groupAssetsBy ?? this.groupAssetsBy,
+ storageIndicator: storageIndicator ?? this.storageIndicator,
+ );
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is TimelineConfig &&
+ other.tilesPerRow == tilesPerRow &&
+ other.groupAssetsBy == groupAssetsBy &&
+ other.storageIndicator == storageIndicator);
+
+ @override
+ int get hashCode => Object.hash(tilesPerRow, groupAssetsBy, storageIndicator);
+
+ @override
+ String toString() =>
+ 'TimelineConfig(tilesPerRow: $tilesPerRow, groupAssetsBy: $groupAssetsBy, storageIndicator: $storageIndicator)';
+}
diff --git a/mobile/lib/domain/models/config/viewer_config.dart b/mobile/lib/domain/models/config/viewer_config.dart
new file mode 100644
index 0000000000..595f2bee5d
--- /dev/null
+++ b/mobile/lib/domain/models/config/viewer_config.dart
@@ -0,0 +1,37 @@
+class ViewerConfig {
+ final bool loopVideo;
+ final bool loadOriginalVideo;
+ final bool autoPlayVideo;
+ final bool tapToNavigate;
+
+ const ViewerConfig({
+ this.loopVideo = true,
+ this.loadOriginalVideo = false,
+ this.autoPlayVideo = true,
+ this.tapToNavigate = false,
+ });
+
+ ViewerConfig copyWith({bool? loopVideo, bool? loadOriginalVideo, bool? autoPlayVideo, bool? tapToNavigate}) =>
+ ViewerConfig(
+ loopVideo: loopVideo ?? this.loopVideo,
+ loadOriginalVideo: loadOriginalVideo ?? this.loadOriginalVideo,
+ autoPlayVideo: autoPlayVideo ?? this.autoPlayVideo,
+ tapToNavigate: tapToNavigate ?? this.tapToNavigate,
+ );
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is ViewerConfig &&
+ other.loopVideo == loopVideo &&
+ other.loadOriginalVideo == loadOriginalVideo &&
+ other.autoPlayVideo == autoPlayVideo &&
+ other.tapToNavigate == tapToNavigate);
+
+ @override
+ int get hashCode => Object.hash(loopVideo, loadOriginalVideo, autoPlayVideo, tapToNavigate);
+
+ @override
+ String toString() =>
+ 'ViewerConfig(loopVideo: $loopVideo, loadOriginalVideo: $loadOriginalVideo, autoPlayVideo: $autoPlayVideo, tapToNavigate: $tapToNavigate)';
+}
diff --git a/mobile/lib/domain/models/exif.model.dart b/mobile/lib/domain/models/exif.model.dart
index 97c0ba3823..4284aef2ab 100644
--- a/mobile/lib/domain/models/exif.model.dart
+++ b/mobile/lib/domain/models/exif.model.dart
@@ -69,7 +69,9 @@ class ExifInfo {
@override
bool operator ==(covariant ExifInfo other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.fileSize == fileSize &&
other.description == description &&
diff --git a/mobile/lib/domain/models/log.model.dart b/mobile/lib/domain/models/log.model.dart
index 9902ca04ca..bed1729f9d 100644
--- a/mobile/lib/domain/models/log.model.dart
+++ b/mobile/lib/domain/models/log.model.dart
@@ -20,7 +20,9 @@ class LogMessage {
@override
bool operator ==(covariant LogMessage other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.message == message &&
other.level == level &&
diff --git a/mobile/lib/domain/models/map.model.dart b/mobile/lib/domain/models/map.model.dart
index ce0834f0cb..b55f176bfd 100644
--- a/mobile/lib/domain/models/map.model.dart
+++ b/mobile/lib/domain/models/map.model.dart
@@ -8,7 +8,9 @@ class Marker {
@override
bool operator ==(covariant Marker other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.location == location && other.assetId == assetId;
}
diff --git a/mobile/lib/domain/models/memory.model.dart b/mobile/lib/domain/models/memory.model.dart
index 40117c5ac6..e786ca18b1 100644
--- a/mobile/lib/domain/models/memory.model.dart
+++ b/mobile/lib/domain/models/memory.model.dart
@@ -2,7 +2,6 @@
import 'dart:convert';
import 'package:collection/collection.dart';
-
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
enum MemoryTypeEnum {
@@ -36,7 +35,9 @@ class MemoryData {
@override
bool operator ==(covariant MemoryData other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.year == year;
}
@@ -132,7 +133,9 @@ class DriftMemory {
@override
bool operator ==(covariant DriftMemory other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
final listEquals = const DeepCollectionEquality().equals;
return other.id == id &&
diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart
index a5d158f9c7..61a3cebc8a 100644
--- a/mobile/lib/domain/models/metadata_key.dart
+++ b/mobile/lib/domain/models/metadata_key.dart
@@ -1,9 +1,13 @@
+import 'dart:convert';
+
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/colors.dart';
+import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/config/system_config.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
+import 'package:immich_mobile/domain/models/timeline.model.dart';
enum MetadataDomain {
appConfig('config.app'),
@@ -15,13 +19,52 @@ enum MetadataDomain {
enum MetadataKey {
// Theme
- primaryColor(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)),
+ themePrimaryColor(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)),
themeMode(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)),
- dynamicTheme(.appConfig, 'theme.dynamicTheme', false),
- colorfulInterface(.appConfig, 'theme.colorfulInterface', true),
+ themeDynamic(.appConfig, 'theme.dynamic', false),
+ themeColorfulInterface(.appConfig, 'theme.colorfulInterface', true),
+
+ // Image
+ imagePreferRemote(.appConfig, 'image.preferRemote', false),
+ imageLoadOriginal(.appConfig, 'image.loadOriginal', false),
+
+ // Viewer
+ viewerLoopVideo(.appConfig, 'viewer.loopVideo', true),
+ viewerLoadOriginalVideo(.appConfig, 'viewer.loadOriginalVideo', false),
+ viewerAutoPlayVideo(.appConfig, 'viewer.autoPlayVideo', true),
+ viewerTapToNavigate(.appConfig, 'viewer.tapToNavigate', false),
+
+ // Timeline
+ timelineTilesPerRow(.appConfig, 'timeline.tilesPerRow', 4),
+ timelineGroupAssetsBy(
+ .appConfig,
+ 'timeline.groupAssetsBy',
+ GroupAssetsBy.day,
+ _EnumCodec(GroupAssetsBy.values),
+ ),
+ timelineStorageIndicator(.appConfig, 'timeline.storageIndicator', true),
// Log
- logLevel(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values));
+ logLevel(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)),
+
+ // Map
+ mapShowFavoriteOnly(.appConfig, 'map.showFavoriteOnly', false),
+ mapRelativeDate(.appConfig, 'map.relativeDate', 0),
+ mapIncludeArchived(.appConfig, 'map.includeArchived', false),
+ mapThemeMode(.appConfig, 'map.themeMode', .system, _EnumCodec(ThemeMode.values)),
+ mapWithPartners(.appConfig, 'map.withPartners', false),
+
+ // Cleanup
+ cleanupKeepFavorites(.appConfig, 'cleanup.keepFavorites', true),
+ cleanupKeepMediaType(
+ .appConfig,
+ 'cleanup.keepMediaType',
+ AssetKeepType.none,
+ _EnumCodec(AssetKeepType.values),
+ ),
+ cleanupKeepAlbumIds>(.appConfig, 'cleanup.keepAlbumIds', [], _ListCodec(_PrimitiveCodec.string)),
+ cleanupCutoffDaysAgo(.appConfig, 'cleanup.cutoffDaysAgo', -1),
+ cleanupDefaultsInitialized(.appConfig, 'cleanup.defaultsInitialized', false);
final MetadataDomain domain;
final String name;
@@ -88,6 +131,39 @@ final class _DateTimeCodec extends _MetadataCodec {
DateTime? decode(String raw) => DateTime.tryParse(raw);
}
+final class _ListCodec extends _MetadataCodec> {
+ final _MetadataCodec _elementCodec;
+
+ const _ListCodec(this._elementCodec);
+
+ @override
+ String encode(List value) => jsonEncode(value.map(_elementCodec.encode).toList());
+
+ @override
+ List? decode(String raw) {
+ try {
+ final decoded = jsonDecode(raw);
+ if (decoded is! List) {
+ return null;
+ }
+ final result = [];
+ for (final item in decoded) {
+ if (item is! String) {
+ return null;
+ }
+ final element = _elementCodec.decode(item);
+ if (element == null) {
+ return null;
+ }
+ result.add(element);
+ }
+ return result;
+ } on FormatException {
+ return null;
+ }
+ }
+}
+
final class _PrimitiveCodec extends _MetadataCodec {
final T? Function(String) _parse;
diff --git a/mobile/lib/domain/models/person.model.dart b/mobile/lib/domain/models/person.model.dart
index 7559720c45..c7cdcff3af 100644
--- a/mobile/lib/domain/models/person.model.dart
+++ b/mobile/lib/domain/models/person.model.dart
@@ -69,7 +69,9 @@ class PersonDto {
@override
bool operator ==(covariant PersonDto other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.id == id &&
other.birthDate == birthDate &&
@@ -160,7 +162,9 @@ class DriftPerson {
@override
bool operator ==(covariant DriftPerson other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.id == id &&
other.createdAt == createdAt &&
diff --git a/mobile/lib/domain/models/search_result.model.dart b/mobile/lib/domain/models/search_result.model.dart
index 21134b73d8..6a782e2f37 100644
--- a/mobile/lib/domain/models/search_result.model.dart
+++ b/mobile/lib/domain/models/search_result.model.dart
@@ -12,7 +12,9 @@ class SearchResult {
@override
bool operator ==(covariant SearchResult other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
final listEquals = const DeepCollectionEquality().equals;
return listEquals(other.assets, assets) && other.nextPage == nextPage;
diff --git a/mobile/lib/domain/models/setting.model.dart b/mobile/lib/domain/models/setting.model.dart
index 2c46507331..0dc48de3b1 100644
--- a/mobile/lib/domain/models/setting.model.dart
+++ b/mobile/lib/domain/models/setting.model.dart
@@ -1,13 +1,6 @@
import 'package:immich_mobile/domain/models/store.model.dart';
enum Setting {
- tilesPerRow(StoreKey.tilesPerRow, 4),
- groupAssetsBy(StoreKey.groupAssetsBy, 0),
- showStorageIndicator(StoreKey.storageIndicator, true),
- loadOriginal(StoreKey.loadOriginal, false),
- loadOriginalVideo(StoreKey.loadOriginalVideo, false),
- autoPlayVideo(StoreKey.autoPlayVideo, true),
- preferRemoteImage(StoreKey.preferRemoteImage, false),
advancedTroubleshooting(StoreKey.advancedTroubleshooting, false),
enableBackup(StoreKey.enableBackup, false);
diff --git a/mobile/lib/domain/models/stack.model.dart b/mobile/lib/domain/models/stack.model.dart
index d5ccf5558d..f17f5788c9 100644
--- a/mobile/lib/domain/models/stack.model.dart
+++ b/mobile/lib/domain/models/stack.model.dart
@@ -37,7 +37,9 @@ class Stack {
@override
bool operator ==(covariant Stack other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.id == id &&
other.createdAt == createdAt &&
@@ -61,7 +63,9 @@ class StackResponse {
@override
bool operator ==(covariant StackResponse other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.id == id && other.primaryAssetId == primaryAssetId && other.assetIds == assetIds;
}
diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart
index 13b3d4807c..cae99834bf 100644
--- a/mobile/lib/domain/models/store.model.dart
+++ b/mobile/lib/domain/models/store.model.dart
@@ -19,35 +19,13 @@ enum StoreKey {
backgroundBackup._(14),
sslClientCertData._(15),
sslClientPasswd._(16),
- // user settings from [AppSettingsEnum] below:
- loadPreview._(100),
- loadOriginal._(101),
- tilesPerRow._(103),
- dynamicLayout._(104),
- groupAssetsBy._(105),
uploadErrorNotificationGracePeriod._(106),
- backgroundBackupTotalProgress._(107),
- backgroundBackupSingleProgress._(108),
- storageIndicator._(109),
- thumbnailCacheSize._(110),
- imageCacheSize._(111),
- albumThumbnailCacheSize._(112),
selectedAlbumSortOrder._(113),
advancedTroubleshooting._(114),
- preferRemoteImage._(116),
- loopVideo._(117),
- // map related settings
- mapShowFavoriteOnly._(118),
- mapRelativeDate._(119),
selfSignedCert._(120),
- mapIncludeArchived._(121),
- ignoreIcloudAssets._(122),
selectedAlbumSortReverse._(123),
- mapThemeMode._(124),
- mapwithPartners._(125),
enableHapticFeedback._(126),
customHeaders._(127),
-
syncAlbums._(131),
// Auto endpoint switching
@@ -56,48 +34,46 @@ enum StoreKey {
localEndpoint._(134),
externalEndpointList._(135),
- // Video settings
- loadOriginalVideo._(136),
manageLocalMediaAndroid._(137),
-
// Read-only Mode settings
readonlyModeEnabled._(138),
-
- autoPlayVideo._(139),
albumGridView._(140),
-
- // Image viewer navigation settings
- tapToNavigate._(141),
+ loadOriginal._(101),
// Map custom time range settings
mapCustomFrom._(142),
mapCustomTo._(143),
// Experimental stuff
- photoManagerCustomFilter._(1000),
- betaPromptShown._(1001),
- betaTimeline._(1002),
enableBackup._(1003),
useWifiForUploadVideos._(1004),
useWifiForUploadPhotos._(1005),
- needBetaMigration._(1006),
- // TODO: Remove this after patching open-api
- shouldResetSync._(1007),
-
- // Free up space
- cleanupKeepFavorites._(1008),
- cleanupKeepMediaType._(1009),
- cleanupKeepAlbumIds._(1010),
- cleanupCutoffDaysAgo._(1011),
- cleanupDefaultsInitialized._(1012),
-
syncMigrationStatus._(1013),
// Legacy keys that have been migrated to the new metadata store
+ legacyLoopVideo._(117),
+ legacyLoadOriginalVideo._(136),
+ legacyAutoPlayVideo._(139),
+ legacyTapToNavigate._(141),
+ legacyPreferRemoteImage._(116),
+ legacyLoadOriginal._(101),
legacyPrimaryColor._(128),
legacyDynamicTheme._(129),
legacyColorfulInterface._(130),
legacyThemeMode._(102),
+ legacyCleanupKeepFavorites._(1008),
+ legacyCleanupKeepMediaType._(1009),
+ legacyCleanupKeepAlbumIds._(1010),
+ legacyCleanupCutoffDaysAgo._(1011),
+ legacyCleanupDefaultsInitialized._(1012),
+ legacyTilesPerRow._(103),
+ legacyGroupAssetsBy._(105),
+ legacyStorageIndicator._(109),
+ legacyMapRelativeDate._(119),
+ legacyMapShowFavoriteOnly._(118),
+ legacyMapIncludeArchived._(121),
+ legacyMapThemeMode._(124),
+ legacyMapwithPartners._(125),
legacyLogLevel._(115);
const StoreKey._(this.id);
@@ -122,7 +98,9 @@ StoreDto: {
@override
bool operator ==(covariant StoreDto other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.key == key && other.value == value;
}
diff --git a/mobile/lib/domain/models/tag.model.dart b/mobile/lib/domain/models/tag.model.dart
index 357367b13e..ba9aef02ee 100644
--- a/mobile/lib/domain/models/tag.model.dart
+++ b/mobile/lib/domain/models/tag.model.dart
@@ -13,7 +13,9 @@ class Tag {
@override
bool operator ==(covariant Tag other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.id == id && other.value == value;
}
diff --git a/mobile/lib/domain/models/timeline.model.dart b/mobile/lib/domain/models/timeline.model.dart
index c531fa4a94..86f6f112fb 100644
--- a/mobile/lib/domain/models/timeline.model.dart
+++ b/mobile/lib/domain/models/timeline.model.dart
@@ -2,6 +2,8 @@ enum GroupAssetsBy { day, month, auto, none }
enum HeaderType { none, month, day, monthAndDay }
+enum SortAssetsBy { taken, uploaded }
+
class Bucket {
final int assetCount;
diff --git a/mobile/lib/domain/models/user.model.dart b/mobile/lib/domain/models/user.model.dart
index 380295b4b3..9ed70d61d6 100644
--- a/mobile/lib/domain/models/user.model.dart
+++ b/mobile/lib/domain/models/user.model.dart
@@ -125,7 +125,9 @@ profileChangedAt: $profileChangedAt
@override
bool operator ==(covariant UserDto other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.id == id &&
((updatedAt == null && other.updatedAt == null) ||
@@ -219,7 +221,9 @@ class PartnerUserDto {
@override
bool operator ==(covariant PartnerUserDto other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.id == id &&
other.email == email &&
diff --git a/mobile/lib/domain/models/user_metadata.model.dart b/mobile/lib/domain/models/user_metadata.model.dart
index af404051a7..3da1d94799 100644
--- a/mobile/lib/domain/models/user_metadata.model.dart
+++ b/mobile/lib/domain/models/user_metadata.model.dart
@@ -35,7 +35,9 @@ isOnboarded: $isOnboarded,
@override
bool operator ==(covariant Onboarding other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return isOnboarded == other.isOnboarded;
}
@@ -132,7 +134,9 @@ showSupportBadge: $showSupportBadge,
@override
bool operator ==(covariant Preferences other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.foldersEnabled == foldersEnabled &&
other.memoriesEnabled == memoriesEnabled &&
@@ -199,7 +203,9 @@ licenseKey: $licenseKey,
@override
bool operator ==(covariant License other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return activatedAt == other.activatedAt && activationKey == other.activationKey && licenseKey == other.licenseKey;
}
@@ -251,7 +257,9 @@ license: ${license ?? ""},
@override
bool operator ==(covariant UserMetadata other) {
- if (identical(this, other)) return true;
+ if (identical(this, other)) {
+ return true;
+ }
return other.userId == userId &&
other.key == key &&
diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart
index a2e96f2313..0c8746700c 100644
--- a/mobile/lib/domain/services/background_worker.service.dart
+++ b/mobile/lib/domain/services/background_worker.service.dart
@@ -105,46 +105,58 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
}
@override
- Future onAndroidUpload() async {
- _logger.info('Android background processing started');
- final sw = Stopwatch()..start();
- try {
- if (!await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6))) {
- _logger.warning("Remote sync did not complete successfully, skipping backup");
- return;
- }
- await _handleBackup();
- } catch (error, stack) {
- _logger.severe("Failed to complete Android background processing", error, stack);
- } finally {
- sw.stop();
- _logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
- await _cleanup();
- }
+ Future onAndroidUpload(int? maxMinutes) async {
+ final hashTimeout = Duration(minutes: _isBackupEnabled ? 3 : 6);
+ final backupTimeout = maxMinutes != null ? Duration(minutes: maxMinutes - 1) : null;
+ return _backgroundLoop(
+ hashTimeout: hashTimeout,
+ backupTimeout: backupTimeout,
+ debugLabel: 'Android background upload',
+ );
}
@override
Future onIosUpload(bool isRefresh, int? maxSeconds) async {
- _logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s');
+ final hashTimeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
+ final backupTimeout = maxSeconds != null ? Duration(seconds: maxSeconds - 1) : null;
+ return _backgroundLoop(hashTimeout: hashTimeout, backupTimeout: backupTimeout, debugLabel: 'iOS background upload');
+ }
+
+ Future _backgroundLoop({
+ required Duration hashTimeout,
+ required Duration? backupTimeout,
+ required String debugLabel,
+ }) async {
+ _logger.info(
+ '$debugLabel started hashTimeout: ${hashTimeout.inSeconds}s, backupTimeout: ${backupTimeout?.inMinutes ?? '~'}m',
+ );
final sw = Stopwatch()..start();
try {
- final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
- if (!await _syncAssets(hashTimeout: timeout)) {
+ if (!await _syncAssets(hashTimeout: hashTimeout)) {
_logger.warning("Remote sync did not complete successfully, skipping backup");
return;
}
final backupFuture = _handleBackup();
- if (maxSeconds != null) {
- await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {});
- } else {
+ Timer? cancelTimer;
+ if (backupTimeout != null) {
+ cancelTimer = Timer(backupTimeout, () {
+ if (!_cancellationToken.isCompleted) {
+ _logger.warning("$debugLabel timed out after ${backupTimeout.inMinutes}m, cancelling backup");
+ _cancellationToken.complete();
+ }
+ });
+ }
+ try {
await backupFuture;
+ } finally {
+ cancelTimer?.cancel();
}
} catch (error, stack) {
- _logger.severe("Failed to complete iOS background upload", error, stack);
+ _logger.severe("Failed to complete $debugLabel", error, stack);
} finally {
sw.stop();
- _logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
+ _logger.info("$debugLabel completed in ${sw.elapsed.inSeconds}s");
await _cleanup();
}
}
@@ -177,7 +189,9 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
final nativeSyncApi = _ref?.read(nativeSyncApiProvider);
_logger.info("Cleaning up background worker");
- _cancellationToken.complete();
+ if (!_cancellationToken.isCompleted) {
+ _cancellationToken.complete();
+ }
final cleanupFutures = [
nativeSyncApi?.cancelHashing(),
workerManagerPatch.dispose().catchError((_) async {
diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart
index 1d9ab1e490..34300dee3d 100644
--- a/mobile/lib/domain/services/local_sync.service.dart
+++ b/mobile/lib/domain/services/local_sync.service.dart
@@ -93,8 +93,7 @@ class LocalSyncService {
if (CurrentPlatform.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
+ // does not include changes for cloud albums.
final cloudAlbums = deviceAlbums.where((a) => a.isCloud).toLocalAlbums();
for (final album in cloudAlbums) {
final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id);
diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart
index f060ba9290..d0af52dcfd 100644
--- a/mobile/lib/domain/services/remote_album.service.dart
+++ b/mobile/lib/domain/services/remote_album.service.dart
@@ -184,7 +184,9 @@ class RemoteAlbumService {
List albums, {
required AssetDateAggregation aggregation,
}) async {
- if (albums.isEmpty) return [];
+ if (albums.isEmpty) {
+ return [];
+ }
final albumIds = albums.map((e) => e.id).toList();
final sortedIds = await _repository.getSortedAlbumIds(albumIds, aggregation: aggregation);
diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart
index b325ffd631..16ed64e6d3 100644
--- a/mobile/lib/domain/services/store.service.dart
+++ b/mobile/lib/domain/services/store.service.dart
@@ -72,7 +72,9 @@ class StoreService {
/// Stores the [value] for the [key]. Skips write if value hasn't changed.
Future put, T>(U key, T value) async {
- if (_cache[key.id] == value) return;
+ if (_cache[key.id] == value) {
+ return;
+ }
await _storeRepository.upsert(key, value);
_cache[key.id] = value;
}
diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart
index 906e352b49..9c8bac4c92 100644
--- a/mobile/lib/domain/services/sync_stream.service.dart
+++ b/mobile/lib/domain/services/sync_stream.service.dart
@@ -24,6 +24,7 @@ enum SyncMigrationTask {
v20260128_ResetExifV1, // EXIF table has incorrect width and height information.
v20260128_CopyExifWidthHeightToAsset, // Asset table has incorrect width and height for video ratio calculations.
v20260128_ResetAssetV1, // Asset v2.5.0 has width and height information that were edited assets.
+ v20260597_ResetAssetV1AssetV2, // Assets didn't include the uploadedAt column.
}
class SyncStreamService {
@@ -132,6 +133,13 @@ class SyncStreamService {
migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name);
}
}
+
+ if (!migrations.contains(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name) &&
+ semVer > const SemVer(major: 2, minor: 7, patch: 5)) {
+ _logger.info("Running pre-sync task: v20260597_ResetAssetV1AssetV2");
+ await _syncApiRepository.deleteSyncAck([SyncEntityType.assetV1, SyncEntityType.assetV2]);
+ migrations.add(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name);
+ }
}
Future _runPostSyncTasks(List migrations) async {
@@ -318,7 +326,9 @@ class SyncStreamService {
}
Future handleWsAssetUploadReadyV1Batch(List batchData) async {
- if (batchData.isEmpty) return;
+ if (batchData.isEmpty) {
+ return;
+ }
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV1 events');
@@ -359,7 +369,9 @@ class SyncStreamService {
}
Future handleWsAssetUploadReadyV2Batch(List batchData) async {
- if (batchData.isEmpty) return;
+ if (batchData.isEmpty) {
+ return;
+ }
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV2 events');
diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart
index a055f8bcae..5779ee1053 100644
--- a/mobile/lib/domain/services/timeline.service.dart
+++ b/mobile/lib/domain/services/timeline.service.dart
@@ -5,10 +5,9 @@ import 'package:collection/collection.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
-import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
-import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
+import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/utils/async_mutex.dart';
@@ -35,18 +34,21 @@ enum TimelineOrigin {
deepLink,
albumActivities,
folder,
+ recentlyAdded,
}
class TimelineFactory {
final DriftTimelineRepository _timelineRepository;
- final SettingsService _settingsService;
+ final MetadataRepository _metadataRepository;
- const TimelineFactory({required DriftTimelineRepository timelineRepository, required SettingsService settingsService})
- : _timelineRepository = timelineRepository,
- _settingsService = settingsService;
+ const TimelineFactory({
+ required DriftTimelineRepository timelineRepository,
+ required MetadataRepository metadataRepository,
+ }) : _timelineRepository = timelineRepository,
+ _metadataRepository = metadataRepository;
GroupAssetsBy get groupBy {
- final group = GroupAssetsBy.values[_settingsService.get(Setting.groupAssetsBy)];
+ final group = _metadataRepository.appConfig.timeline.groupAssetsBy;
// We do not support auto grouping in the new timeline yet, fallback to day grouping
return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group;
}
@@ -61,6 +63,8 @@ class TimelineFactory {
TimelineService remoteAssets(String userId) => TimelineService(_timelineRepository.remote(userId, groupBy));
+ TimelineService recentlyAdded(String userId) => TimelineService(_timelineRepository.recentlyAdded(userId, groupBy));
+
TimelineService favorite(String userId) => TimelineService(_timelineRepository.favorite(userId, groupBy));
TimelineService trash(String userId) => TimelineService(_timelineRepository.trash(userId, groupBy));
diff --git a/mobile/lib/domain/services/user.service.dart b/mobile/lib/domain/services/user.service.dart
index 1f9c015ad7..e7b4b0f4e6 100644
--- a/mobile/lib/domain/services/user.service.dart
+++ b/mobile/lib/domain/services/user.service.dart
@@ -30,7 +30,9 @@ class UserService {
Future refreshMyUser() async {
final user = await _userApiRepository.getMyUser();
- if (user == null) return null;
+ if (user == null) {
+ return null;
+ }
await _storeService.put(StoreKey.currentUser, user);
return user;
}
diff --git a/mobile/lib/extensions/asset_extensions.dart b/mobile/lib/extensions/asset_extensions.dart
index 3a994f9cb8..7e1bef1a1c 100644
--- a/mobile/lib/extensions/asset_extensions.dart
+++ b/mobile/lib/extensions/asset_extensions.dart
@@ -11,6 +11,7 @@ extension DTOToAsset on api.AssetResponseDto {
checksum: checksum,
createdAt: fileCreatedAt,
updatedAt: updatedAt,
+ uploadedAt: createdAt,
ownerId: ownerId,
visibility: visibility.toAssetVisibility(),
durationMs: duration,
@@ -33,6 +34,7 @@ extension DTOToAsset on api.AssetResponseDto {
checksum: checksum,
createdAt: fileCreatedAt,
updatedAt: updatedAt,
+ uploadedAt: createdAt,
ownerId: ownerId,
visibility: visibility.toAssetVisibility(),
durationMs: duration,
diff --git a/mobile/lib/extensions/scroll_extensions.dart b/mobile/lib/extensions/scroll_extensions.dart
index 5b8f9e2a13..eb11734c67 100644
--- a/mobile/lib/extensions/scroll_extensions.dart
+++ b/mobile/lib/extensions/scroll_extensions.dart
@@ -94,8 +94,12 @@ class SnapScrollPhysics extends ScrollPhysics {
bool get allowUserScrolling => false;
static double target(ScrollMetrics position, double velocity, double snapOffset) {
- if (velocity > _minFlingVelocity) return snapOffset;
- if (velocity < -_minFlingVelocity) return position.pixels < snapOffset ? 0.0 : snapOffset;
+ if (velocity > _minFlingVelocity) {
+ return snapOffset;
+ }
+ if (velocity < -_minFlingVelocity) {
+ return position.pixels < snapOffset ? 0.0 : snapOffset;
+ }
return position.pixels < minSnapDistance ? 0.0 : snapOffset;
}
}
diff --git a/mobile/lib/infrastructure/entities/asset_face.entity.dart b/mobile/lib/infrastructure/entities/asset_face.entity.dart
index 40fe9ab1c1..b94a0cf094 100644
--- a/mobile/lib/infrastructure/entities/asset_face.entity.dart
+++ b/mobile/lib/infrastructure/entities/asset_face.entity.dart
@@ -5,6 +5,11 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)')
+@TableIndex.sql('''
+CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person
+ON asset_face_entity (person_id, asset_id)
+WHERE is_visible = 1 AND deleted_at IS NULL
+''')
class AssetFaceEntity extends Table with DriftDefaultsMixin {
const AssetFaceEntity();
diff --git a/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart b/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart
index c97dd545a8..d262325742 100644
--- a/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart
+++ b/mobile/lib/infrastructure/entities/asset_face.entity.drift.dart
@@ -1350,3 +1350,7 @@ i0.Index get idxAssetFaceAssetId => i0.Index(
'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
);
+i0.Index get idxAssetFaceVisiblePerson => i0.Index(
+ 'idx_asset_face_visible_person',
+ 'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
+);
diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart
index e009029ea7..120fbd0c68 100644
--- a/mobile/lib/infrastructure/entities/exif.entity.dart
+++ b/mobile/lib/infrastructure/entities/exif.entity.dart
@@ -6,6 +6,10 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)')
+@TableIndex.sql('''
+CREATE INDEX IF NOT EXISTS idx_remote_exif_city
+ON remote_exif_entity (city) WHERE city IS NOT NULL
+''')
class RemoteExifEntity extends Table with DriftDefaultsMixin {
const RemoteExifEntity();
diff --git a/mobile/lib/infrastructure/entities/exif.entity.drift.dart b/mobile/lib/infrastructure/entities/exif.entity.drift.dart
index 8695e2004b..cbe31f5bb4 100644
--- a/mobile/lib/infrastructure/entities/exif.entity.drift.dart
+++ b/mobile/lib/infrastructure/entities/exif.entity.drift.dart
@@ -1883,3 +1883,8 @@ class RemoteExifEntityCompanion
.toString();
}
}
+
+i0.Index get idxRemoteExifCity => i0.Index(
+ 'idx_remote_exif_city',
+ 'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
+);
diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift b/mobile/lib/infrastructure/entities/merged_asset.drift
index daad02e2b3..d0321ab1ef 100644
--- a/mobile/lib/infrastructure/entities/merged_asset.drift
+++ b/mobile/lib/infrastructure/entities/merged_asset.drift
@@ -4,7 +4,7 @@ import 'local_asset.entity.dart';
import 'local_album.entity.dart';
import 'local_album_asset.entity.dart';
-mergedAsset:
+mergedAsset:
SELECT
rae.id as remote_id,
(SELECT lae.id FROM local_asset_entity lae WHERE lae.checksum = rae.checksum LIMIT 1) as local_id,
@@ -27,7 +27,8 @@ SELECT
NULL as longitude,
NULL as adjustmentTime,
rae.is_edited,
- 0 as playback_style
+ 0 as playback_style,
+ rae.uploaded_at
FROM
remote_asset_entity rae
LEFT JOIN
@@ -65,7 +66,8 @@ SELECT
lae.longitude,
lae.adjustment_time,
0 as is_edited,
- lae.playback_style
+ lae.playback_style,
+ NULL as uploaded_at
FROM
local_asset_entity lae
WHERE NOT EXISTS (
diff --git a/mobile/lib/infrastructure/entities/merged_asset.drift.dart b/mobile/lib/infrastructure/entities/merged_asset.drift.dart
index 1e501d4028..2d05ef6ceb 100644
--- a/mobile/lib/infrastructure/entities/merged_asset.drift.dart
+++ b/mobile/lib/infrastructure/entities/merged_asset.drift.dart
@@ -29,7 +29,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
);
$arrayStartIndex += generatedlimit.amountOfVariables;
return customSelect(
- 'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_ms, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_ms, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
+ 'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_ms, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style, rae.uploaded_at FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_ms, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style, NULL AS uploaded_at FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
variables: [
for (var $ in userIds) i0.Variable($),
...generatedlimit.introducedVariables,
@@ -68,6 +68,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
adjustmentTime: row.readNullable('adjustmentTime'),
isEdited: row.read('is_edited'),
playbackStyle: row.read('playback_style'),
+ uploadedAt: row.readNullable('uploaded_at'),
),
);
}
@@ -141,6 +142,7 @@ class MergedAssetResult {
final DateTime? adjustmentTime;
final bool isEdited;
final int playbackStyle;
+ final DateTime? uploadedAt;
MergedAssetResult({
this.remoteId,
this.localId,
@@ -164,6 +166,7 @@ class MergedAssetResult {
this.adjustmentTime,
required this.isEdited,
required this.playbackStyle,
+ this.uploadedAt,
});
}
diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart
index 7cd8913542..8644667168 100644
--- a/mobile/lib/infrastructure/entities/remote_asset.entity.dart
+++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart
@@ -5,9 +5,6 @@ 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.sql(
- 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
-)
@TableIndex.sql('''
CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum
ON remote_asset_entity (owner_id, checksum)
@@ -20,12 +17,10 @@ WHERE (library_id IS NOT NULL);
''')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)')
-@TableIndex.sql(
- "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))",
-)
-@TableIndex.sql(
- "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))",
-)
+@TableIndex.sql('''
+CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created
+ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)
+''')
class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
const RemoteAssetEntity();
@@ -43,6 +38,8 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin
DateTimeColumn get deletedAt => dateTime().nullable()();
+ DateTimeColumn get uploadedAt => dateTime().nullable()();
+
TextColumn get livePhotoVideoId => text().nullable()();
IntColumn get visibility => intEnum()();
@@ -66,6 +63,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
type: type,
createdAt: createdAt,
updatedAt: updatedAt,
+ uploadedAt: uploadedAt,
durationMs: durationMs,
isFavorite: isFavorite,
height: height,
diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart
index 845c99e3c2..8141573343 100644
--- a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart
+++ b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart
@@ -27,6 +27,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder =
i0.Value localDateTime,
i0.Value thumbHash,
i0.Value deletedAt,
+ i0.Value uploadedAt,
i0.Value livePhotoVideoId,
required i2.AssetVisibility visibility,
i0.Value stackId,
@@ -49,6 +50,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder =
i0.Value localDateTime,
i0.Value thumbHash,
i0.Value deletedAt,
+ i0.Value uploadedAt,
i0.Value livePhotoVideoId,
i0.Value visibility,
i0.Value stackId,
@@ -177,6 +179,11 @@ class $$RemoteAssetEntityTableFilterComposer
builder: (column) => i0.ColumnFilters(column),
);
+ i0.ColumnFilters get uploadedAt => $composableBuilder(
+ column: $table.uploadedAt,
+ builder: (column) => i0.ColumnFilters(column),
+ );
+
i0.ColumnFilters get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => i0.ColumnFilters(column),
@@ -305,6 +312,11 @@ class $$RemoteAssetEntityTableOrderingComposer
builder: (column) => i0.ColumnOrderings(column),
);
+ i0.ColumnOrderings get uploadedAt => $composableBuilder(
+ column: $table.uploadedAt,
+ builder: (column) => i0.ColumnOrderings(column),
+ );
+
i0.ColumnOrderings get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => i0.ColumnOrderings(column),
@@ -412,6 +424,11 @@ class $$RemoteAssetEntityTableAnnotationComposer
i0.GeneratedColumn get deletedAt =>
$composableBuilder(column: $table.deletedAt, builder: (column) => column);
+ i0.GeneratedColumn get uploadedAt => $composableBuilder(
+ column: $table.uploadedAt,
+ builder: (column) => column,
+ );
+
i0.GeneratedColumn get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => column,
@@ -507,6 +524,7 @@ class $$RemoteAssetEntityTableTableManager
i0.Value localDateTime = const i0.Value.absent(),
i0.Value thumbHash = const i0.Value.absent(),
i0.Value deletedAt = const i0.Value.absent(),
+ i0.Value uploadedAt = const i0.Value.absent(),
i0.Value livePhotoVideoId = const i0.Value.absent(),
i0.Value visibility =
const i0.Value.absent(),
@@ -528,6 +546,7 @@ class $$RemoteAssetEntityTableTableManager
localDateTime: localDateTime,
thumbHash: thumbHash,
deletedAt: deletedAt,
+ uploadedAt: uploadedAt,
livePhotoVideoId: livePhotoVideoId,
visibility: visibility,
stackId: stackId,
@@ -550,6 +569,7 @@ class $$RemoteAssetEntityTableTableManager
i0.Value localDateTime = const i0.Value.absent(),
i0.Value thumbHash = const i0.Value.absent(),
i0.Value deletedAt = const i0.Value.absent(),
+ i0.Value uploadedAt = const i0.Value.absent(),
i0.Value livePhotoVideoId = const i0.Value.absent(),
required i2.AssetVisibility visibility,
i0.Value stackId = const i0.Value.absent(),
@@ -570,6 +590,7 @@ class $$RemoteAssetEntityTableTableManager
localDateTime: localDateTime,
thumbHash: thumbHash,
deletedAt: deletedAt,
+ uploadedAt: uploadedAt,
livePhotoVideoId: livePhotoVideoId,
visibility: visibility,
stackId: stackId,
@@ -645,9 +666,9 @@ typedef $$RemoteAssetEntityTableProcessedTableManager =
i1.RemoteAssetEntityData,
i0.PrefetchHooks Function({bool ownerId})
>;
-i0.Index get idxRemoteAssetOwnerChecksum => i0.Index(
- 'idx_remote_asset_owner_checksum',
- 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
+i0.Index get uQRemoteAssetsOwnerChecksum => i0.Index(
+ 'UQ_remote_assets_owner_checksum',
+ 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
@@ -818,6 +839,18 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
);
+ static const i0.VerificationMeta _uploadedAtMeta = const i0.VerificationMeta(
+ 'uploadedAt',
+ );
+ @override
+ late final i0.GeneratedColumn uploadedAt =
+ i0.GeneratedColumn(
+ 'uploaded_at',
+ aliasedName,
+ true,
+ type: i0.DriftSqlType.dateTime,
+ requiredDuringInsert: false,
+ );
static const i0.VerificationMeta _livePhotoVideoIdMeta =
const i0.VerificationMeta('livePhotoVideoId');
@override
@@ -894,6 +927,7 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
localDateTime,
thumbHash,
deletedAt,
+ uploadedAt,
livePhotoVideoId,
visibility,
stackId,
@@ -998,6 +1032,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta),
);
}
+ if (data.containsKey('uploaded_at')) {
+ context.handle(
+ _uploadedAtMeta,
+ uploadedAt.isAcceptableOrUnknown(data['uploaded_at']!, _uploadedAtMeta),
+ );
+ }
if (data.containsKey('live_photo_video_id')) {
context.handle(
_livePhotoVideoIdMeta,
@@ -1095,6 +1135,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
i0.DriftSqlType.dateTime,
data['${effectivePrefix}deleted_at'],
),
+ uploadedAt: attachedDatabase.typeMapping.read(
+ i0.DriftSqlType.dateTime,
+ data['${effectivePrefix}uploaded_at'],
+ ),
livePhotoVideoId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}live_photo_video_id'],
@@ -1153,6 +1197,7 @@ class RemoteAssetEntityData extends i0.DataClass
final DateTime? localDateTime;
final String? thumbHash;
final DateTime? deletedAt;
+ final DateTime? uploadedAt;
final String? livePhotoVideoId;
final i2.AssetVisibility visibility;
final String? stackId;
@@ -1173,6 +1218,7 @@ class RemoteAssetEntityData extends i0.DataClass
this.localDateTime,
this.thumbHash,
this.deletedAt,
+ this.uploadedAt,
this.livePhotoVideoId,
required this.visibility,
this.stackId,
@@ -1212,6 +1258,9 @@ class RemoteAssetEntityData extends i0.DataClass
if (!nullToAbsent || deletedAt != null) {
map['deleted_at'] = i0.Variable(deletedAt);
}
+ if (!nullToAbsent || uploadedAt != null) {
+ map['uploaded_at'] = i0.Variable(uploadedAt);
+ }
if (!nullToAbsent || livePhotoVideoId != null) {
map['live_photo_video_id'] = i0.Variable(livePhotoVideoId);
}
@@ -1252,6 +1301,7 @@ class RemoteAssetEntityData extends i0.DataClass
localDateTime: serializer.fromJson(json['localDateTime']),
thumbHash: serializer.fromJson(json['thumbHash']),
deletedAt: serializer.fromJson(json['deletedAt']),
+ uploadedAt: serializer.fromJson(json['uploadedAt']),
livePhotoVideoId: serializer.fromJson(json['livePhotoVideoId']),
visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromJson(
serializer.fromJson(json['visibility']),
@@ -1281,6 +1331,7 @@ class RemoteAssetEntityData extends i0.DataClass
'localDateTime': serializer.toJson(localDateTime),
'thumbHash': serializer.toJson(thumbHash),
'deletedAt': serializer.toJson(deletedAt),
+ 'uploadedAt': serializer.toJson(uploadedAt),
'livePhotoVideoId': serializer.toJson(livePhotoVideoId),
'visibility': serializer.toJson(
i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility),
@@ -1306,6 +1357,7 @@ class RemoteAssetEntityData extends i0.DataClass
i0.Value localDateTime = const i0.Value.absent(),
i0.Value thumbHash = const i0.Value.absent(),
i0.Value deletedAt = const i0.Value.absent(),
+ i0.Value uploadedAt = const i0.Value.absent(),
i0.Value livePhotoVideoId = const i0.Value.absent(),
i2.AssetVisibility? visibility,
i0.Value stackId = const i0.Value.absent(),
@@ -1328,6 +1380,7 @@ class RemoteAssetEntityData extends i0.DataClass
: this.localDateTime,
thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash,
deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt,
+ uploadedAt: uploadedAt.present ? uploadedAt.value : this.uploadedAt,
livePhotoVideoId: livePhotoVideoId.present
? livePhotoVideoId.value
: this.livePhotoVideoId,
@@ -1358,6 +1411,9 @@ class RemoteAssetEntityData extends i0.DataClass
: this.localDateTime,
thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash,
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
+ uploadedAt: data.uploadedAt.present
+ ? data.uploadedAt.value
+ : this.uploadedAt,
livePhotoVideoId: data.livePhotoVideoId.present
? data.livePhotoVideoId.value
: this.livePhotoVideoId,
@@ -1387,6 +1443,7 @@ class RemoteAssetEntityData extends i0.DataClass
..write('localDateTime: $localDateTime, ')
..write('thumbHash: $thumbHash, ')
..write('deletedAt: $deletedAt, ')
+ ..write('uploadedAt: $uploadedAt, ')
..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility, ')
..write('stackId: $stackId, ')
@@ -1412,6 +1469,7 @@ class RemoteAssetEntityData extends i0.DataClass
localDateTime,
thumbHash,
deletedAt,
+ uploadedAt,
livePhotoVideoId,
visibility,
stackId,
@@ -1436,6 +1494,7 @@ class RemoteAssetEntityData extends i0.DataClass
other.localDateTime == this.localDateTime &&
other.thumbHash == this.thumbHash &&
other.deletedAt == this.deletedAt &&
+ other.uploadedAt == this.uploadedAt &&
other.livePhotoVideoId == this.livePhotoVideoId &&
other.visibility == this.visibility &&
other.stackId == this.stackId &&
@@ -1459,6 +1518,7 @@ class RemoteAssetEntityCompanion
final i0.Value localDateTime;
final i0.Value thumbHash;
final i0.Value deletedAt;
+ final i0.Value uploadedAt;
final i0.Value livePhotoVideoId;
final i0.Value visibility;
final i0.Value stackId;
@@ -1479,6 +1539,7 @@ class RemoteAssetEntityCompanion
this.localDateTime = const i0.Value.absent(),
this.thumbHash = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(),
+ this.uploadedAt = const i0.Value.absent(),
this.livePhotoVideoId = const i0.Value.absent(),
this.visibility = const i0.Value.absent(),
this.stackId = const i0.Value.absent(),
@@ -1500,6 +1561,7 @@ class RemoteAssetEntityCompanion
this.localDateTime = const i0.Value.absent(),
this.thumbHash = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(),
+ this.uploadedAt = const i0.Value.absent(),
this.livePhotoVideoId = const i0.Value.absent(),
required i2.AssetVisibility visibility,
this.stackId = const i0.Value.absent(),
@@ -1526,6 +1588,7 @@ class RemoteAssetEntityCompanion
i0.Expression? localDateTime,
i0.Expression? thumbHash,
i0.Expression