Merge branch 'main' into feat/memories-view

pull/28675/head
Timon 2026-06-03 09:46:49 +02:00 committed by GitHub
commit 1b451f3d07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
350 changed files with 21700 additions and 5738 deletions

View File

@ -15,7 +15,7 @@ services:
volumes: volumes:
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data - ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- pnpm_store_server:/buildcache/pnpm-store - build_cache:/buildcache
- ../packages/plugin-core:/build/plugins/immich-plugin-core - ../packages/plugin-core:/build/plugins/immich-plugin-core
immich-web: immich-web:
env_file: !reset [] env_file: !reset []

1
.github/FUNDING.yml vendored
View File

@ -1 +0,0 @@
custom: ['https://buy.immich.app', 'https://immich.store']

View File

@ -51,7 +51,7 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -79,7 +79,7 @@ jobs:
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -201,7 +201,7 @@ jobs:
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@ -19,7 +19,7 @@ jobs:
actions: write actions: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@ -24,7 +24,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Check for breaking API changes - name: Check for breaking API changes
uses: oasdiff/oasdiff-action/breaking@6147a58e5d1249a12f42fc864ab791d571a30015 # v0.0.47 uses: oasdiff/oasdiff-action/breaking@50e6a3413e5aa9c3ae4d8393c34745be44288b46 # v0.0.48
with: with:
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
revision: open-api/immich-openapi-specs.json revision: open-api/immich-openapi-specs.json

View File

@ -31,7 +31,7 @@ jobs:
working-directory: ./packages/cli working-directory: ./packages/cli
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -49,7 +49,9 @@ jobs:
- name: Publish - name: Publish
if: ${{ github.event_name == 'release' }} if: ${{ github.event_name == 'release' }}
run: mise run ci-publish env:
NPM_TAG: ${{ github.event.release.prerelease && 'rc' || 'latest' }}
run: mise run ci-publish -- --tag "$NPM_TAG"
docker: docker:
name: Docker name: Docker
@ -61,7 +63,7 @@ jobs:
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -73,13 +75,13 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
if: ${{ !github.event.pull_request.head.repo.fork }} if: ${{ !github.event.pull_request.head.repo.fork }}
with: with:
registry: ghcr.io registry: ghcr.io
@ -94,7 +96,7 @@ jobs:
- name: Generate docker image tags - name: Generate docker image tags
id: metadata id: metadata
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0 uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with: with:
flavor: | flavor: |
latest=false latest=false
@ -102,10 +104,10 @@ jobs:
name=ghcr.io/${{ github.repository_owner }}/immich-cli name=ghcr.io/${{ github.repository_owner }}/immich-cli
tags: | tags: |
type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'release' }} type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'release' }}
type=raw,value=latest,enable=${{ github.event_name == 'release' }} type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }}
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with: with:
file: packages/cli/Dockerfile file: packages/cli/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64

View File

@ -35,7 +35,7 @@ jobs:
needs: [get_body, should_run] needs: [get_body, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' }} if: ${{ needs.should_run.outputs.should_run == 'true' }}
container: container:
image: ghcr.io/immich-app/mdq:main@sha256:0a8b8867773a0f8368061f47578603f438349f8f1f28b0e16105f481e5c794e0 image: ghcr.io/immich-app/mdq:main@sha256:e73f60195b39748c4876f23e3e6cd22a68a9754acec8aef1fd6979fd52cd2c9f
outputs: outputs:
checked: ${{ steps.get_checkbox.outputs.checked }} checked: ${{ steps.get_checkbox.outputs.checked }}
steps: steps:

View File

@ -44,7 +44,7 @@ jobs:
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -57,7 +57,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -70,7 +70,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@ -83,6 +83,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'

View File

@ -23,7 +23,7 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -60,7 +60,7 @@ jobs:
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn'] suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
steps: steps:
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -90,7 +90,7 @@ jobs:
suffix: [''] suffix: ['']
steps: steps:
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -132,7 +132,7 @@ jobs:
suffixes: '-rocm' suffixes: '-rocm'
platforms: linux/amd64 platforms: linux/amd64
runner-mapping: '{"linux/amd64": "pokedex-large"}' runner-mapping: '{"linux/amd64": "pokedex-large"}'
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0 uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@db54dcf16fbb12c43479a23749ceea0ad1b4a704 # multi-runner-build-workflow-v3.0.0
permissions: permissions:
contents: read contents: read
actions: read actions: read
@ -147,7 +147,7 @@ jobs:
platforms: ${{ matrix.platforms }} platforms: ${{ matrix.platforms }}
runner-mapping: ${{ matrix.runner-mapping }} runner-mapping: ${{ matrix.runner-mapping }}
suffixes: ${{ matrix.suffixes }} suffixes: ${{ matrix.suffixes }}
dockerhub-push: ${{ github.event_name == 'release' }} dockerhub-push: ${{ github.event_name == 'release' && !github.event.release.prerelease }}
build-args: | build-args: |
DEVICE=${{ matrix.device }} DEVICE=${{ matrix.device }}
@ -155,7 +155,7 @@ jobs:
name: Build and Push Server name: Build and Push Server
needs: pre-job needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }} if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0 uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@db54dcf16fbb12c43479a23749ceea0ad1b4a704 # multi-runner-build-workflow-v3.0.0
permissions: permissions:
contents: read contents: read
actions: read actions: read
@ -167,7 +167,7 @@ jobs:
image: immich-server image: immich-server
context: . context: .
dockerfile: server/Dockerfile dockerfile: server/Dockerfile
dockerhub-push: ${{ github.event_name == 'release' }} dockerhub-push: ${{ github.event_name == 'release' && !github.event.release.prerelease }}
build-args: | build-args: |
DEVICE=cpu DEVICE=cpu

View File

@ -21,7 +21,7 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -54,7 +54,7 @@ jobs:
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@ -20,7 +20,7 @@ jobs:
artifact: ${{ steps.get-artifact.outputs.result }} artifact: ${{ steps.get-artifact.outputs.result }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -98,9 +98,16 @@ jobs:
shouldDeploy: true shouldDeploy: true
}; };
} else if (eventType == "release") { } else if (eventType == "release") {
const tag = context.payload.workflow_run.head_branch;
const { data: release } = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag,
});
parameters = { parameters = {
event: "release", event: "release",
name: context.payload.workflow_run.head_branch, name: tag,
prerelease: release.prerelease,
shouldDeploy: !isFork shouldDeploy: !isFork
}; };
} }
@ -119,7 +126,7 @@ jobs:
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }} if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -146,6 +153,7 @@ jobs:
const parameters = JSON.parse(process.env.PARAM_JSON); const parameters = JSON.parse(process.env.PARAM_JSON);
core.setOutput("event", parameters.event); core.setOutput("event", parameters.event);
core.setOutput("name", parameters.name); core.setOutput("name", parameters.name);
core.setOutput("prerelease", parameters.prerelease);
core.setOutput("shouldDeploy", parameters.shouldDeploy); core.setOutput("shouldDeploy", parameters.shouldDeploy);
- name: Download artifact - name: Download artifact
@ -203,7 +211,7 @@ jobs:
run: mise run //docs:deploy run: mise run //docs:deploy
- name: Deploy Docs Release Domain - name: Deploy Docs Release Domain
if: ${{ steps.parameters.outputs.event == 'release' }} if: ${{ steps.parameters.outputs.event == 'release' && steps.parameters.outputs.prerelease != 'true' }}
env: env:
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}} TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

View File

@ -17,7 +17,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@ -15,7 +15,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@ -14,7 +14,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@ -49,7 +49,7 @@ jobs:
permissions: {} # No job-level permissions are needed because it uses the app-token permissions: {} # No job-level permissions are needed because it uses the app-token
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -137,7 +137,7 @@ jobs:
github-token: ${{ steps.generate-token.outputs.token }} github-token: ${{ steps.generate-token.outputs.token }}
- name: Create draft release - name: Create draft release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with: with:
draft: true draft: true
tag_name: ${{ needs.bump_version.outputs.version }} tag_name: ${{ needs.bump_version.outputs.version }}

View File

@ -14,7 +14,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -32,7 +32,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@ -16,7 +16,7 @@ jobs:
packages: write packages: write
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -39,4 +39,6 @@ jobs:
run: pnpm --filter @immich/sdk build run: pnpm --filter @immich/sdk build
- name: Publish - name: Publish
run: pnpm --filter @immich/sdk publish --provenance --no-git-checks env:
NPM_TAG: ${{ github.event.release.prerelease && 'rc' || 'latest' }}
run: pnpm --filter @immich/sdk publish --provenance --no-git-checks --tag "$NPM_TAG"

View File

@ -20,7 +20,7 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -49,7 +49,7 @@ jobs:
working-directory: ./mobile working-directory: ./mobile
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -72,10 +72,6 @@ jobs:
run: flutter pub get run: flutter pub get
working-directory: ./mobile/packages/ui working-directory: ./mobile/packages/ui
- name: Install dependencies for UI Showcase
run: flutter pub get
working-directory: ./mobile/packages/ui/showcase
- name: Generate translation files - name: Generate translation files
run: mise //mobile:codegen:translation run: mise //mobile:codegen:translation

View File

@ -17,7 +17,7 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -71,7 +71,7 @@ jobs:
contents: read contents: read
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -102,7 +102,7 @@ jobs:
working-directory: ./packages/cli working-directory: ./packages/cli
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -133,7 +133,7 @@ jobs:
working-directory: ./packages/cli working-directory: ./packages/cli
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -177,7 +177,7 @@ jobs:
working-directory: ./web working-directory: ./web
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -215,7 +215,7 @@ jobs:
working-directory: ./web working-directory: ./web
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -243,7 +243,7 @@ jobs:
contents: read contents: read
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -293,7 +293,7 @@ jobs:
working-directory: ./e2e working-directory: ./e2e
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -325,7 +325,7 @@ jobs:
working-directory: ./server working-directory: ./server
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -361,7 +361,7 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm] runner: [ubuntu-latest, ubuntu-24.04-arm]
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -374,7 +374,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- name: Setup Node - name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@ -438,7 +438,7 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm] runner: [ubuntu-latest, ubuntu-24.04-arm]
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -451,7 +451,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- name: Setup Node - name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@ -546,7 +546,7 @@ jobs:
contents: read contents: read
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -583,7 +583,7 @@ jobs:
working-directory: ./machine-learning working-directory: ./machine-learning
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -613,7 +613,7 @@ jobs:
working-directory: ./.github working-directory: ./.github
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -643,7 +643,7 @@ jobs:
contents: read contents: read
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -664,7 +664,7 @@ jobs:
contents: read contents: read
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -722,7 +722,7 @@ jobs:
- 5432:5432 - 5432:5432
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@ -24,7 +24,7 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }} should_run: ${{ steps.check.outputs.should_run }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@ -47,7 +47,7 @@ jobs:
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }} if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
steps: steps:
- id: token - id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0 uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@ -1,134 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation
in our community a harassment-free experience for everyone, regardless
of age, body size, visible or invisible disability, ethnicity, sex
characteristics, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance,
race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open,
welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for
our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our
mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or
political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in
a professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our
standards of acceptable behavior and will take appropriate and fair
corrective action in response to any behavior that they deem
inappropriate, threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit,
or reject comments, commits, code, wiki edits, issues, and other
contributions that are not aligned to this Code of Conduct, and will
communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also
applies when an individual is officially representing the community in
public spaces. Examples of representing our community include using an
official e-mail address, posting via an official social media account,
or acting as an appointed representative at an online or offline
event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported to the community leaders responsible for enforcement
at our Discord channel. All complaints
will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and
security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in
determining the consequences for any action they deem in violation of
this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior
deemed unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders,
providing clarity around the nature of the violation and an
explanation of why the behavior was inappropriate. A public apology
may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued
behavior. No interaction with the people involved, including
unsolicited interaction with those enforcing the Code of Conduct, for
a specified period of time. This includes avoiding interactions in
community spaces as well as external channels like social
media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards,
including sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or
public communication with the community for a specified period of
time. No public or private interaction with the people involved,
including unsolicited interaction with those enforcing the Code of
Conduct, is allowed during this period. Violating these terms may lead
to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of
community standards, including sustained inappropriate behavior,
harassment of an individual, or aggression toward or disparagement of
classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction
within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor
Covenant][homepage], version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of
conduct enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the
FAQ at https://www.contributor-covenant.org/faq. Translations are
available at https://www.contributor-covenant.org/translations.

View File

@ -1,5 +0,0 @@
# Security Policy
## Reporting a Vulnerability
Please report security issues to `security@immich.app`

View File

@ -154,7 +154,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193 image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1

View File

@ -56,7 +56,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193 image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
@ -85,7 +85,7 @@ services:
container_name: immich_prometheus container_name: immich_prometheus
ports: ports:
- 9090:9090 - 9090:9090
image: prom/prometheus@sha256:e4254400b85610324913f0dc4acf92603d9984e7519414c5a12811aa6146acc3 image: prom/prometheus@sha256:69f5241418838263316593f7274a304b095c40bcf22e57272865da91bd60a8ac
volumes: volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml - ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus - prometheus-data:/prometheus

View File

@ -61,7 +61,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193 image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
user: '1000:1000' user: '1000:1000'
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true

View File

@ -49,7 +49,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193 image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always

View File

@ -109,6 +109,24 @@ mise //mobile:translation
The mobile app asks you what backend to connect to. You can utilize the demo backend (https://demo.immich.app/) if you don't need to change server code or upload photos. Alternatively, you can run the server yourself per the instructions above. The mobile app asks you what backend to connect to. You can utilize the demo backend (https://demo.immich.app/) if you don't need to change server code or upload photos. Alternatively, you can run the server yourself per the instructions above.
#### UI components and widget previews
Shared design-system widgets (buttons, inputs, forms) live in the
[`immich_ui` package](https://github.com/immich-app/immich/tree/main/mobile/packages/ui/)
under `mobile/packages/ui/`. Components are defined in `lib/src/components/`
and have matching previews in `lib/src/previews/`.
To inspect a component in isolation with a light/dark toggle and hot reload,
launch [Flutter's Widget Previewer](https://docs.flutter.dev/tools/widget-previewer):
```bash
cd mobile/packages/ui
flutter widget-preview start
```
In VS Code or Android Studio with the Flutter plugin, the previewer
auto-starts when you open the **Flutter Widget Preview** tab in the sidebar.
## IDE setup ## IDE setup
### Lint / format extensions ### Lint / format extensions

View File

@ -44,7 +44,7 @@ services:
redis: redis:
container_name: immich-e2e-redis container_name: immich-e2e-redis
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193 image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1

View File

@ -95,6 +95,7 @@ describe('/server', () => {
major: expect.any(Number), major: expect.any(Number),
minor: expect.any(Number), minor: expect.any(Number),
patch: expect.any(Number), patch: expect.any(Number),
prerelease: null,
}); });
}); });
}); });
@ -115,6 +116,7 @@ describe('/server', () => {
oauthAutoLaunch: false, oauthAutoLaunch: false,
ocr: false, ocr: false,
passwordLogin: true, passwordLogin: true,
realtimeTranscoding: false,
search: true, search: true,
sidecar: true, sidecar: true,
trash: true, trash: true,
@ -139,6 +141,7 @@ describe('/server', () => {
maintenanceMode: false, maintenanceMode: false,
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json', mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json', mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
minFaces: 3,
}); });
}); });
}); });

View File

@ -21,18 +21,18 @@ describe('/system-config', () => {
const response1 = await request(app) const response1 = await request(app)
.put('/system-config') .put('/system-config')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...config, newVersionCheck: { enabled: false } }); .send({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } });
expect(response1.status).toBe(200); expect(response1.status).toBe(200);
expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false } }); expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } });
const response2 = await request(app) const response2 = await request(app)
.put('/system-config') .put('/system-config')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...config, newVersionCheck: { enabled: true } }); .send({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } });
expect(response2.status).toBe(200); expect(response2.status).toBe(200);
expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true } }); expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } });
}); });
it('should reject an invalid config entry', async () => { it('should reject an invalid config entry', async () => {

View File

@ -230,6 +230,21 @@ describe('/users', () => {
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ download: { includeEmbeddedVideos: true } }); expect(after).toMatchObject({ download: { includeEmbeddedVideos: true } });
}); });
it('should update minimum face count to display people', async () => {
const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(before).toMatchObject({ people: { minimumFaces: 3 } });
const { status, body } = await request(app)
.put('/users/me/preferences')
.send({ people: { minimumFaces: 2 } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ people: { minimumFaces: 2 } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ people: { minimumFaces: 2 } });
});
}); });
describe('GET /users/:id', () => { describe('GET /users/:id', () => {

View File

@ -305,6 +305,8 @@
"refreshing_all_libraries": "Refreshing all libraries", "refreshing_all_libraries": "Refreshing all libraries",
"registration": "Admin Registration", "registration": "Admin Registration",
"registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.", "registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
"release_channel_release_candidate": "Release candidate",
"release_channel_stable": "Stable",
"remove_failed_jobs": "Remove failed jobs", "remove_failed_jobs": "Remove failed jobs",
"require_password_change_on_login": "Require user to change password on first login", "require_password_change_on_login": "Require user to change password on first login",
"reset_settings_to_default": "Reset settings to default", "reset_settings_to_default": "Reset settings to default",
@ -399,6 +401,10 @@
"transcoding_preferred_hardware_device_description": "Applies only to VAAPI and QSV. Sets the dri node used for hardware transcoding.", "transcoding_preferred_hardware_device_description": "Applies only to VAAPI and QSV. Sets the dri node used for hardware transcoding.",
"transcoding_preset_preset": "Preset (-preset)", "transcoding_preset_preset": "Preset (-preset)",
"transcoding_preset_preset_description": "Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above 'faster'.", "transcoding_preset_preset_description": "Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above 'faster'.",
"transcoding_realtime": "Real-time Transcoding [EXPERIMENTAL]",
"transcoding_realtime_description": "Allows transcoding to be performed in real-time as the video is being streamed. Enables quality switching, but may cause higher playback latency and stuttering depending on server capabilities.",
"transcoding_realtime_enabled": "Enable real-time transcoding",
"transcoding_realtime_enabled_description": "If disabled, the server will refuse to start new real-time transcoding sessions.",
"transcoding_reference_frames": "Reference frames", "transcoding_reference_frames": "Reference frames",
"transcoding_reference_frames_description": "The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically.", "transcoding_reference_frames_description": "The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically.",
"transcoding_required_description": "Only videos not in an accepted format", "transcoding_required_description": "Only videos not in an accepted format",
@ -442,6 +448,8 @@
"user_settings_description": "Manage user settings", "user_settings_description": "Manage user settings",
"user_successfully_removed": "User {email} has been successfully removed.", "user_successfully_removed": "User {email} has been successfully removed.",
"users_page_description": "Admin users page", "users_page_description": "Admin users page",
"version_check_channel": "Release channel",
"version_check_channel_description": "Pick the release channel you want to get version announcements for",
"version_check_enabled_description": "Enable version check", "version_check_enabled_description": "Enable version check",
"version_check_implications": "The version check feature relies on periodic communication with {server}", "version_check_implications": "The version check feature relies on periodic communication with {server}",
"version_check_settings": "Version Check", "version_check_settings": "Version Check",
@ -1584,6 +1592,8 @@
"merge_people_prompt": "Do you want to merge these people? This action is irreversible.", "merge_people_prompt": "Do you want to merge these people? This action is irreversible.",
"merge_people_successfully": "Merge people successfully", "merge_people_successfully": "Merge people successfully",
"merged_people_count": "Merged {count, plural, one {# person} other {# people}}", "merged_people_count": "Merged {count, plural, one {# person} other {# people}}",
"minFaces": "Minimum faces",
"minFaces_description": "The minimum number of recognized faces for a person to be displayed",
"minimize": "Minimize", "minimize": "Minimize",
"minute": "Minute", "minute": "Minute",
"minutes": "Minutes", "minutes": "Minutes",
@ -2233,6 +2243,7 @@
"slideshow_repeat": "Repeat slideshow", "slideshow_repeat": "Repeat slideshow",
"slideshow_repeat_description": "Loop back to beginning when slideshow ends", "slideshow_repeat_description": "Loop back to beginning when slideshow ends",
"slideshow_settings": "Slideshow settings", "slideshow_settings": "Slideshow settings",
"smart_album": "Smart album",
"sort_albums_by": "Sort albums by...", "sort_albums_by": "Sort albums by...",
"sort_created": "Date created", "sort_created": "Date created",
"sort_items": "Number of items", "sort_items": "Number of items",
@ -2451,6 +2462,7 @@
"video": "Video", "video": "Video",
"video_hover_setting": "Play video thumbnail on hover", "video_hover_setting": "Play video thumbnail on hover",
"video_hover_setting_description": "Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon.", "video_hover_setting_description": "Play video thumbnail when mouse is hovering over item. Even when disabled, playback can be started by hovering over the play icon.",
"video_quality": "Video quality",
"videos": "Videos", "videos": "Videos",
"videos_count": "{count, plural, one {# Video} other {# Videos}}", "videos_count": "{count, plural, one {# Video} other {# Videos}}",
"videos_only": "Videos only", "videos_only": "Videos only",

View File

@ -1,8 +1,8 @@
ARG DEVICE=cpu ARG DEVICE=cpu
FROM python:3.11-bookworm@sha256:970c99f886b839fc8829289040c1845dadaf2cae46b37acc7710333158ec29b4 AS builder-cpu FROM python:3.11-bookworm@sha256:121d86b6d08752968a7dddbc708849e5f3a839bbff47f32212b46d2a1d842bab AS builder-cpu
FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec5fb2f8f6403244621664 AS builder-openvino FROM python:3.13-slim-trixie@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 AS builder-openvino
FROM builder-cpu AS builder-cuda FROM builder-cpu AS builder-cuda
@ -39,12 +39,12 @@ RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy
FROM python:3.11-slim-bookworm@sha256:9c6f90801e6b68e772b7c0ca74260cbf7af9f320acec894e26fccdaccfbe3b47 AS prod-cpu FROM python:3.11-slim-bookworm@sha256:8dca233de9f3d9bb410665f00a4da6dd06f331083137e0e98ccf227236fcc438 AS prod-cpu
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \ ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
MACHINE_LEARNING_MODEL_ARENA=false MACHINE_LEARNING_MODEL_ARENA=false
FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec5fb2f8f6403244621664 AS prod-openvino FROM python:3.13-slim-trixie@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 AS prod-openvino
RUN apt-get update && \ RUN apt-get update && \
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \

View File

@ -49,6 +49,7 @@ try:
str(settings.http_keepalive_timeout_s), str(settings.http_keepalive_timeout_s),
"--graceful-timeout", "--graceful-timeout",
"10", "10",
"--no-control-socket",
], ],
) as cmd: ) as cmd:
cmd.wait() cmd.wait()

View File

@ -12,7 +12,7 @@ from zipfile import BadZipFile
import orjson import orjson
from fastapi import Depends, FastAPI, File, Form, HTTPException from fastapi import Depends, FastAPI, File, Form, HTTPException
from fastapi.responses import ORJSONResponse, PlainTextResponse from fastapi.responses import PlainTextResponse
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile
from PIL.Image import Image from PIL.Image import Image
from pydantic import ValidationError from pydantic import ValidationError
@ -32,6 +32,7 @@ from .schemas import (
ModelIdentity, ModelIdentity,
ModelTask, ModelTask,
ModelType, ModelType,
ORJSONResponse,
PipelineRequest, PipelineRequest,
T, T,
) )

View File

@ -89,7 +89,9 @@ class OpenClipTextualEncoder(BaseCLIPTextualEncoder):
tokenizer: Tokenizer = Tokenizer.from_file(self.tokenizer_file_path.as_posix()) tokenizer: Tokenizer = Tokenizer.from_file(self.tokenizer_file_path.as_posix())
pad_id: int = tokenizer.token_to_id(pad_token) pad_id = tokenizer.token_to_id(pad_token)
if pad_id is None:
raise ValueError(f"Pad token '{pad_token}' not found in tokenizer vocab")
tokenizer.enable_padding(length=context_length, pad_token=pad_token, pad_id=pad_id) tokenizer.enable_padding(length=context_length, pad_token=pad_token, pad_id=pad_id)
tokenizer.enable_truncation(max_length=context_length) tokenizer.enable_truncation(max_length=context_length)

View File

@ -64,6 +64,7 @@ class TextRecognizer(InferenceModel):
rec_batch_num=max_batch_size if max_batch_size else 6, rec_batch_num=max_batch_size if max_batch_size else 6,
rec_img_shape=(3, 48, 320), rec_img_shape=(3, 48, 320),
lang_type=self.language, lang_type=self.language,
model_root_dir=self.cache_dir,
) )
) )
return session return session

View File

@ -3,9 +3,16 @@ from typing import Any, Literal, Protocol, TypeGuard, TypeVar
import numpy as np import numpy as np
import numpy.typing as npt import numpy.typing as npt
import orjson
from fastapi.responses import JSONResponse
from typing_extensions import TypedDict from typing_extensions import TypedDict
class ORJSONResponse(JSONResponse):
def render(self, content: Any) -> bytes:
return orjson.dumps(content, option=orjson.OPT_SERIALIZE_NUMPY)
class StrEnum(str, Enum): class StrEnum(str, Enum):
value: str value: str

View File

@ -1028,7 +1028,12 @@ class TestOcr:
text_recognizer.load() text_recognizer.load()
rapid_recognizer.assert_called_once_with( rapid_recognizer.assert_called_once_with(
OcrOptions(session=ort_session.return_value, rec_batch_num=6, rec_img_shape=(3, 48, 320)) OcrOptions(
session=ort_session.return_value,
rec_batch_num=6,
rec_img_shape=(3, 48, 320),
model_root_dir=text_recognizer.cache_dir,
)
) )
def test_set_custom_max_batch_size(self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture) -> None: def test_set_custom_max_batch_size(self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture) -> None:
@ -1041,7 +1046,12 @@ class TestOcr:
text_recognizer.load() text_recognizer.load()
rapid_recognizer.assert_called_once_with( rapid_recognizer.assert_called_once_with(
OcrOptions(session=ort_session.return_value, rec_batch_num=4, rec_img_shape=(3, 48, 320)) OcrOptions(
session=ort_session.return_value,
rec_batch_num=4,
rec_img_shape=(3, 48, 320),
model_root_dir=text_recognizer.cache_dir,
)
) )
def test_ignore_other_custom_max_batch_size( def test_ignore_other_custom_max_batch_size(
@ -1056,7 +1066,12 @@ class TestOcr:
text_recognizer.load() text_recognizer.load()
rapid_recognizer.assert_called_once_with( rapid_recognizer.assert_called_once_with(
OcrOptions(session=ort_session.return_value, rec_batch_num=6, rec_img_shape=(3, 48, 320)) OcrOptions(
session=ort_session.return_value,
rec_batch_num=6,
rec_img_shape=(3, 48, 320),
model_root_dir=text_recognizer.cache_dir,
)
) )

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,30 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html # @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools."aqua:flutter/flutter"]] [[tools."aqua:flutter/flutter"]]
version = "3.44.0" version = "3.44.1"
backend = "aqua:flutter/flutter" backend = "aqua:flutter/flutter"
[tools."aqua:flutter/flutter"."platforms.linux-arm64"] [tools."aqua:flutter/flutter"."platforms.linux-arm64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz" url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.linux-arm64-musl"] [tools."aqua:flutter/flutter"."platforms.linux-arm64-musl"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz" url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.linux-x64"] [tools."aqua:flutter/flutter"."platforms.linux-x64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz" url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.linux-x64-musl"] [tools."aqua:flutter/flutter"."platforms.linux-x64-musl"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz" url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.1-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.macos-arm64"] [tools."aqua:flutter/flutter"."platforms.macos-arm64"]
checksum = "blake3:fb03aa5d9790205c948922ec3f0751c16e4575b09d6ae9dd4fbeb664a69f0e00" checksum = "blake3:15069c982a30ca0189a83edb5627b69d91485ad94fb74d2de8585b43364e9e8e"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.0-stable.zip" url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.1-stable.zip"
[tools."aqua:flutter/flutter"."platforms.macos-x64"] [tools."aqua:flutter/flutter"."platforms.macos-x64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.0-stable.zip" url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.1-stable.zip"
[tools."aqua:flutter/flutter"."platforms.windows-x64"] [tools."aqua:flutter/flutter"."platforms.windows-x64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.0-stable.zip" url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.1-stable.zip"
[[tools.flutter]] [[tools.flutter]]
version = "3.41.9-stable" version = "3.41.9-stable"

View File

@ -16,7 +16,7 @@ config_roots = [
[tools] [tools]
node = "24.15.0" node = "24.15.0"
"aqua:flutter/flutter" = "3.44.0" "aqua:flutter/flutter" = "3.44.1"
pnpm = "10.33.4" pnpm = "10.33.4"
terragrunt = "1.0.3" terragrunt = "1.0.3"
opentofu = "1.11.6" opentofu = "1.11.6"
@ -54,8 +54,8 @@ lockfile = true
[tasks.plugins] [tasks.plugins]
run = [ run = [
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile", "pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile",
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core build", "pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter @immich/plugin-core build",
] ]
[tasks.open-api-typescript] [tasks.open-api-typescript]
@ -108,7 +108,7 @@ depends = "//:plugins"
dir = "docker" dir = "docker"
interactive = true interactive = true
env = { COMPOSE_BAKE = true } env = { COMPOSE_BAKE = true }
run = "docker compose -f ./docker-compose.prod.yml up --remove-orphans" run = "docker compose -f ./docker-compose.prod.yml up --build --remove-orphans"
depends_post = "//:prod-down" depends_post = "//:prod-down"
[tasks.prod-scale] [tasks.prod-scale]

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */; }; 467DA6EAF83F3481F8BD94AB /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8AB817AA297EDEC88B23F3F6 /* Pods_ShareExtension.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
@ -22,7 +22,7 @@
B2EE00022E72CA15008B6CA7 /* PermissionApi.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */; }; B2EE00022E72CA15008B6CA7 /* PermissionApi.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */; };
B2EE00042E72CA15008B6CA7 /* PermissionApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */; }; B2EE00042E72CA15008B6CA7 /* PermissionApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */; };
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; }; B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; };
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; }; D3BED739C0BC29BB32E18EB2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CC499FBCE6B29B2DAFED7130 /* Pods_Runner.framework */; };
F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
F0B57D3A2DF764BD00DC5BCC /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */; }; F0B57D3A2DF764BD00DC5BCC /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */; };
F0B57D3C2DF764BD00DC5BCC /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */; }; F0B57D3C2DF764BD00DC5BCC /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */; };
@ -85,16 +85,18 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
10B378D23F917891A0F23E33 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = "<group>"; }; 614A7F5DC5DB09E89E4FCBE8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
681FBA560D5D2ADDE4F0B59E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
6D160F04A389B9FFBC557803 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8AB817AA297EDEC88B23F3F6 /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
937632897A02DE9C249F20A6 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Immich-Debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Immich-Debug.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146EE1CF9000F007C117D /* Immich-Debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Immich-Debug.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@ -103,7 +105,6 @@
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A01DD6982F7F43B40049AB63 /* ImageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRequest.swift; sourceTree = "<group>"; }; A01DD6982F7F43B40049AB63 /* ImageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRequest.swift; sourceTree = "<group>"; };
B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = "<group>"; }; B21E34A92E5AFD210031FDB9 /* BackgroundWorkerApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkerApiImpl.swift; sourceTree = "<group>"; };
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; }; B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = "<group>"; }; B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = "<group>"; };
@ -111,12 +112,11 @@
B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApi.g.swift; sourceTree = "<group>"; }; B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApi.g.swift; sourceTree = "<group>"; };
B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApiImpl.swift; sourceTree = "<group>"; }; B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApiImpl.swift; sourceTree = "<group>"; };
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; }; B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; };
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; C4A6A71F33CE37B3C913115C /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
CC499FBCE6B29B2DAFED7130 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
FA9973382CF6DF4B000EF859 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; }; FA9973382CF6DF4B000EF859 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
FAC6F8902D287C890078CB2F /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; FAC6F8902D287C890078CB2F /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
FAC6F8B12D287F120078CB2F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; FAC6F8B12D287F120078CB2F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -199,7 +199,7 @@
FEE084F82EC172460045228E /* SQLiteData in Frameworks */, FEE084F82EC172460045228E /* SQLiteData in Frameworks */,
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */, FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */,
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */, FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */,
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */, D3BED739C0BC29BB32E18EB2 /* Pods_Runner.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -216,7 +216,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */, 467DA6EAF83F3481F8BD94AB /* Pods_ShareExtension.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -226,12 +226,12 @@
0FB772A5B9601143383626CA /* Pods */ = { 0FB772A5B9601143383626CA /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */, 614A7F5DC5DB09E89E4FCBE8 /* Pods-Runner.debug.xcconfig */,
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */, 6D160F04A389B9FFBC557803 /* Pods-Runner.release.xcconfig */,
F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */, 681FBA560D5D2ADDE4F0B59E /* Pods-Runner.profile.xcconfig */,
F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */, 937632897A02DE9C249F20A6 /* Pods-ShareExtension.debug.xcconfig */,
571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */, 10B378D23F917891A0F23E33 /* Pods-ShareExtension.release.xcconfig */,
B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */, C4A6A71F33CE37B3C913115C /* Pods-ShareExtension.profile.xcconfig */,
); );
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
@ -239,10 +239,10 @@
1754452DD81DA6620E279E51 /* Frameworks */ = { 1754452DD81DA6620E279E51 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */,
357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */,
F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */, F0B57D392DF764BD00DC5BCC /* WidgetKit.framework */,
F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */, F0B57D3B2DF764BD00DC5BCC /* SwiftUI.framework */,
CC499FBCE6B29B2DAFED7130 /* Pods_Runner.framework */,
8AB817AA297EDEC88B23F3F6 /* Pods_ShareExtension.framework */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@ -370,7 +370,7 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
4044AF030EF7D8721844FFBA /* [CP] Check Pods Manifest.lock */, BAEA01ACA3F5C9CD3D732370 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
@ -378,8 +378,8 @@
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */, FAC6F89A2D287C890078CB2F /* Embed Foundation Extensions */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */, 513DA7292DED6106813332F4 /* [CP] Embed Pods Frameworks */,
6724EEB7D74949FA08581154 /* [CP] Copy Pods Resources */, 2FA39DEC809D6D7C4A01EFCB /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@ -393,6 +393,9 @@
FEE084F22EC172080045228E /* Schemas */, FEE084F22EC172080045228E /* Schemas */,
); );
name = Runner; name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner; productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */; productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
@ -421,7 +424,7 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = FAC6F8A02D287C890078CB2F /* Build configuration list for PBXNativeTarget "ShareExtension" */; buildConfigurationList = FAC6F8A02D287C890078CB2F /* Build configuration list for PBXNativeTarget "ShareExtension" */;
buildPhases = ( buildPhases = (
3BEF3D71D97E337D921C0EB5 /* [CP] Check Pods Manifest.lock */, 8EC9CF3E20AF32BF24D4F3E1 /* [CP] Check Pods Manifest.lock */,
FAC6F88C2D287C890078CB2F /* Sources */, FAC6F88C2D287C890078CB2F /* Sources */,
FAC6F88D2D287C890078CB2F /* Frameworks */, FAC6F88D2D287C890078CB2F /* Frameworks */,
FAC6F88E2D287C890078CB2F /* Resources */, FAC6F88E2D287C890078CB2F /* Resources */,
@ -470,7 +473,7 @@
); );
mainGroup = 97C146E51CF9000F007C117D; mainGroup = 97C146E51CF9000F007C117D;
packageReferences = ( packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */, FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */, FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
); );
@ -517,6 +520,23 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
2FA39DEC809D6D7C4A01EFCB /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
@ -533,7 +553,24 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
}; };
3BEF3D71D97E337D921C0EB5 /* [CP] Check Pods Manifest.lock */ = { 513DA7292DED6106813332F4 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
8EC9CF3E20AF32BF24D4F3E1 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -555,7 +592,22 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
4044AF030EF7D8721844FFBA /* [CP] Check Pods Manifest.lock */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
BAEA01ACA3F5C9CD3D732370 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@ -577,55 +629,6 @@
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
6724EEB7D74949FA08581154 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n";
};
D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@ -1092,7 +1095,7 @@
}; };
FAC6F89C2D287C890078CB2F /* Debug */ = { FAC6F89C2D287C890078CB2F /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = F8A35EA3C3E01BD66AFDE0E5 /* Pods-ShareExtension.debug.xcconfig */; baseConfigurationReference = 937632897A02DE9C249F20A6 /* Pods-ShareExtension.debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@ -1135,7 +1138,7 @@
}; };
FAC6F89D2D287C890078CB2F /* Release */ = { FAC6F89D2D287C890078CB2F /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */; baseConfigurationReference = 10B378D23F917891A0F23E33 /* Pods-ShareExtension.release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@ -1175,7 +1178,7 @@
}; };
FAC6F89E2D287C890078CB2F /* Profile */ = { FAC6F89E2D287C890078CB2F /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = B1FBA9EE014DE20271B0FE77 /* Pods-ShareExtension.profile.xcconfig */; baseConfigurationReference = C4A6A71F33CE37B3C913115C /* Pods-ShareExtension.profile.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@ -1258,6 +1261,13 @@
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */ = { FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
@ -1278,6 +1288,10 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
FEE084F72EC172460045228E /* SQLiteData */ = { FEE084F72EC172460045228E /* SQLiteData */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */; package = FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */;

View File

@ -5,8 +5,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/combine-schedulers", "location" : "https://github.com/pointfreeco/combine-schedulers",
"state" : { "state" : {
"revision" : "5928286acce13def418ec36d05a001a9641086f2", "revision" : "fd16d76fd8b9a976d88bfb6cacc05ca8d19c91b6",
"version" : "1.0.3" "version" : "1.1.0"
} }
}, },
{ {

View File

@ -49,7 +49,7 @@ def get_version_from_pubspec
pubspec = YAML.load_file(pubspec_path) pubspec = YAML.load_file(pubspec_path)
version_string = pubspec['version'] version_string = pubspec['version']
version_string ? version_string.split('+').first : nil version_string ? version_string.split('+').first.split('-').first : nil
end end
# Helper method to configure code signing for all targets # Helper method to configure code signing for all targets

View File

@ -12,7 +12,7 @@ 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/timeline_config.dart';
import 'package:immich_mobile/domain/models/config/viewer_config.dart'; import 'package:immich_mobile/domain/models/config/viewer_config.dart';
import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
@ -95,7 +95,7 @@ class AppConfig {
String toString() => String toString() =>
'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network)'; 'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network)';
T read<T extends Object>(MetadataKey<T> key) => T read<T extends Object>(SettingsKey<T> key) =>
(switch (key) { (switch (key) {
.logLevel => logLevel, .logLevel => logLevel,
.themePrimaryColor => theme.primaryColor, .themePrimaryColor => theme.primaryColor,
@ -143,15 +143,10 @@ class AppConfig {
}) })
as T; as T;
factory AppConfig.fromEntries(Map<MetadataKey<Object>, Object> entries) { factory AppConfig.fromEntries(Map<SettingsKey<Object>, Object> overrides) =>
var config = const AppConfig(); overrides.entries.fold(const AppConfig(), (config, entry) => config.write(entry.key, entry.value));
for (final MapEntry(key: key, value: value) in entries.entries) {
config = config.write(key, value);
}
return config;
}
AppConfig write<T extends Object>(MetadataKey<T> key, T value) { AppConfig write<T extends Object>(SettingsKey<T> key, T value) {
return switch (key) { return switch (key) {
.logLevel => copyWith(logLevel: value as LogLevel), .logLevel => copyWith(logLevel: value as LogLevel),
.themePrimaryColor => copyWith(theme: theme.copyWith(primaryColor: value as ImmichColorPreset)), .themePrimaryColor => copyWith(theme: theme.copyWith(primaryColor: value as ImmichColorPreset)),

View File

@ -7,14 +7,7 @@ import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
enum MetadataScope { enum SettingsKey<T extends Object> {
user, // keys with this scope are deleted on logout
system;
const MetadataScope();
}
enum MetadataKey<T extends Object> {
// Theme // Theme
themePrimaryColor<ImmichColorPreset>(codec: _EnumCodec(ImmichColorPreset.values)), themePrimaryColor<ImmichColorPreset>(codec: _EnumCodec(ImmichColorPreset.values)),
themeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)), themeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
@ -32,14 +25,11 @@ enum MetadataKey<T extends Object> {
viewerTapToNavigate<bool>(), viewerTapToNavigate<bool>(),
// Network // Network
networkAutoEndpointSwitching<bool>(scope: .system), networkAutoEndpointSwitching<bool>(),
networkPreferredWifiName<String>(scope: .system), networkPreferredWifiName<String>(),
networkLocalEndpoint<String>(scope: .system), networkLocalEndpoint<String>(),
networkExternalEndpointList<List<String>>(scope: .system, codec: _ListCodec(_PrimitiveCodec.string)), networkExternalEndpointList<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
networkCustomHeaders<Map<String, String>>( networkCustomHeaders<Map<String, String>>(codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string)),
scope: .system,
codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string),
),
// Album // Album
albumSortMode<AlbumSortMode>(codec: _EnumCodec(AlbumSortMode.values)), albumSortMode<AlbumSortMode>(codec: _EnumCodec(AlbumSortMode.values)),
@ -60,7 +50,7 @@ enum MetadataKey<T extends Object> {
timelineStorageIndicator<bool>(), timelineStorageIndicator<bool>(),
// Log // Log
logLevel<LogLevel>(scope: .system, codec: _EnumCodec(LogLevel.values)), logLevel<LogLevel>(codec: _EnumCodec(LogLevel.values)),
// Map // Map
mapShowFavoriteOnly<bool>(), mapShowFavoriteOnly<bool>(),
@ -83,25 +73,24 @@ enum MetadataKey<T extends Object> {
slideshowLook<SlideshowLook>(codec: _EnumCodec(SlideshowLook.values)), slideshowLook<SlideshowLook>(codec: _EnumCodec(SlideshowLook.values)),
slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values)); slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values));
final MetadataScope scope; final _SettingsCodec<T>? _codecOverride;
final _MetadataCodec<T>? _codecOverride;
const MetadataKey({this.scope = .user, _MetadataCodec<T>? codec}) : _codecOverride = codec; const SettingsKey({_SettingsCodec<T>? codec}) : _codecOverride = codec;
_MetadataCodec<T> get _codec => _codecOverride ?? _MetadataCodec.forType(T); _SettingsCodec<T> get _codec => _codecOverride ?? _SettingsCodec.forType(T);
String encode(T value) => _codec.encode(value); String encode(T value) => _codec.encode(value);
T decode(String raw) => _codec.decode(raw); T decode(String raw) => _codec.decode(raw);
} }
sealed class _MetadataCodec<T extends Object> { sealed class _SettingsCodec<T extends Object> {
const _MetadataCodec(); const _SettingsCodec();
String encode(T value); String encode(T value);
T decode(String raw); T decode(String raw);
static const Map<Type, _MetadataCodec<Object>> _primitives = { static const Map<Type, _SettingsCodec<Object>> _primitives = {
int: _PrimitiveCodec.integer, int: _PrimitiveCodec.integer,
double: _PrimitiveCodec.real, double: _PrimitiveCodec.real,
bool: _PrimitiveCodec.boolean, bool: _PrimitiveCodec.boolean,
@ -109,16 +98,16 @@ sealed class _MetadataCodec<T extends Object> {
DateTime: _DateTimeCodec(), DateTime: _DateTimeCodec(),
}; };
static _MetadataCodec<T> forType<T extends Object>(Type runtimeType) { static _SettingsCodec<T> forType<T extends Object>(Type runtimeType) {
final codec = _primitives[runtimeType]; final codec = _primitives[runtimeType];
if (codec == null) { if (codec == null) {
throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the MetadataKey.'); throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the SettingsKey.');
} }
return codec as _MetadataCodec<T>; return codec as _SettingsCodec<T>;
} }
} }
final class _EnumCodec<T extends Enum> extends _MetadataCodec<T> { final class _EnumCodec<T extends Enum> extends _SettingsCodec<T> {
final List<T> values; final List<T> values;
const _EnumCodec(this.values); const _EnumCodec(this.values);
@ -130,7 +119,7 @@ final class _EnumCodec<T extends Enum> extends _MetadataCodec<T> {
T decode(String raw) => values.firstWhere((v) => v.name == raw); T decode(String raw) => values.firstWhere((v) => v.name == raw);
} }
final class _DateTimeCodec extends _MetadataCodec<DateTime> { final class _DateTimeCodec extends _SettingsCodec<DateTime> {
const _DateTimeCodec(); const _DateTimeCodec();
@override @override
@ -140,9 +129,9 @@ final class _DateTimeCodec extends _MetadataCodec<DateTime> {
DateTime decode(String raw) => DateTime.parse(raw); DateTime decode(String raw) => DateTime.parse(raw);
} }
final class _MapCodec<K extends Object, V extends Object> extends _MetadataCodec<Map<K, V>> { final class _MapCodec<K extends Object, V extends Object> extends _SettingsCodec<Map<K, V>> {
final _MetadataCodec<K> _keyCodec; final _SettingsCodec<K> _keyCodec;
final _MetadataCodec<V> _valueCodec; final _SettingsCodec<V> _valueCodec;
const _MapCodec(this._keyCodec, this._valueCodec); const _MapCodec(this._keyCodec, this._valueCodec);
@ -178,8 +167,8 @@ final class _MapCodec<K extends Object, V extends Object> extends _MetadataCodec
} }
} }
final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> { final class _ListCodec<T extends Object> extends _SettingsCodec<List<T>> {
final _MetadataCodec<T> _elementCodec; final _SettingsCodec<T> _elementCodec;
const _ListCodec(this._elementCodec); const _ListCodec(this._elementCodec);
@ -208,7 +197,7 @@ final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
} }
} }
final class _PrimitiveCodec<T extends Object> extends _MetadataCodec<T> { final class _PrimitiveCodec<T extends Object> extends _SettingsCodec<T> {
final T Function(String) _parse; final T Function(String) _parse;
const _PrimitiveCodec._(this._parse); const _PrimitiveCodec._(this._parse);

View File

@ -11,7 +11,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart'; import 'package:immich_mobile/platform/background_worker_api.g.dart';
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
@ -39,7 +39,7 @@ class BackgroundWorkerFgService {
_foregroundHostApi.saveNotificationMessage(title, body); _foregroundHostApi.saveNotificationMessage(title, body);
Future<void> configure({int? minimumDelaySeconds, bool? requireCharging}) { Future<void> configure({int? minimumDelaySeconds, bool? requireCharging}) {
final backup = MetadataRepository.instance.appConfig.backup; final backup = SettingsRepository.instance.appConfig.backup;
return _foregroundHostApi.configure( return _foregroundHostApi.configure(
BackgroundWorkerSettings( BackgroundWorkerSettings(
minimumDelaySeconds: minimumDelaySeconds ?? backup.triggerDelay, minimumDelaySeconds: minimumDelaySeconds ?? backup.triggerDelay,
@ -67,7 +67,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
BackgroundWorkerFlutterApi.setUp(this); BackgroundWorkerFlutterApi.setUp(this);
} }
bool get _isBackupEnabled => MetadataRepository.instance.appConfig.backup.enabled; bool get _isBackupEnabled => SettingsRepository.instance.appConfig.backup.enabled;
Future<void> init() async { Future<void> init() async {
try { try {

View File

@ -2,9 +2,9 @@ import 'dart:async';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -12,10 +12,10 @@ import 'package:logging/logging.dart';
/// ///
/// It listens to Dart's [Logger.root], buffers logs in memory (optionally), /// It listens to Dart's [Logger.root], buffers logs in memory (optionally),
/// writes them to a persistent [LogRepository], and manages log levels via /// writes them to a persistent [LogRepository], and manages log levels via
/// [MetadataRepository]. /// [SettingsRepository].
class LogService { class LogService {
final LogRepository _logRepository; final LogRepository _logRepository;
final MetadataRepository _metadataRepository; final SettingsRepository _settingsRepository;
final List<LogMessage> _msgBuffer = []; final List<LogMessage> _msgBuffer = [];
@ -38,12 +38,12 @@ class LogService {
static Future<LogService> init({ static Future<LogService> init({
required LogRepository logRepository, required LogRepository logRepository,
required MetadataRepository metadataRepository, required SettingsRepository settingsRepository,
bool shouldBuffer = true, bool shouldBuffer = true,
}) async { }) async {
_instance ??= await create( _instance ??= await create(
logRepository: logRepository, logRepository: logRepository,
metadataRepository: metadataRepository, settingsRepository: settingsRepository,
shouldBuffer: shouldBuffer, shouldBuffer: shouldBuffer,
); );
return _instance!; return _instance!;
@ -51,17 +51,17 @@ class LogService {
static Future<LogService> create({ static Future<LogService> create({
required LogRepository logRepository, required LogRepository logRepository,
required MetadataRepository metadataRepository, required SettingsRepository settingsRepository,
bool shouldBuffer = true, bool shouldBuffer = true,
}) async { }) async {
final instance = LogService._(logRepository, metadataRepository, shouldBuffer); final instance = LogService._(logRepository, settingsRepository, shouldBuffer);
await logRepository.truncate(limit: kLogTruncateLimit); await logRepository.truncate(limit: kLogTruncateLimit);
final level = instance._metadataRepository.appConfig.logLevel; final level = instance._settingsRepository.appConfig.logLevel;
Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO; Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO;
return instance; return instance;
} }
LogService._(this._logRepository, this._metadataRepository, this._shouldBuffer) { LogService._(this._logRepository, this._settingsRepository, this._shouldBuffer) {
_logSubscription = Logger.root.onRecord.listen(_handleLogRecord); _logSubscription = Logger.root.onRecord.listen(_handleLogRecord);
} }
@ -91,7 +91,7 @@ class LogService {
} }
Future<void> setLogLevel(LogLevel level) async { Future<void> setLogLevel(LogLevel level) async {
await _metadataRepository.write(MetadataKey.logLevel, level); await _settingsRepository.write(SettingsKey.logLevel, level);
Logger.root.level = level.toLevel(); Logger.root.level = level.toLevel();
} }

View File

@ -192,43 +192,30 @@ class RemoteAlbumService {
required UserDto uploader, required UserDto uploader,
required AlbumAssetCandidates candidates, required AlbumAssetCandidates candidates,
UploadCallbacks uploadCallbacks = const UploadCallbacks(), UploadCallbacks uploadCallbacks = const UploadCallbacks(),
Completer<void>? cancelToken,
}) async { }) async {
int addedCount = 0; int addedCount = 0;
if (candidates.remoteAssetIds.isNotEmpty) { if (candidates.remoteAssetIds.isNotEmpty) {
addedCount += await addAssets(albumId: albumId, assetIds: candidates.remoteAssetIds); addedCount += await addAssets(albumId: albumId, assetIds: candidates.remoteAssetIds);
} }
if (candidates.localAssetsToUpload.isNotEmpty) { if (candidates.localAssetsToUpload.isNotEmpty) {
addedCount += await _uploadAndAddLocals(albumId, uploader, candidates.localAssetsToUpload, uploadCallbacks); addedCount += await _uploadAndAddLocals(
albumId,
uploader,
candidates.localAssetsToUpload,
uploadCallbacks,
cancelToken,
);
} }
return addedCount; return addedCount;
} }
/// Creates an album, seeding it with already-remote asset IDs, then uploads
/// local-only assets and links each one as it finishes.
Future<RemoteAlbum> createAlbumWithAssets({
required String title,
required UserDto owner,
String? description,
AlbumAssetCandidates candidates = const AlbumAssetCandidates(remoteAssetIds: [], localAssetsToUpload: []),
UploadCallbacks uploadCallbacks = const UploadCallbacks(),
}) async {
final album = await createAlbum(
title: title,
owner: owner,
description: description,
assetIds: candidates.remoteAssetIds,
);
if (candidates.localAssetsToUpload.isNotEmpty) {
await _uploadAndAddLocals(album.id, owner, candidates.localAssetsToUpload, uploadCallbacks);
}
return album;
}
Future<int> _uploadAndAddLocals( Future<int> _uploadAndAddLocals(
String albumId, String albumId,
UserDto uploader, UserDto uploader,
List<LocalAsset> localAssets, List<LocalAsset> localAssets,
UploadCallbacks userCallbacks, UploadCallbacks userCallbacks,
Completer<void>? cancelToken,
) async { ) async {
int addedCount = 0; int addedCount = 0;
final pendingAdds = <Future<void>>[]; final pendingAdds = <Future<void>>[];
@ -258,7 +245,7 @@ class RemoteAlbumService {
return; return;
} }
pendingAdds.add( pendingAdds.add(
_linkUploadedAssetToAlbum(albumId, remoteId, uploader, source) linkUploadedAssetToAlbum(albumId, remoteId, uploader, source)
.then<void>((added) { .then<void>((added) {
addedCount += added; addedCount += added;
}) })
@ -269,7 +256,7 @@ class RemoteAlbumService {
}, },
); );
await _uploadService.uploadManual(localAssets, callbacks: wrappedCallbacks); await _uploadService.uploadManual(localAssets, callbacks: wrappedCallbacks, cancelToken: cancelToken);
await Future.wait(pendingAdds); await Future.wait(pendingAdds);
return addedCount; return addedCount;
} }
@ -288,7 +275,7 @@ class RemoteAlbumService {
/// `remote_asset_entity` row from the local source so the FK-protected /// `remote_asset_entity` row from the local source so the FK-protected
/// junction insert succeeds. Sync overwrites the placeholder later with /// junction insert succeeds. Sync overwrites the placeholder later with
/// the authoritative server data. /// the authoritative server data.
Future<int> _linkUploadedAssetToAlbum(String albumId, String remoteId, UserDto uploader, LocalAsset source) async { Future<int> linkUploadedAssetToAlbum(String albumId, String remoteId, UserDto uploader, LocalAsset source) async {
final result = await _albumApiRepository.addAssets(albumId, [remoteId]); final result = await _albumApiRepository.addAssets(albumId, [remoteId]);
if (result.added.isEmpty) { if (result.added.isEmpty) {
return 0; return 0;

View File

@ -7,7 +7,7 @@ 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/events.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/utils/async_mutex.dart'; import 'package:immich_mobile/utils/async_mutex.dart';
@ -39,12 +39,12 @@ enum TimelineOrigin {
class TimelineFactory { class TimelineFactory {
final DriftTimelineRepository _timelineRepository; final DriftTimelineRepository _timelineRepository;
final MetadataRepository _metadataRepository; final SettingsRepository _settingsRepository;
const TimelineFactory({required this._timelineRepository, required this._metadataRepository}); const TimelineFactory({required this._timelineRepository, required this._settingsRepository});
GroupAssetsBy get groupBy { GroupAssetsBy get groupBy {
final group = _metadataRepository.appConfig.timeline.groupAssetsBy; final group = _settingsRepository.appConfig.timeline.groupAssetsBy;
// We do not support auto grouping in the new timeline yet, fallback to day grouping // We do not support auto grouping in the new timeline yet, fallback to day grouping
return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group; return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group;
} }

View File

@ -1,8 +1,8 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class MetadataEntity extends Table with DriftDefaultsMixin { class SettingsEntity extends Table with DriftDefaultsMixin {
const MetadataEntity(); const SettingsEntity();
TextColumn get key => text()(); TextColumn get key => text()();
@ -14,5 +14,5 @@ class MetadataEntity extends Table with DriftDefaultsMixin {
Set<Column> get primaryKey => {key}; Set<Column> get primaryKey => {key};
@override @override
String get tableName => "metadata"; String get tableName => "settings";
} }

View File

@ -1,28 +1,28 @@
// dart format width=80 // dart format width=80
// ignore_for_file: type=lint // ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0; import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'
as i1; as i1;
import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart' import 'package:immich_mobile/infrastructure/entities/settings.entity.dart'
as i2; as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3; import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
typedef $$MetadataEntityTableCreateCompanionBuilder = typedef $$SettingsEntityTableCreateCompanionBuilder =
i1.MetadataEntityCompanion Function({ i1.SettingsEntityCompanion Function({
required String key, required String key,
required String value, required String value,
i0.Value<DateTime> updatedAt, i0.Value<DateTime> updatedAt,
}); });
typedef $$MetadataEntityTableUpdateCompanionBuilder = typedef $$SettingsEntityTableUpdateCompanionBuilder =
i1.MetadataEntityCompanion Function({ i1.SettingsEntityCompanion Function({
i0.Value<String> key, i0.Value<String> key,
i0.Value<String> value, i0.Value<String> value,
i0.Value<DateTime> updatedAt, i0.Value<DateTime> updatedAt,
}); });
class $$MetadataEntityTableFilterComposer class $$SettingsEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> { extends i0.Composer<i0.GeneratedDatabase, i1.$SettingsEntityTable> {
$$MetadataEntityTableFilterComposer({ $$SettingsEntityTableFilterComposer({
required super.$db, required super.$db,
required super.$table, required super.$table,
super.joinBuilder, super.joinBuilder,
@ -45,9 +45,9 @@ class $$MetadataEntityTableFilterComposer
); );
} }
class $$MetadataEntityTableOrderingComposer class $$SettingsEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> { extends i0.Composer<i0.GeneratedDatabase, i1.$SettingsEntityTable> {
$$MetadataEntityTableOrderingComposer({ $$SettingsEntityTableOrderingComposer({
required super.$db, required super.$db,
required super.$table, required super.$table,
super.joinBuilder, super.joinBuilder,
@ -70,9 +70,9 @@ class $$MetadataEntityTableOrderingComposer
); );
} }
class $$MetadataEntityTableAnnotationComposer class $$SettingsEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> { extends i0.Composer<i0.GeneratedDatabase, i1.$SettingsEntityTable> {
$$MetadataEntityTableAnnotationComposer({ $$SettingsEntityTableAnnotationComposer({
required super.$db, required super.$db,
required super.$table, required super.$table,
super.joinBuilder, super.joinBuilder,
@ -89,47 +89,47 @@ class $$MetadataEntityTableAnnotationComposer
$composableBuilder(column: $table.updatedAt, builder: (column) => column); $composableBuilder(column: $table.updatedAt, builder: (column) => column);
} }
class $$MetadataEntityTableTableManager class $$SettingsEntityTableTableManager
extends extends
i0.RootTableManager< i0.RootTableManager<
i0.GeneratedDatabase, i0.GeneratedDatabase,
i1.$MetadataEntityTable, i1.$SettingsEntityTable,
i1.MetadataEntityData, i1.SettingsEntityData,
i1.$$MetadataEntityTableFilterComposer, i1.$$SettingsEntityTableFilterComposer,
i1.$$MetadataEntityTableOrderingComposer, i1.$$SettingsEntityTableOrderingComposer,
i1.$$MetadataEntityTableAnnotationComposer, i1.$$SettingsEntityTableAnnotationComposer,
$$MetadataEntityTableCreateCompanionBuilder, $$SettingsEntityTableCreateCompanionBuilder,
$$MetadataEntityTableUpdateCompanionBuilder, $$SettingsEntityTableUpdateCompanionBuilder,
( (
i1.MetadataEntityData, i1.SettingsEntityData,
i0.BaseReferences< i0.BaseReferences<
i0.GeneratedDatabase, i0.GeneratedDatabase,
i1.$MetadataEntityTable, i1.$SettingsEntityTable,
i1.MetadataEntityData i1.SettingsEntityData
>, >,
), ),
i1.MetadataEntityData, i1.SettingsEntityData,
i0.PrefetchHooks Function() i0.PrefetchHooks Function()
> { > {
$$MetadataEntityTableTableManager( $$SettingsEntityTableTableManager(
i0.GeneratedDatabase db, i0.GeneratedDatabase db,
i1.$MetadataEntityTable table, i1.$SettingsEntityTable table,
) : super( ) : super(
i0.TableManagerState( i0.TableManagerState(
db: db, db: db,
table: table, table: table,
createFilteringComposer: () => createFilteringComposer: () =>
i1.$$MetadataEntityTableFilterComposer($db: db, $table: table), i1.$$SettingsEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () => createOrderingComposer: () =>
i1.$$MetadataEntityTableOrderingComposer($db: db, $table: table), i1.$$SettingsEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () => i1 createComputedFieldComposer: () => i1
.$$MetadataEntityTableAnnotationComposer($db: db, $table: table), .$$SettingsEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback: updateCompanionCallback:
({ ({
i0.Value<String> key = const i0.Value.absent(), i0.Value<String> key = const i0.Value.absent(),
i0.Value<String> value = const i0.Value.absent(), i0.Value<String> value = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(), i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.MetadataEntityCompanion( }) => i1.SettingsEntityCompanion(
key: key, key: key,
value: value, value: value,
updatedAt: updatedAt, updatedAt: updatedAt,
@ -139,7 +139,7 @@ class $$MetadataEntityTableTableManager
required String key, required String key,
required String value, required String value,
i0.Value<DateTime> updatedAt = const i0.Value.absent(), i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.MetadataEntityCompanion.insert( }) => i1.SettingsEntityCompanion.insert(
key: key, key: key,
value: value, value: value,
updatedAt: updatedAt, updatedAt: updatedAt,
@ -152,34 +152,34 @@ class $$MetadataEntityTableTableManager
); );
} }
typedef $$MetadataEntityTableProcessedTableManager = typedef $$SettingsEntityTableProcessedTableManager =
i0.ProcessedTableManager< i0.ProcessedTableManager<
i0.GeneratedDatabase, i0.GeneratedDatabase,
i1.$MetadataEntityTable, i1.$SettingsEntityTable,
i1.MetadataEntityData, i1.SettingsEntityData,
i1.$$MetadataEntityTableFilterComposer, i1.$$SettingsEntityTableFilterComposer,
i1.$$MetadataEntityTableOrderingComposer, i1.$$SettingsEntityTableOrderingComposer,
i1.$$MetadataEntityTableAnnotationComposer, i1.$$SettingsEntityTableAnnotationComposer,
$$MetadataEntityTableCreateCompanionBuilder, $$SettingsEntityTableCreateCompanionBuilder,
$$MetadataEntityTableUpdateCompanionBuilder, $$SettingsEntityTableUpdateCompanionBuilder,
( (
i1.MetadataEntityData, i1.SettingsEntityData,
i0.BaseReferences< i0.BaseReferences<
i0.GeneratedDatabase, i0.GeneratedDatabase,
i1.$MetadataEntityTable, i1.$SettingsEntityTable,
i1.MetadataEntityData i1.SettingsEntityData
>, >,
), ),
i1.MetadataEntityData, i1.SettingsEntityData,
i0.PrefetchHooks Function() i0.PrefetchHooks Function()
>; >;
class $MetadataEntityTable extends i2.MetadataEntity class $SettingsEntityTable extends i2.SettingsEntity
with i0.TableInfo<$MetadataEntityTable, i1.MetadataEntityData> { with i0.TableInfo<$SettingsEntityTable, i1.SettingsEntityData> {
@override @override
final i0.GeneratedDatabase attachedDatabase; final i0.GeneratedDatabase attachedDatabase;
final String? _alias; final String? _alias;
$MetadataEntityTable(this.attachedDatabase, [this._alias]); $SettingsEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _keyMeta = const i0.VerificationMeta('key'); static const i0.VerificationMeta _keyMeta = const i0.VerificationMeta('key');
@override @override
late final i0.GeneratedColumn<String> key = i0.GeneratedColumn<String>( late final i0.GeneratedColumn<String> key = i0.GeneratedColumn<String>(
@ -219,10 +219,10 @@ class $MetadataEntityTable extends i2.MetadataEntity
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@override @override
String get actualTableName => $name; String get actualTableName => $name;
static const String $name = 'metadata'; static const String $name = 'settings';
@override @override
i0.VerificationContext validateIntegrity( i0.VerificationContext validateIntegrity(
i0.Insertable<i1.MetadataEntityData> instance, { i0.Insertable<i1.SettingsEntityData> instance, {
bool isInserting = false, bool isInserting = false,
}) { }) {
final context = i0.VerificationContext(); final context = i0.VerificationContext();
@ -255,9 +255,9 @@ class $MetadataEntityTable extends i2.MetadataEntity
@override @override
Set<i0.GeneratedColumn> get $primaryKey => {key}; Set<i0.GeneratedColumn> get $primaryKey => {key};
@override @override
i1.MetadataEntityData map(Map<String, dynamic> data, {String? tablePrefix}) { i1.SettingsEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.MetadataEntityData( return i1.SettingsEntityData(
key: attachedDatabase.typeMapping.read( key: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string, i0.DriftSqlType.string,
data['${effectivePrefix}key'], data['${effectivePrefix}key'],
@ -274,8 +274,8 @@ class $MetadataEntityTable extends i2.MetadataEntity
} }
@override @override
$MetadataEntityTable createAlias(String alias) { $SettingsEntityTable createAlias(String alias) {
return $MetadataEntityTable(attachedDatabase, alias); return $SettingsEntityTable(attachedDatabase, alias);
} }
@override @override
@ -284,12 +284,12 @@ class $MetadataEntityTable extends i2.MetadataEntity
bool get isStrict => true; bool get isStrict => true;
} }
class MetadataEntityData extends i0.DataClass class SettingsEntityData extends i0.DataClass
implements i0.Insertable<i1.MetadataEntityData> { implements i0.Insertable<i1.SettingsEntityData> {
final String key; final String key;
final String value; final String value;
final DateTime updatedAt; final DateTime updatedAt;
const MetadataEntityData({ const SettingsEntityData({
required this.key, required this.key,
required this.value, required this.value,
required this.updatedAt, required this.updatedAt,
@ -303,12 +303,12 @@ class MetadataEntityData extends i0.DataClass
return map; return map;
} }
factory MetadataEntityData.fromJson( factory SettingsEntityData.fromJson(
Map<String, dynamic> json, { Map<String, dynamic> json, {
i0.ValueSerializer? serializer, i0.ValueSerializer? serializer,
}) { }) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer; serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return MetadataEntityData( return SettingsEntityData(
key: serializer.fromJson<String>(json['key']), key: serializer.fromJson<String>(json['key']),
value: serializer.fromJson<String>(json['value']), value: serializer.fromJson<String>(json['value']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']), updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
@ -324,17 +324,17 @@ class MetadataEntityData extends i0.DataClass
}; };
} }
i1.MetadataEntityData copyWith({ i1.SettingsEntityData copyWith({
String? key, String? key,
String? value, String? value,
DateTime? updatedAt, DateTime? updatedAt,
}) => i1.MetadataEntityData( }) => i1.SettingsEntityData(
key: key ?? this.key, key: key ?? this.key,
value: value ?? this.value, value: value ?? this.value,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,
); );
MetadataEntityData copyWithCompanion(i1.MetadataEntityCompanion data) { SettingsEntityData copyWithCompanion(i1.SettingsEntityCompanion data) {
return MetadataEntityData( return SettingsEntityData(
key: data.key.present ? data.key.value : this.key, key: data.key.present ? data.key.value : this.key,
value: data.value.present ? data.value.value : this.value, value: data.value.present ? data.value.value : this.value,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
@ -343,7 +343,7 @@ class MetadataEntityData extends i0.DataClass
@override @override
String toString() { String toString() {
return (StringBuffer('MetadataEntityData(') return (StringBuffer('SettingsEntityData(')
..write('key: $key, ') ..write('key: $key, ')
..write('value: $value, ') ..write('value: $value, ')
..write('updatedAt: $updatedAt') ..write('updatedAt: $updatedAt')
@ -356,29 +356,29 @@ class MetadataEntityData extends i0.DataClass
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
(other is i1.MetadataEntityData && (other is i1.SettingsEntityData &&
other.key == this.key && other.key == this.key &&
other.value == this.value && other.value == this.value &&
other.updatedAt == this.updatedAt); other.updatedAt == this.updatedAt);
} }
class MetadataEntityCompanion class SettingsEntityCompanion
extends i0.UpdateCompanion<i1.MetadataEntityData> { extends i0.UpdateCompanion<i1.SettingsEntityData> {
final i0.Value<String> key; final i0.Value<String> key;
final i0.Value<String> value; final i0.Value<String> value;
final i0.Value<DateTime> updatedAt; final i0.Value<DateTime> updatedAt;
const MetadataEntityCompanion({ const SettingsEntityCompanion({
this.key = const i0.Value.absent(), this.key = const i0.Value.absent(),
this.value = const i0.Value.absent(), this.value = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(),
}); });
MetadataEntityCompanion.insert({ SettingsEntityCompanion.insert({
required String key, required String key,
required String value, required String value,
this.updatedAt = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(),
}) : key = i0.Value(key), }) : key = i0.Value(key),
value = i0.Value(value); value = i0.Value(value);
static i0.Insertable<i1.MetadataEntityData> custom({ static i0.Insertable<i1.SettingsEntityData> custom({
i0.Expression<String>? key, i0.Expression<String>? key,
i0.Expression<String>? value, i0.Expression<String>? value,
i0.Expression<DateTime>? updatedAt, i0.Expression<DateTime>? updatedAt,
@ -390,12 +390,12 @@ class MetadataEntityCompanion
}); });
} }
i1.MetadataEntityCompanion copyWith({ i1.SettingsEntityCompanion copyWith({
i0.Value<String>? key, i0.Value<String>? key,
i0.Value<String>? value, i0.Value<String>? value,
i0.Value<DateTime>? updatedAt, i0.Value<DateTime>? updatedAt,
}) { }) {
return i1.MetadataEntityCompanion( return i1.SettingsEntityCompanion(
key: key ?? this.key, key: key ?? this.key,
value: value ?? this.value, value: value ?? this.value,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,
@ -419,7 +419,7 @@ class MetadataEntityCompanion
@override @override
String toString() { String toString() {
return (StringBuffer('MetadataEntityCompanion(') return (StringBuffer('SettingsEntityCompanion(')
..write('key: $key, ') ..write('key: $key, ')
..write('value: $value, ') ..write('value: $value, ')
..write('updatedAt: $updatedAt') ..write('updatedAt: $updatedAt')

View File

@ -13,7 +13,7 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart'; import 'package:immich_mobile/infrastructure/entities/settings.entity.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
import 'package:immich_mobile/infrastructure/entities/person.entity.dart'; import 'package:immich_mobile/infrastructure/entities/person.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
@ -55,7 +55,7 @@ import 'package:logging/logging.dart';
StoreEntity, StoreEntity,
TrashedLocalAssetEntity, TrashedLocalAssetEntity,
AssetEditEntity, AssetEditEntity,
MetadataEntity, SettingsEntity,
], ],
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'}, include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
) )
@ -98,7 +98,7 @@ class Drift extends $Drift {
} }
@override @override
int get schemaVersion => 26; int get schemaVersion => 27;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@ -276,6 +276,9 @@ class Drift extends $Drift {
from25To26: (m, v26) async { from25To26: (m, v26) async {
await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt); await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt);
}, },
from26To27: (m, v27) async {
await customStatement('ALTER TABLE metadata RENAME TO settings');
},
), ),
); );

View File

@ -43,7 +43,7 @@ import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity
as i20; as i20;
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'
as i21; as i21;
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'
as i22; as i22;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart' import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i23; as i23;
@ -91,7 +91,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
.$TrashedLocalAssetEntityTable(this); .$TrashedLocalAssetEntityTable(this);
late final i21.$AssetEditEntityTable assetEditEntity = i21 late final i21.$AssetEditEntityTable assetEditEntity = i21
.$AssetEditEntityTable(this); .$AssetEditEntityTable(this);
late final i22.$MetadataEntityTable metadataEntity = i22.$MetadataEntityTable( late final i22.$SettingsEntityTable settingsEntity = i22.$SettingsEntityTable(
this, this,
); );
i23.MergedAssetDrift get mergedAssetDrift => i24.ReadDatabaseContainer( i23.MergedAssetDrift get mergedAssetDrift => i24.ReadDatabaseContainer(
@ -132,7 +132,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
storeEntity, storeEntity,
trashedLocalAssetEntity, trashedLocalAssetEntity,
assetEditEntity, assetEditEntity,
metadataEntity, settingsEntity,
i10.idxPartnerSharedWithId, i10.idxPartnerSharedWithId,
i11.idxLatLng, i11.idxLatLng,
i11.idxRemoteExifCity, i11.idxRemoteExifCity,
@ -395,6 +395,6 @@ class $DriftManager {
); );
i21.$$AssetEditEntityTableTableManager get assetEditEntity => i21.$$AssetEditEntityTableTableManager get assetEditEntity =>
i21.$$AssetEditEntityTableTableManager(_db, _db.assetEditEntity); i21.$$AssetEditEntityTableTableManager(_db, _db.assetEditEntity);
i22.$$MetadataEntityTableTableManager get metadataEntity => i22.$$SettingsEntityTableTableManager get settingsEntity =>
i22.$$MetadataEntityTableTableManager(_db, _db.metadataEntity); i22.$$SettingsEntityTableTableManager(_db, _db.settingsEntity);
} }

View File

@ -13539,6 +13539,550 @@ i1.GeneratedColumn<String> _column_212(String aliasedName) =>
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
$customConstraints: 'NULL', $customConstraints: 'NULL',
); );
final class Schema27 extends i0.VersionedSchema {
Schema27({required super.database}) : super(version: 27);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAlbumAssetAlbumAsset,
idxLocalAssetChecksum,
idxLocalAssetCloudId,
idxStackPrimaryAssetId,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
idxRemoteAssetStackId,
idxRemoteAssetOwnerVisibilityDeletedCreated,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
remoteAssetCloudIdEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
trashedLocalAssetEntity,
assetEditEntity,
settings,
idxPartnerSharedWithId,
idxLatLng,
idxRemoteExifCity,
idxRemoteAlbumAssetAlbumAsset,
idxRemoteAssetCloudId,
idxPersonOwnerId,
idxAssetFacePersonId,
idxAssetFaceAssetId,
idxAssetFaceVisiblePerson,
idxTrashedLocalAssetChecksum,
idxTrashedLocalAssetAlbum,
idxAssetEditAssetId,
];
late final Shape33 userEntity = Shape33(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_109,
_column_110,
_column_111,
_column_112,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape50 remoteAssetEntity = Shape50(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_119,
_column_120,
_column_121,
_column_122,
_column_123,
_column_124,
_column_212,
_column_125,
_column_126,
_column_127,
_column_128,
_column_129,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape35 stackEntity = Shape35(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_121,
_column_130,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape36 localAssetEntity = Shape36(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_131,
_column_120,
_column_132,
_column_133,
_column_134,
_column_135,
_column_136,
_column_137,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape48 remoteAlbumEntity = Shape48(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_138,
_column_114,
_column_115,
_column_139,
_column_140,
_column_141,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape38 localAlbumEntity = Shape38(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_115,
_column_142,
_column_143,
_column_144,
_column_145,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape39 localAlbumAssetEntity = Shape39(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_146, _column_147, _column_145],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
'idx_local_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxLocalAssetCloudId = i1.Index(
'idx_local_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
);
final i1.Index idxStackPrimaryAssetId = i1.Index(
'idx_stack_primary_asset_id',
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
final i1.Index idxRemoteAssetStackId = i1.Index(
'idx_remote_asset_stack_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
);
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
'idx_remote_asset_owner_visibility_deleted_created',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
);
late final Shape40 authUserEntity = Shape40(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_109,
_column_148,
_column_110,
_column_111,
_column_149,
_column_150,
_column_151,
_column_152,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_153, _column_154, _column_155],
attachedDatabase: database,
),
alias: null,
);
late final Shape41 partnerEntity = Shape41(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_156, _column_157, _column_158],
attachedDatabase: database,
),
alias: null,
);
late final Shape42 remoteExifEntity = Shape42(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_159,
_column_160,
_column_161,
_column_162,
_column_163,
_column_164,
_column_117,
_column_116,
_column_165,
_column_166,
_column_167,
_column_168,
_column_135,
_column_136,
_column_169,
_column_170,
_column_171,
_column_172,
_column_173,
_column_174,
_column_175,
_column_176,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_159, _column_177],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_177, _column_153, _column_178],
attachedDatabase: database,
),
alias: null,
);
late final Shape43 remoteAssetCloudIdEntity = Shape43(
source: i0.VersionedTable(
entityName: 'remote_asset_cloud_id_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_159,
_column_179,
_column_180,
_column_134,
_column_135,
_column_136,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape44 memoryEntity = Shape44(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_124,
_column_121,
_column_113,
_column_181,
_column_182,
_column_183,
_column_184,
_column_185,
_column_186,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_159, _column_187],
attachedDatabase: database,
),
alias: null,
);
late final Shape45 personEntity = Shape45(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_121,
_column_108,
_column_188,
_column_189,
_column_190,
_column_191,
_column_192,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape46 assetFaceEntity = Shape46(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_193,
_column_194,
_column_195,
_column_196,
_column_197,
_column_198,
_column_199,
_column_200,
_column_201,
_column_124,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_202, _column_203, _column_204],
attachedDatabase: database,
),
alias: null,
);
late final Shape47 trashedLocalAssetEntity = Shape47(
source: i0.VersionedTable(
entityName: 'trashed_local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id, album_id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_205,
_column_131,
_column_120,
_column_132,
_column_206,
_column_137,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape32 assetEditEntity = Shape32(
source: i0.VersionedTable(
entityName: 'asset_edit_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_207,
_column_208,
_column_209,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape49 settings = Shape49(
source: i0.VersionedTable(
entityName: 'settings',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY("key")'],
columns: [_column_210, _column_211, _column_115],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxPartnerSharedWithId = i1.Index(
'idx_partner_shared_with_id',
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxRemoteExifCity = i1.Index(
'idx_remote_exif_city',
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
);
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
'idx_remote_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxRemoteAssetCloudId = i1.Index(
'idx_remote_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
);
final i1.Index idxPersonOwnerId = i1.Index(
'idx_person_owner_id',
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
);
final i1.Index idxAssetFacePersonId = i1.Index(
'idx_asset_face_person_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
);
final i1.Index idxAssetFaceAssetId = i1.Index(
'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
);
final i1.Index idxAssetFaceVisiblePerson = i1.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',
);
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
'idx_trashed_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
);
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
'idx_trashed_local_asset_album',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
);
final i1.Index idxAssetEditAssetId = i1.Index(
'idx_asset_edit_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
);
}
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@ -13565,6 +14109,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24, required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25, required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
required Future<void> Function(i1.Migrator m, Schema26 schema) from25To26, required Future<void> Function(i1.Migrator m, Schema26 schema) from25To26,
required Future<void> Function(i1.Migrator m, Schema27 schema) from26To27,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -13693,6 +14238,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from25To26(migrator, schema); await from25To26(migrator, schema);
return 26; return 26;
case 26:
final schema = Schema27(database: database);
final migrator = i1.Migrator(database, schema);
await from26To27(migrator, schema);
return 27;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -13725,6 +14275,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24, required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25, required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
required Future<void> Function(i1.Migrator m, Schema26 schema) from25To26, required Future<void> Function(i1.Migrator m, Schema26 schema) from25To26,
required Future<void> Function(i1.Migrator m, Schema27 schema) from26To27,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
@ -13752,5 +14303,6 @@ i1.OnUpgrade stepByStep({
from23To24: from23To24, from23To24: from23To24,
from24To25: from24To25, from24To25: from24To25,
from25To26: from25To26, from25To26: from25To26,
from26To27: from26To27,
), ),
); );

View File

@ -1,72 +0,0 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class MetadataRepository extends DriftDatabaseRepository {
final Drift _db;
MetadataRepository._(this._db) : super(_db);
static MetadataRepository? _instance;
static MetadataRepository get instance {
final instance = _instance;
if (instance == null) {
throw StateError('MetadataRepository not initialized. Call ensureInitialized() first');
}
return instance;
}
AppConfig _appConfig = const .new();
AppConfig get appConfig => _appConfig;
static Future<MetadataRepository> ensureInitialized(Drift db) async {
if (_instance == null) {
final instance = MetadataRepository._(db);
await instance.refresh();
_instance = instance;
}
return _instance!;
}
Future<void> refresh() async => _applyOverrides(await _db.select(_db.metadataEntity).get());
Future<void> write<T extends Object, U extends T>(MetadataKey<T> key, U value) async {
if (value == _appConfig.read(key)) {
return;
}
if (value == defaultConfig.read(key)) {
await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(key.name))).go();
} else {
await _db
.into(_db.metadataEntity)
.insertOnConflictUpdate(
MetadataEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())),
);
}
_appConfig = _appConfig.write(key, value);
}
Stream<AppConfig> watchConfig() => _db.select(_db.metadataEntity).watch().map((rows) {
_applyOverrides(rows);
return _appConfig;
});
void _applyOverrides(List<MetadataEntityData> rows) {
_appConfig = AppConfig.fromEntries(
rows.fold({}, (overrides, row) {
final metadataKey = MetadataKey.values.firstWhereOrNull((key) => key.name == row.key);
if (metadataKey == null) {
return overrides;
}
return {...overrides, metadataKey: metadataKey.decode(row.value)};
}),
);
}
}

View File

@ -0,0 +1,84 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class SettingsRepository extends DriftDatabaseRepository {
final Drift _db;
SettingsRepository._(this._db) : super(_db);
static SettingsRepository? _instance;
static SettingsRepository get instance {
final instance = _instance;
if (instance == null) {
throw StateError('SettingsRepository not initialized. Call ensureInitialized() first');
}
return instance;
}
AppConfig _appConfig = const .new();
AppConfig get appConfig => _appConfig;
static Future<SettingsRepository> ensureInitialized(Drift db) async {
if (_instance == null) {
final instance = SettingsRepository._(db);
await instance.refresh();
_instance = instance;
}
return _instance!;
}
Future<void> refresh() async => _applyOverrides(await _db.select(_db.settingsEntity).get());
Future<void> clear(Iterable<SettingsKey> keys) async {
if (keys.isEmpty) {
return;
}
final names = keys.map((key) => key.name).toList();
await (_db.delete(_db.settingsEntity)..where((row) => row.key.isIn(names))).go();
for (final key in keys) {
_appConfig = _appConfig.write(key, defaultConfig.read(key));
}
}
Future<void> write<T extends Object, U extends T>(SettingsKey<T> key, U value) async {
if (value == _appConfig.read(key)) {
return;
}
if (value == defaultConfig.read(key)) {
return clear([key]);
}
await _db
.into(_db.settingsEntity)
.insertOnConflictUpdate(
SettingsEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())),
);
_appConfig = _appConfig.write(key, value);
}
Stream<AppConfig> watchConfig() => _db.select(_db.settingsEntity).watch().map((rows) {
_applyOverrides(rows);
return _appConfig;
});
void _applyOverrides(List<SettingsEntityData> rows) {
_appConfig = AppConfig.fromEntries(
rows.fold({}, (overrides, row) {
final metadataKey = SettingsKey.values.firstWhereOrNull((key) => key.name == row.key);
if (metadataKey == null) {
return overrides;
}
return {...overrides, metadataKey: metadataKey.decode(row.value)};
}),
);
}
}

View File

@ -25,7 +25,7 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart';

View File

@ -2,16 +2,12 @@ import 'package:immich_mobile/utils/semver.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class ServerVersion extends SemVer { class ServerVersion extends SemVer {
const ServerVersion({required super.major, required super.minor, required super.patch}); const ServerVersion({required super.major, required super.minor, required super.patch, super.prerelease});
@override ServerVersion.fromDto(ServerVersionResponseDto dto)
String toString() { : super(major: dto.major, minor: dto.minor, patch: dto.patch_, prerelease: dto.prerelease);
return 'ServerVersion(major: $major, minor: $minor, patch: $patch)';
}
ServerVersion.fromDto(ServerVersionResponseDto dto) : super(major: dto.major, minor: dto.minor, patch: dto.patch_); bool isAtLeast({int major = 0, int minor = 0, int patch = 0, int? prerelease}) {
return this >= SemVer(major: major, minor: minor, patch: patch, prerelease: prerelease);
bool isAtLeast({int major = 0, int minor = 0, int patch = 0}) {
return this >= SemVer(major: major, minor: minor, patch: patch);
} }
} }

View File

@ -8,11 +8,11 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart'; import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart'; import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
@ -103,7 +103,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
return; return;
} }
final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled; final isBackupEnabled = SettingsRepository.instance.appConfig.backup.enabled;
await ref.read(driftBackupProvider.notifier).getBackupStatus(user.id); await ref.read(driftBackupProvider.notifier).getBackupStatus(user.id);
final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount)); final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
final totalChanged = currentTotalAssetCount != _initialTotalAssetCount; final totalChanged = currentTotalAssetCount != _initialTotalAssetCount;

View File

@ -4,10 +4,10 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -27,7 +27,7 @@ class DriftBackupOptionsPage extends ConsumerWidget {
// There is an issue with Flutter where the pop event // There is an issue with Flutter where the pop event
// can be triggered multiple times, so we guard it with _hasPopped // can be triggered multiple times, so we guard it with _hasPopped
final currentBackup = ref.read(metadataProvider).appConfig.backup; final currentBackup = ref.read(appConfigProvider).backup;
final currentCellularForVideos = currentBackup.useCellularForVideos; final currentCellularForVideos = currentBackup.useCellularForVideos;
final currentCellularForPhotos = currentBackup.useCellularForPhotos; final currentCellularForPhotos = currentBackup.useCellularForPhotos;
@ -45,7 +45,7 @@ class DriftBackupOptionsPage extends ConsumerWidget {
} }
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled; final isBackupEnabled = SettingsRepository.instance.appConfig.backup.enabled;
if (!isBackupEnabled) { if (!isBackupEnabled) {
return; return;
} }

View File

@ -3,10 +3,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
class SettingsHeader { class SettingsHeader {
String key = ""; String key = "";
@ -22,7 +21,7 @@ class HeaderSettingsPage extends HookConsumerWidget {
final headers = useState<List<SettingsHeader>>([]); final headers = useState<List<SettingsHeader>>([]);
final setInitialHeaders = useState(false); final setInitialHeaders = useState(false);
final storedHeaders = ref.read(metadataProvider).appConfig.network.customHeaders; final storedHeaders = ref.read(appConfigProvider).network.customHeaders;
if (!setInitialHeaders.value) { if (!setInitialHeaders.value) {
storedHeaders.forEach((k, v) { storedHeaders.forEach((k, v) {
final header = SettingsHeader(); final header = SettingsHeader();
@ -94,7 +93,7 @@ class HeaderSettingsPage extends HookConsumerWidget {
headersMap[key] = value; headersMap[key] = value;
} }
await ref.read(metadataProvider).write(MetadataKey.networkCustomHeaders, headersMap); await ref.read(settingsProvider).write(.networkCustomHeaders, headersMap);
await ref.read(apiServiceProvider).updateHeaders(); await ref.read(apiServiceProvider).updateHeaders();
} }
} }

View File

@ -12,7 +12,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
@ -341,7 +341,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
await backgroundManager.hashAssets(); await backgroundManager.hashAssets();
} }
if (MetadataRepository.instance.appConfig.backup.syncAlbums) { if (SettingsRepository.instance.appConfig.backup.syncAlbums) {
await backgroundManager.syncLinkedAlbum(); await backgroundManager.syncLinkedAlbum();
} }
} catch (e) { } catch (e) {
@ -370,7 +370,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
} }
Future<void> _resumeBackup(DriftBackupNotifier notifier) async { Future<void> _resumeBackup(DriftBackupNotifier notifier) async {
final isEnableBackup = MetadataRepository.instance.appConfig.backup.enabled; final isEnableBackup = SettingsRepository.instance.appConfig.backup.enabled;
if (isEnableBackup) { if (isEnableBackup) {
final currentUser = Store.tryGet(StoreKey.currentUser); final currentUser = Store.tryGet(StoreKey.currentUser);

View File

@ -17,7 +17,7 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.wid
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart';

View File

@ -6,6 +6,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
@ -142,13 +143,18 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
return; return;
} }
final addedCount = await ref.read(remoteAlbumProvider.notifier).addAssets(album.id, [latest.remoteId!]); final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.viewer, album);
if (!context.mounted) { if (!context.mounted) {
return; return;
} }
if (addedCount == 0) { if (!result.success) {
ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error);
return;
}
if (result.count == 0) {
ImmichToast.show( ImmichToast.show(
context: context, context: context,
msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}), msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name}),
@ -159,7 +165,7 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
); );
// Invalidate using the asset's remote ID to refresh the "Appears in" list // Refresh the "Appears in" list on the asset's info panel.
ref.invalidate(albumsContainingAssetProvider(latest.remoteId!)); ref.invalidate(albumsContainingAssetProvider(latest.remoteId!));
} }

View File

@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
@ -15,12 +14,11 @@ import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@ -58,7 +56,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final albumConfig = ref.read(metadataProvider).appConfig.album; final albumConfig = ref.read(appConfigProvider).album;
setState(() { setState(() {
sort = AlbumSort(mode: albumConfig.sortMode, isReverse: albumConfig.isReverse); sort = AlbumSort(mode: albumConfig.sortMode, isReverse: albumConfig.isReverse);
@ -94,7 +92,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
setState(() { setState(() {
isGrid = !isGrid; isGrid = !isGrid;
}); });
ref.read(metadataProvider).write(MetadataKey.albumIsGrid, isGrid); ref.read(settingsProvider).write(.albumIsGrid, isGrid);
} }
void changeFilter(QuickFilterMode mode) { void changeFilter(QuickFilterMode mode) {
@ -110,9 +108,9 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
this.sort = sort; this.sort = sort;
}); });
final metadata = ref.read(metadataProvider); final metadata = ref.read(settingsProvider);
await metadata.write(MetadataKey.albumSortMode, sort.mode); await metadata.write(.albumSortMode, sort.mode);
await metadata.write(MetadataKey.albumIsReverse, sort.isReverse); await metadata.write(.albumIsReverse, sort.isReverse);
await sortAlbums(); await sortAlbums();
} }
@ -747,12 +745,10 @@ class AddToAlbumHeader extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
Future<void> onCreateAlbum() async { Future<void> onCreateAlbum() async {
final selectedAssets = ref.read(multiSelectProvider).selectedAssets;
final newAlbum = await ref final newAlbum = await ref
.read(remoteAlbumProvider.notifier) .read(remoteAlbumProvider.notifier)
.createAlbum( .createAlbumWithAssets(title: "Untitled Album", assets: selectedAssets);
title: "Untitled Album",
assetIds: ref.read(multiSelectProvider).selectedAssets.map((e) => (e as RemoteAsset).id).toList(),
);
if (newAlbum == null) { if (newAlbum == null) {
ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr()); ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr());

View File

@ -5,6 +5,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart'; import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart';
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
/// Pinned banner sliver that surfaces in-flight album uploads directly under /// Pinned banner sliver that surfaces in-flight album uploads directly under
/// the album app bar. Renders nothing while the queue is empty. Tapping the /// the album app bar. Renders nothing while the queue is empty. Tapping the
@ -165,6 +166,8 @@ class _PendingUploadsSheet extends ConsumerWidget {
} }
final failedCount = pending.where((p) => p.failed).length; final failedCount = pending.where((p) => p.failed).length;
final inFlightCount = pending.length - failedCount;
final canAbort = inFlightCount > 0 && ref.watch(manualUploadCancelTokenProvider) != null;
return SafeArea( return SafeArea(
child: Padding( child: Padding(
@ -183,7 +186,21 @@ class _PendingUploadsSheet extends ConsumerWidget {
style: context.textTheme.titleMedium, style: context.textTheme.titleMedium,
), ),
), ),
if (failedCount > 0) if (canAbort)
TextButton.icon(
onPressed: () {
final cancelToken = ref.read(manualUploadCancelTokenProvider);
if (cancelToken != null && !cancelToken.isCompleted) {
cancelToken.complete();
}
ref.read(manualUploadCancelTokenProvider.notifier).state = null;
ref.read(pendingAlbumUploadsProvider(albumId).notifier).clear();
},
icon: const Icon(Icons.stop_circle_outlined, size: 18),
label: Text('cancel'.t(context: context)),
style: TextButton.styleFrom(foregroundColor: context.colorScheme.error),
)
else if (failedCount > 0)
TextButton.icon( TextButton.icon(
onPressed: () => ref.read(pendingAlbumUploadsProvider(albumId).notifier).clearFailed(), onPressed: () => ref.read(pendingAlbumUploadsProvider(albumId).notifier).clearFailed(),
icon: const Icon(Icons.clear_rounded, size: 18), icon: const Icon(Icons.clear_rounded, size: 18),

View File

@ -19,7 +19,7 @@ import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
@ -241,7 +241,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
return; return;
} }
final tapToNavigate = ref.read(metadataProvider).appConfig.viewer.tapToNavigate; final tapToNavigate = ref.read(appConfigProvider).viewer.tapToNavigate;
if (!tapToNavigate) { if (!tapToNavigate) {
_viewer.toggleControls(); _viewer.toggleControls();
return; return;

View File

@ -63,16 +63,19 @@ class SheetTile extends ConsumerWidget {
subtitleWidget = null; subtitleWidget = null;
} }
return ListTile( return Material(
dense: true, type: MaterialType.transparency,
visualDensity: VisualDensity.compact, child: ListTile(
title: GestureDetector(onLongPress: () => copyTitle(context, ref), child: titleWidget), dense: true,
titleAlignment: ListTileTitleAlignment.center, visualDensity: VisualDensity.compact,
leading: leading, title: GestureDetector(onLongPress: () => copyTitle(context, ref), child: titleWidget),
trailing: trailing, titleAlignment: ListTileTitleAlignment.center,
contentPadding: leading == null ? null : const EdgeInsets.only(left: 25), leading: leading,
subtitle: subtitleWidget, trailing: trailing,
onTap: onTap, contentPadding: leading == null ? null : const EdgeInsets.only(left: 25),
subtitle: subtitleWidget,
onTap: onTap,
),
); );
} }
} }

View File

@ -12,7 +12,7 @@ import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.pro
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:native_video_player/native_video_player.dart'; import 'package:native_video_player/native_video_player.dart';
@ -128,7 +128,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
final remoteId = (videoAsset as RemoteAsset).id; final remoteId = (videoAsset as RemoteAsset).id;
final serverEndpoint = Store.get(StoreKey.serverEndpoint); final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final isOriginalVideo = ref.read(metadataProvider).appConfig.viewer.loadOriginalVideo; final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo;
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback'; final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
final String videoUrl = videoAsset.livePhotoVideoId != null final String videoUrl = videoAsset.livePhotoVideoId != null
? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl' ? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl'
@ -161,7 +161,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
return; return;
} }
final autoPlayVideo = ref.read(metadataProvider).appConfig.viewer.autoPlayVideo; final autoPlayVideo = ref.read(appConfigProvider).viewer.autoPlayVideo;
if (autoPlayVideo || widget.asset.isMotionPhoto) { if (autoPlayVideo || widget.asset.isMotionPhoto) {
await _notifier.play(); await _notifier.play();
} }
@ -212,7 +212,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
} }
await _notifier.load(source); await _notifier.load(source);
final loopVideo = ref.read(metadataProvider).appConfig.viewer.loopVideo; final loopVideo = ref.read(appConfigProvider).viewer.loopVideo;
await _notifier.setLoop(!widget.asset.isMotionPhoto && loopVideo); await _notifier.setLoop(!widget.asset.isMotionPhoto && loopVideo);
await _notifier.setVolume(1); await _notifier.setVolume(1);
} }

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart';
@ -34,7 +33,7 @@ class ViewerKebabMenu extends ConsumerWidget {
final isInLockedView = ref.watch(inLockedViewProvider); final isInLockedView = ref.watch(inLockedViewProvider);
final currentAlbum = ref.watch(currentRemoteAlbumProvider); final currentAlbum = ref.watch(currentRemoteAlbumProvider);
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive; final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting); final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting);
final actionContext = ActionButtonContext( final actionContext = ActionButtonContext(
asset: asset, asset: asset,

View File

@ -1,10 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
class BackupToggleButton extends ConsumerStatefulWidget { class BackupToggleButton extends ConsumerStatefulWidget {
final VoidCallback onStart; final VoidCallback onStart;
@ -31,7 +30,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
end: 1, end: 1,
).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut)); ).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_isEnabled = ref.read(metadataProvider).appConfig.backup.enabled; _isEnabled = ref.read(appConfigProvider).backup.enabled;
} }
@override @override
@ -41,7 +40,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
} }
Future<void> _onToggle(bool value) async { Future<void> _onToggle(bool value) async {
await ref.read(metadataProvider).write(MetadataKey.backupEnabled, value); await ref.read(settingsProvider).write(.backupEnabled, value);
setState(() { setState(() {
_isEnabled = value; _isEnabled = value;

View File

@ -3,9 +3,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart';
@ -25,7 +23,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
@ -63,37 +61,23 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false), userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false),
); );
Future<void> addAssetsToAlbum(RemoteAlbum album) async { Future<void> addToAlbum(RemoteAlbum album) async {
final selectedAssets = multiselect.selectedAssets; final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album);
if (selectedAssets.isEmpty) {
if (!context.mounted) {
return; return;
} }
final remoteAssets = selectedAssets.whereType<RemoteAsset>(); if (!result.success) {
final addedCount = await ref ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error);
.read(remoteAlbumProvider.notifier) return;
.addAssets(album.id, remoteAssets.map((e) => e.id).toList());
if (selectedAssets.length != remoteAssets.length) {
ImmichToast.show(
context: context,
msg: 'add_to_album_bottom_sheet_some_local_assets'.t(context: context),
);
} }
ImmichToast.show(
if (addedCount != remoteAssets.length) { context: context,
ImmichToast.show( msg: result.count == 0
context: context, ? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name})
msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}), : 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
); );
} else {
ImmichToast.show(
context: context,
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {"album": album.name}),
);
}
ref.read(multiSelectProvider.notifier).reset();
} }
Future<void> onKeyboardExpand() { Future<void> onKeyboardExpand() {
@ -131,12 +115,10 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
const DeleteLocalActionButton(source: ActionSource.timeline), const DeleteLocalActionButton(source: ActionSource.timeline),
if (multiselect.onlyLocal) const UploadActionButton(source: ActionSource.timeline), if (multiselect.onlyLocal) const UploadActionButton(source: ActionSource.timeline),
], ],
slivers: multiselect.hasRemote slivers: [
? [ const AddToAlbumHeader(),
const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand),
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand), ],
]
: [],
); );
} }
} }

View File

@ -1,25 +1,78 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class LocalAlbumBottomSheet extends ConsumerWidget { class LocalAlbumBottomSheet extends ConsumerStatefulWidget {
const LocalAlbumBottomSheet({super.key}); const LocalAlbumBottomSheet({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { ConsumerState<LocalAlbumBottomSheet> createState() => _LocalAlbumBottomSheetState();
return const BaseBottomSheet( }
class _LocalAlbumBottomSheetState extends ConsumerState<LocalAlbumBottomSheet> {
late final DraggableScrollableController sheetController;
@override
void initState() {
super.initState();
sheetController = DraggableScrollableController();
}
@override
void dispose() {
sheetController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
Future<void> addToAlbum(RemoteAlbum album) async {
final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album);
if (!context.mounted) {
return;
}
if (!result.success) {
ImmichToast.show(context: context, msg: 'scaffold_body_error_occurred'.tr(), toastType: ToastType.error);
return;
}
ImmichToast.show(
context: context,
msg: result.count == 0
? 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {'album': album.name})
: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
);
}
Future<void> onKeyboardExpand() {
return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
}
return BaseBottomSheet(
controller: sheetController,
initialChildSize: 0.25, initialChildSize: 0.25,
maxChildSize: 0.4, maxChildSize: 0.85,
shouldCloseOnMinExtent: false, shouldCloseOnMinExtent: false,
actions: [ actions: const [
ShareActionButton(source: ActionSource.timeline), ShareActionButton(source: ActionSource.timeline),
DeleteLocalActionButton(source: ActionSource.timeline), DeleteLocalActionButton(source: ActionSource.timeline),
UploadActionButton(source: ActionSource.timeline), UploadActionButton(source: ActionSource.timeline),
], ],
slivers: [
const AddToAlbumHeader(),
AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand),
],
); );
} }
} }

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
@ -21,7 +20,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_b
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
@ -56,29 +55,28 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
final ownsAlbum = ref.watch(currentUserProvider)?.id == widget.album.ownerId; final ownsAlbum = ref.watch(currentUserProvider)?.id == widget.album.ownerId;
Future<void> addAssetsToAlbum(RemoteAlbum album) async { Future<void> addToAlbum(RemoteAlbum album) async {
final selectedAssets = multiselect.selectedAssets; final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album);
if (selectedAssets.isEmpty) {
if (!context.mounted) {
return; return;
} }
final addedCount = await ref if (!result.success) {
.read(remoteAlbumProvider.notifier)
.addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList());
if (addedCount != selectedAssets.length) {
ImmichToast.show( ImmichToast.show(
context: context, context: context,
msg: 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name}), msg: 'scaffold_body_error_occurred'.t(context: context),
); toastType: ToastType.error,
} else {
ImmichToast.show(
context: context,
msg: 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}),
); );
return;
} }
ref.read(multiSelectProvider.notifier).reset(); ImmichToast.show(
context: context,
msg: result.count == 0
? 'add_to_album_bottom_sheet_already_exists'.t(context: context, args: {"album": album.name})
: 'add_to_album_bottom_sheet_added'.t(context: context, args: {"album": album.name}),
);
} }
Future<void> onKeyboardExpand() { Future<void> onKeyboardExpand() {
@ -118,10 +116,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
SetAlbumCoverActionButton(source: ActionSource.timeline, albumId: widget.album.id), SetAlbumCoverActionButton(source: ActionSource.timeline, albumId: widget.album.id),
], ],
slivers: ownsAlbum slivers: ownsAlbum
? [ ? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand)]
const AddToAlbumHeader(),
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
]
: null, : null,
); );
} }

View File

@ -4,7 +4,7 @@ import 'package:async/async.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/loaders/image_request.dart'; import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
@ -189,5 +189,5 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai
bool _shouldUseLocalAsset(BaseAsset asset) => bool _shouldUseLocalAsset(BaseAsset asset) =>
asset.hasLocal && asset.hasLocal &&
(!asset.hasRemote || !MetadataRepository.instance.appConfig.image.preferRemote) && (!asset.hasRemote || !SettingsRepository.instance.appConfig.image.preferRemote) &&
!asset.isEdited; !asset.isEdited;

View File

@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/loaders/image_request.dart'; import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/presentation/widgets/images/animated_image_stream_completer.dart'; import 'package:immich_mobile/presentation/widgets/images/animated_image_stream_completer.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart'; import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
@ -104,7 +104,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
return; return;
} }
final loadOriginal = MetadataRepository.instance.appConfig.image.loadOriginal; final loadOriginal = SettingsRepository.instance.appConfig.image.loadOriginal;
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio; final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
var request = this.request = LocalImageRequest( var request = this.request = LocalImageRequest(
localId: key.id, localId: key.id,

View File

@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/loaders/image_request.dart'; import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/presentation/widgets/images/animated_image_stream_completer.dart'; import 'package:immich_mobile/presentation/widgets/images/animated_image_stream_completer.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart'; import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
@ -122,7 +122,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
edited: key.edited, edited: key.edited,
), ),
); );
final loadOriginal = assetType == AssetType.image && MetadataRepository.instance.appConfig.image.loadOriginal; final loadOriginal = assetType == AssetType.image && SettingsRepository.instance.appConfig.image.loadOriginal;
yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal); yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal);
if (!loadOriginal) { if (!loadOriginal) {

View File

@ -9,7 +9,7 @@ import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
class ThumbnailTile extends ConsumerStatefulWidget { class ThumbnailTile extends ConsumerStatefulWidget {

View File

@ -1,11 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/providers/infrastructure/map.provider.dart'; import 'package:immich_mobile/providers/infrastructure/map.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/map/map_state.provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart';
import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:maplibre_gl/maplibre_gl.dart';
@ -81,25 +80,25 @@ class MapStateNotifier extends Notifier<MapState> {
} }
void switchFavoriteOnly(bool isFavoriteOnly) { void switchFavoriteOnly(bool isFavoriteOnly) {
ref.read(metadataProvider).write(MetadataKey.mapShowFavoriteOnly, isFavoriteOnly); ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly);
state = state.copyWith(onlyFavorites: isFavoriteOnly); state = state.copyWith(onlyFavorites: isFavoriteOnly);
EventStream.shared.emit(const MapMarkerReloadEvent()); EventStream.shared.emit(const MapMarkerReloadEvent());
} }
void switchIncludeArchived(bool isIncludeArchived) { void switchIncludeArchived(bool isIncludeArchived) {
ref.read(metadataProvider).write(MetadataKey.mapIncludeArchived, isIncludeArchived); ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived);
state = state.copyWith(includeArchived: isIncludeArchived); state = state.copyWith(includeArchived: isIncludeArchived);
EventStream.shared.emit(const MapMarkerReloadEvent()); EventStream.shared.emit(const MapMarkerReloadEvent());
} }
void switchWithPartners(bool isWithPartners) { void switchWithPartners(bool isWithPartners) {
ref.read(metadataProvider).write(MetadataKey.mapWithPartners, isWithPartners); ref.read(settingsProvider).write(.mapWithPartners, isWithPartners);
state = state.copyWith(withPartners: isWithPartners); state = state.copyWith(withPartners: isWithPartners);
EventStream.shared.emit(const MapMarkerReloadEvent()); EventStream.shared.emit(const MapMarkerReloadEvent());
} }
void setRelativeTime(int relativeDays) { void setRelativeTime(int relativeDays) {
ref.read(metadataProvider).write(MetadataKey.mapRelativeDate, relativeDays); ref.read(settingsProvider).write(.mapRelativeDate, relativeDays);
state = state.copyWith(relativeDays: relativeDays); state = state.copyWith(relativeDays: relativeDays);
EventStream.shared.emit(const MapMarkerReloadEvent()); EventStream.shared.emit(const MapMarkerReloadEvent());
} }

View File

@ -5,7 +5,7 @@ import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/presentation/widgets/timeline/fixed/segment_builder.dart'; import 'package:immich_mobile/presentation/widgets/timeline/fixed/segment_builder.dart';
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
class TimelineArgs { class TimelineArgs {

View File

@ -10,7 +10,6 @@ import 'package:flutter/rendering.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
@ -22,7 +21,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart'; import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
@ -459,7 +458,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
_restoreAssetIndex = targetAssetIndex; _restoreAssetIndex = targetAssetIndex;
}); });
ref.read(metadataProvider).write(MetadataKey.timelineTilesPerRow, _perRow); ref.read(settingsProvider).write(.timelineTilesPerRow, _perRow);
} }
}; };
}, },

View File

@ -67,6 +67,11 @@ class AlbumPendingUploadsNotifier extends AutoDisposeFamilyNotifier<List<Pending
_syncKeepAlive(); _syncKeepAlive();
} }
void clear() {
state = const [];
_syncKeepAlive();
}
void _syncKeepAlive() { void _syncKeepAlive() {
if (state.isEmpty) { if (state.isEmpty) {
_keepAliveLink?.close(); _keepAliveLink?.close();

View File

@ -9,8 +9,8 @@ import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart';
@ -107,7 +107,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
final backgroundManager = _ref.read(backgroundSyncProvider); final backgroundManager = _ref.read(backgroundSyncProvider);
final isAlbumLinkedSyncEnable = _ref.read(metadataProvider).appConfig.backup.syncAlbums; final isAlbumLinkedSyncEnable = _ref.read(appConfigProvider).backup.syncAlbums;
try { try {
bool syncSuccess = false; bool syncSuccess = false;
@ -137,7 +137,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
} }
Future<void> _resumeBackup() async { Future<void> _resumeBackup() async {
final isEnableBackup = _ref.read(metadataProvider).appConfig.backup.enabled; final isEnableBackup = _ref.read(appConfigProvider).backup.enabled;
if (isEnableBackup) { if (isEnableBackup) {
final currentUser = Store.tryGet(StoreKey.currentUser); final currentUser = Store.tryGet(StoreKey.currentUser);

View File

@ -3,7 +3,6 @@ import 'dart:convert';
import 'package:flutter_udid/flutter_udid.dart'; import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart';
@ -11,7 +10,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/models/auth/auth_state.model.dart'; import 'package:immich_mobile/models/auth/auth_state.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/services/auth.service.dart';
@ -130,7 +129,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
await _apiService.updateHeaders(); await _apiService.updateHeaders();
final serverEndpoint = Store.get(StoreKey.serverEndpoint); final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final headerMap = _ref.read(metadataProvider).appConfig.network.customHeaders; final headerMap = _ref.read(appConfigProvider).network.customHeaders;
final customHeaders = headerMap.isEmpty ? null : jsonEncode(headerMap); final customHeaders = headerMap.isEmpty ? null : jsonEncode(headerMap);
await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders); await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders);
@ -179,19 +178,19 @@ class AuthNotifier extends StateNotifier<AuthState> {
} }
Future<void> saveWifiName(String wifiName) async { Future<void> saveWifiName(String wifiName) async {
await _ref.read(metadataProvider).write(MetadataKey.networkPreferredWifiName, wifiName); await _ref.read(settingsProvider).write(.networkPreferredWifiName, wifiName);
} }
Future<void> saveLocalEndpoint(String url) async { Future<void> saveLocalEndpoint(String url) async {
await _ref.read(metadataProvider).write(MetadataKey.networkLocalEndpoint, url); await _ref.read(settingsProvider).write(.networkLocalEndpoint, url);
} }
String? getSavedWifiName() { String? getSavedWifiName() {
return _ref.read(metadataProvider).appConfig.network.preferredWifiName; return _ref.read(appConfigProvider).network.preferredWifiName;
} }
String? getSavedLocalEndpoint() { String? getSavedLocalEndpoint() {
return _ref.read(metadataProvider).appConfig.network.localEndpoint; return _ref.read(appConfigProvider).network.localEndpoint;
} }
/// Returns the current server endpoint (with /api) URL from the store /// Returns the current server endpoint (with /api) URL from the store

View File

@ -1,8 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/cleanup.service.dart'; import 'package:immich_mobile/services/cleanup.service.dart';
@ -54,21 +54,21 @@ final cleanupProvider = StateNotifierProvider<CleanupNotifier, CleanupState>((re
return CleanupNotifier( return CleanupNotifier(
ref.watch(cleanupServiceProvider), ref.watch(cleanupServiceProvider),
ref.watch(currentUserProvider)?.id, ref.watch(currentUserProvider)?.id,
ref.watch(metadataProvider), ref.watch(settingsProvider),
); );
}); });
class CleanupNotifier extends StateNotifier<CleanupState> { class CleanupNotifier extends StateNotifier<CleanupState> {
final CleanupService _cleanupService; final CleanupService _cleanupService;
final String? _userId; final String? _userId;
final MetadataRepository _metadataRepository; final SettingsRepository _settingsRepository;
CleanupNotifier(this._cleanupService, this._userId, this._metadataRepository) : super(const CleanupState()) { CleanupNotifier(this._cleanupService, this._userId, this._settingsRepository) : super(const CleanupState()) {
_loadPersistedSettings(); _loadPersistedSettings();
} }
void _loadPersistedSettings() { void _loadPersistedSettings() {
final cleanup = _metadataRepository.appConfig.cleanup; final cleanup = _settingsRepository.appConfig.cleanup;
final keepFavorites = cleanup.keepFavorites; final keepFavorites = cleanup.keepFavorites;
final keepMediaType = cleanup.keepMediaType; final keepMediaType = cleanup.keepMediaType;
final keepAlbumIds = cleanup.keepAlbumIds.toSet(); final keepAlbumIds = cleanup.keepAlbumIds.toSet();
@ -87,18 +87,18 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
state = state.copyWith(selectedDate: date, assetsToDelete: []); state = state.copyWith(selectedDate: date, assetsToDelete: []);
if (date != null) { if (date != null) {
final daysAgo = DateTime.now().difference(date).inDays; final daysAgo = DateTime.now().difference(date).inDays;
_metadataRepository.write(.cleanupCutoffDaysAgo, daysAgo); _settingsRepository.write(.cleanupCutoffDaysAgo, daysAgo);
} }
} }
void setKeepMediaType(AssetKeepType keepMediaType) { void setKeepMediaType(AssetKeepType keepMediaType) {
state = state.copyWith(keepMediaType: keepMediaType, assetsToDelete: []); state = state.copyWith(keepMediaType: keepMediaType, assetsToDelete: []);
_metadataRepository.write(.cleanupKeepMediaType, keepMediaType); _settingsRepository.write(.cleanupKeepMediaType, keepMediaType);
} }
void setKeepFavorites(bool keepFavorites) { void setKeepFavorites(bool keepFavorites) {
state = state.copyWith(keepFavorites: keepFavorites, assetsToDelete: []); state = state.copyWith(keepFavorites: keepFavorites, assetsToDelete: []);
_metadataRepository.write(.cleanupKeepFavorites, keepFavorites); _settingsRepository.write(.cleanupKeepFavorites, keepFavorites);
} }
void toggleKeepAlbum(String albumId) { void toggleKeepAlbum(String albumId) {
@ -118,7 +118,7 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
} }
void _persistExcludedAlbumIds(Set<String> albumIds) { void _persistExcludedAlbumIds(Set<String> albumIds) {
_metadataRepository.write(.cleanupKeepAlbumIds, albumIds.toList()); _settingsRepository.write(.cleanupKeepAlbumIds, albumIds.toList());
} }
void cleanupStaleAlbumIds(Set<String> existingAlbumIds) { void cleanupStaleAlbumIds(Set<String> existingAlbumIds) {
@ -131,7 +131,7 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
} }
void applyDefaultAlbumSelections(List<(String id, String name)> albums) { void applyDefaultAlbumSelections(List<(String id, String name)> albums) {
final isInitialized = _metadataRepository.appConfig.cleanup.defaultsInitialized; final isInitialized = _settingsRepository.appConfig.cleanup.defaultsInitialized;
if (isInitialized) { if (isInitialized) {
return; return;
} }
@ -144,7 +144,7 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
_persistExcludedAlbumIds(keepAlbumIds); _persistExcludedAlbumIds(keepAlbumIds);
} }
_metadataRepository.write(.cleanupDefaultsInitialized, true); _settingsRepository.write(.cleanupDefaultsInitialized, true);
} }
Future<void> scanAssets() async { Future<void> scanAssets() async {

View File

@ -5,12 +5,15 @@ import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart';
import 'package:immich_mobile/domain/services/asset.service.dart'; import 'package:immich_mobile/domain/services/asset.service.dart';
import 'package:immich_mobile/domain/services/remote_album.service.dart';
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider; import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider;
import 'package:immich_mobile/providers/infrastructure/tag.provider.dart'; import 'package:immich_mobile/providers/infrastructure/tag.provider.dart';
@ -373,6 +376,52 @@ class ActionNotifier extends Notifier<void> {
} }
} }
Future<ActionResult> addToAlbum(ActionSource source, RemoteAlbum album) async {
final selected = _getAssets(source).toList(growable: false);
if (selected.isEmpty) {
return const ActionResult(count: 0, success: true);
}
final candidates = RemoteAlbumService.categorizeCandidates(selected);
final remoteIds = candidates.remoteAssetIds;
final localAssets = candidates.localAssetsToUpload;
final albumNotifier = ref.read(remoteAlbumProvider.notifier);
int addedRemote = 0;
if (remoteIds.isNotEmpty) {
try {
addedRemote = await albumNotifier.addAssets(album.id, remoteIds);
} catch (error, stack) {
_logger.severe('Failed to add assets to album ${album.id}', error, stack);
return ActionResult(count: 0, success: false, error: error.toString());
}
}
// Keep the selection available for retry if the remote add fails. Once the
// album mutation succeeds, clear timeline selection so upload overlays can render.
if (source == ActionSource.timeline) {
ref.read(multiSelectProvider.notifier).reset();
}
if (localAssets.isEmpty) {
return ActionResult(count: addedRemote, success: true);
}
final uploadResult = await upload(
source,
assets: localAssets,
onAssetUploaded: (asset, remoteId) async {
await albumNotifier.linkUploadedAssetToAlbum(album.id, asset, remoteId);
},
);
return ActionResult(
count: addedRemote + uploadResult.count,
success: uploadResult.success,
error: uploadResult.error,
);
}
Future<ActionResult> removeFromAlbum(ActionSource source, String albumId) async { Future<ActionResult> removeFromAlbum(ActionSource source, String albumId) async {
final ids = _getRemoteIdsForSource(source); final ids = _getRemoteIdsForSource(source);
try { try {
@ -495,8 +544,16 @@ class ActionNotifier extends Notifier<void> {
} }
} }
Future<ActionResult> upload(ActionSource source, {List<LocalAsset>? assets}) async { Future<ActionResult> upload(
ActionSource source, {
List<LocalAsset>? assets,
FutureOr<void> Function(LocalAsset asset, String remoteId)? onAssetUploaded,
}) async {
final assetsToUpload = assets ?? _getAssets(source).whereType<LocalAsset>().toList(); final assetsToUpload = assets ?? _getAssets(source).whereType<LocalAsset>().toList();
final assetById = {for (final a in assetsToUpload) a.id: a};
final uploadedAssetIds = <String>{};
final failedAssetIds = <String>{};
final postUploadTasks = <Future<void>>[];
final progressNotifier = ref.read(assetUploadProgressProvider.notifier); final progressNotifier = ref.read(assetUploadProgressProvider.notifier);
final cancelToken = Completer<void>(); final cancelToken = Completer<void>();
@ -518,16 +575,43 @@ class ActionNotifier extends Notifier<void> {
}, },
onSuccess: (localAssetId, remoteAssetId) { onSuccess: (localAssetId, remoteAssetId) {
progressNotifier.remove(localAssetId); progressNotifier.remove(localAssetId);
uploadedAssetIds.add(localAssetId);
final asset = assetById[localAssetId];
final callback = onAssetUploaded;
if (asset != null && callback != null) {
postUploadTasks.add(
Future.sync(() => callback(asset, remoteAssetId)).catchError((Object error, StackTrace stack) {
failedAssetIds.add(localAssetId);
progressNotifier.setError(localAssetId);
_logger.warning('Post-upload callback failed for $localAssetId', error, stack);
}),
);
}
}, },
onError: (localAssetId, errorMessage) { onError: (localAssetId, errorMessage) {
failedAssetIds.add(localAssetId);
progressNotifier.setError(localAssetId); progressNotifier.setError(localAssetId);
}, },
), ),
); );
return ActionResult(count: assetsToUpload.length, success: true);
await Future.wait(postUploadTasks);
final successCount = uploadedAssetIds.difference(failedAssetIds).length;
final isSuccess = successCount == assetsToUpload.length && failedAssetIds.isEmpty;
return ActionResult(
count: successCount,
success: isSuccess,
error: isSuccess ? null : 'Failed to upload ${assetsToUpload.length - successCount} assets',
);
} catch (error, stack) { } catch (error, stack) {
_logger.severe('Failed manually upload assets', error, stack); _logger.severe('Failed manually upload assets', error, stack);
return ActionResult(count: assetsToUpload.length, success: false, error: error.toString());
return ActionResult(
count: uploadedAssetIds.difference(failedAssetIds).length,
success: false,
error: error.toString(),
);
} finally { } finally {
ref.read(manualUploadCancelTokenProvider.notifier).state = null; ref.read(manualUploadCancelTokenProvider.notifier).state = null;
Future.delayed(const Duration(seconds: 2), () { Future.delayed(const Duration(seconds: 2), () {

View File

@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/services/remote_album.service.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart'; import 'package:immich_mobile/providers/album/pending_album_uploads.provider.dart';
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart';
@ -207,6 +208,22 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
return added; return added;
} }
/// Links a freshly-uploaded local asset to an album using its new remote ID,
/// upserting a placeholder remote asset row so the local DB join survives
/// until the next sync catches up.
Future<int> linkUploadedAssetToAlbum(String albumId, LocalAsset source, String remoteId) async {
final currentUser = ref.read(currentUserProvider);
if (currentUser == null) {
throw Exception('User not logged in');
}
final added = await _remoteAlbumService.linkUploadedAssetToAlbum(albumId, remoteId, currentUser, source);
if (added > 0) {
await _refreshAlbumInState(albumId);
}
return added;
}
/// Adds a heterogeneous asset selection to an album. Already-remote assets /// Adds a heterogeneous asset selection to an album. Already-remote assets
/// are linked immediately; local-only assets are queued in /// are linked immediately; local-only assets are queued in
/// [pendingAlbumUploadsProvider] (so the album page can show them with /// [pendingAlbumUploadsProvider] (so the album page can show them with
@ -221,11 +238,18 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
final pendingNotifier = ref.read(pendingAlbumUploadsProvider(albumId).notifier); final pendingNotifier = ref.read(pendingAlbumUploadsProvider(albumId).notifier);
pendingNotifier.enqueue(candidates.localAssetsToUpload); pendingNotifier.enqueue(candidates.localAssetsToUpload);
Completer<void>? cancelToken;
if (candidates.localAssetsToUpload.isNotEmpty) {
cancelToken = Completer<void>();
ref.read(manualUploadCancelTokenProvider.notifier).state = cancelToken;
}
try { try {
final added = await _remoteAlbumService.addAssetsToAlbum( final added = await _remoteAlbumService.addAssetsToAlbum(
albumId: albumId, albumId: albumId,
uploader: currentUser, uploader: currentUser,
candidates: candidates, candidates: candidates,
cancelToken: cancelToken,
uploadCallbacks: UploadCallbacks( uploadCallbacks: UploadCallbacks(
onProgress: (localAssetId, _, bytes, totalBytes) { onProgress: (localAssetId, _, bytes, totalBytes) {
final progress = totalBytes > 0 ? bytes / totalBytes : 0.0; final progress = totalBytes > 0 ? bytes / totalBytes : 0.0;
@ -245,6 +269,15 @@ class RemoteAlbumNotifier extends Notifier<RemoteAlbumState> {
} }
_logger.severe('Failed to add assets to album $albumId', error, stack); _logger.severe('Failed to add assets to album $albumId', error, stack);
rethrow; rethrow;
} finally {
if (cancelToken != null) {
if (cancelToken.isCompleted) {
pendingNotifier.clear();
}
if (ref.read(manualUploadCancelTokenProvider) == cancelToken) {
ref.read(manualUploadCancelTokenProvider.notifier).state = null;
}
}
} }
} }

View File

@ -1,11 +1,11 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
final metadataProvider = Provider.autoDispose<MetadataRepository>((_) => MetadataRepository.instance); final settingsProvider = Provider.autoDispose<SettingsRepository>((_) => SettingsRepository.instance);
final appConfigProvider = Provider.autoDispose<AppConfig>((ref) { final appConfigProvider = Provider.autoDispose<AppConfig>((ref) {
final repo = ref.watch(metadataProvider); final repo = ref.watch(settingsProvider);
final subscription = repo.watchConfig().listen((event) => ref.state = event); final subscription = repo.watchConfig().listen((event) => ref.state = event);
ref.onDispose(subscription.cancel); ref.onDispose(subscription.cancel);
return repo.appConfig; return repo.appConfig;

View File

@ -3,7 +3,7 @@ import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
final timelineRepositoryProvider = Provider<DriftTimelineRepository>( final timelineRepositoryProvider = Provider<DriftTimelineRepository>(
@ -29,7 +29,7 @@ final timelineServiceProvider = Provider<TimelineService>(
final timelineFactoryProvider = Provider<TimelineFactory>( final timelineFactoryProvider = Provider<TimelineFactory>(
(ref) => TimelineFactory( (ref) => TimelineFactory(
timelineRepository: ref.watch(timelineRepositoryProvider), timelineRepository: ref.watch(timelineRepositoryProvider),
metadataRepository: ref.watch(metadataProvider), settingsRepository: ref.watch(settingsProvider),
), ),
); );

View File

@ -1,8 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/models/map/map_state.model.dart'; import 'package:immich_mobile/models/map/map_state.model.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
final mapStateNotifierProvider = NotifierProvider<MapStateNotifier, MapState>(MapStateNotifier.new); final mapStateNotifierProvider = NotifierProvider<MapStateNotifier, MapState>(MapStateNotifier.new);
@ -27,12 +26,12 @@ class MapStateNotifier extends Notifier<MapState> {
} }
void switchTheme(ThemeMode mode) { void switchTheme(ThemeMode mode) {
ref.read(metadataProvider).write(MetadataKey.mapThemeMode, mode); ref.read(settingsProvider).write(.mapThemeMode, mode);
state = state.copyWith(themeMode: mode); state = state.copyWith(themeMode: mode);
} }
void switchFavoriteOnly(bool isFavoriteOnly) { void switchFavoriteOnly(bool isFavoriteOnly) {
ref.read(metadataProvider).write(MetadataKey.mapShowFavoriteOnly, isFavoriteOnly); ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly);
state = state.copyWith(showFavoriteOnly: isFavoriteOnly, shouldRefetchMarkers: true); state = state.copyWith(showFavoriteOnly: isFavoriteOnly, shouldRefetchMarkers: true);
} }
@ -41,17 +40,17 @@ class MapStateNotifier extends Notifier<MapState> {
} }
void switchIncludeArchived(bool isIncludeArchived) { void switchIncludeArchived(bool isIncludeArchived) {
ref.read(metadataProvider).write(MetadataKey.mapIncludeArchived, isIncludeArchived); ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived);
state = state.copyWith(includeArchived: isIncludeArchived, shouldRefetchMarkers: true); state = state.copyWith(includeArchived: isIncludeArchived, shouldRefetchMarkers: true);
} }
void switchWithPartners(bool isWithPartners) { void switchWithPartners(bool isWithPartners) {
ref.read(metadataProvider).write(MetadataKey.mapWithPartners, isWithPartners); ref.read(settingsProvider).write(.mapWithPartners, isWithPartners);
state = state.copyWith(withPartners: isWithPartners, shouldRefetchMarkers: true); state = state.copyWith(withPartners: isWithPartners, shouldRefetchMarkers: true);
} }
void setRelativeTime(int relativeTime) { void setRelativeTime(int relativeTime) {
ref.read(metadataProvider).write(MetadataKey.mapRelativeDate, relativeTime); ref.read(settingsProvider).write(.mapRelativeDate, relativeTime);
state = state.copyWith(relativeTime: relativeTime, shouldRefetchMarkers: true); state = state.copyWith(relativeTime: relativeTime, shouldRefetchMarkers: true);
} }
} }

View File

@ -1,5 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/theme/color_scheme.dart'; import 'package:immich_mobile/theme/color_scheme.dart';
import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart';
import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/theme/theme_data.dart';

View File

@ -7,7 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/network.repository.dar
import 'package:immich_mobile/models/server_info/server_version.model.dart'; import 'package:immich_mobile/models/server_info/server_version.model.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debounce.dart';
import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/debug_print.dart';
@ -193,7 +193,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
return; return;
} }
final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums; final isSyncAlbumEnabled = _ref.read(appConfigProvider).backup.syncAlbums;
try { try {
unawaited( unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) { _ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) {
@ -214,7 +214,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
return; return;
} }
final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums; final isSyncAlbumEnabled = _ref.read(appConfigProvider).backup.syncAlbums;
try { try {
unawaited( unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) { _ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) {

View File

@ -1,38 +1,40 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
final authRepositoryProvider = Provider<AuthRepository>( final authRepositoryProvider = Provider<AuthRepository>(
(ref) => AuthRepository(ref.watch(driftProvider), ref.watch(appConfigProvider)), (ref) => AuthRepository(ref.watch(driftProvider), ref.watch(settingsProvider)),
); );
class AuthRepository { class AuthRepository {
final Drift _drift; final Drift _drift;
final AppConfig _config; final SettingsRepository _settings;
const AuthRepository(this._drift, this._config); const AuthRepository(this._drift, this._settings);
Future<void> clearLocalData() async { Future<void> clearLocalData() async {
await SyncStreamRepository(_drift).reset(); await SyncStreamRepository(_drift).reset();
} }
bool getEndpointSwitchingFeature() { bool getEndpointSwitchingFeature() {
return _config.network.autoEndpointSwitching; return _settings.appConfig.network.autoEndpointSwitching;
} }
String? getPreferredWifiName() { String? getPreferredWifiName() {
return _config.network.preferredWifiName; return _settings.appConfig.network.preferredWifiName;
} }
String? getLocalEndpoint() { String? getLocalEndpoint() {
return _config.network.localEndpoint; return _settings.appConfig.network.localEndpoint;
} }
List<AuxilaryEndpoint> getExternalEndpointList() { List<AuxilaryEndpoint> getExternalEndpointList() {
return _config.network.externalEndpointList.map((url) => AuxilaryEndpoint(url: url, status: .valid)).toList(); return _settings.appConfig.network.externalEndpointList
.map((url) => AuxilaryEndpoint(url: url, status: .valid))
.toList();
} }
} }

View File

@ -5,7 +5,7 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/utils/url_helper.dart';
@ -13,7 +13,7 @@ import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class ApiService { class ApiService {
late ApiClient _apiClient; final ApiClient _apiClient = ApiClient(basePath: '');
late UsersApi usersApi; late UsersApi usersApi;
late AuthenticationApi authenticationApi; late AuthenticationApi authenticationApi;
@ -54,7 +54,7 @@ class ApiService {
} }
setEndpoint(String endpoint) { setEndpoint(String endpoint) {
_apiClient = ApiClient(basePath: endpoint); _apiClient.basePath = endpoint;
_apiClient.client = NetworkRepository.client; _apiClient.client = NetworkRepository.client;
usersApi = UsersApi(_apiClient); usersApi = UsersApi(_apiClient);
authenticationApi = AuthenticationApi(_apiClient); authenticationApi = AuthenticationApi(_apiClient);
@ -177,7 +177,7 @@ class ApiService {
if (serverEndpoint != null && serverEndpoint.isNotEmpty) { if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
urls.add(serverEndpoint); urls.add(serverEndpoint);
} }
final network = MetadataRepository.instance.appConfig.network; final network = SettingsRepository.instance.appConfig.network;
final localEndpoint = network.localEndpoint; final localEndpoint = network.localEndpoint;
if (localEndpoint.isNotEmpty) { if (localEndpoint.isNotEmpty) {
urls.add(localEndpoint); urls.add(localEndpoint);
@ -191,7 +191,7 @@ class ApiService {
} }
static Map<String, String> getRequestHeaders() { static Map<String, String> getRequestHeaders() {
return MetadataRepository.instance.appConfig.network.customHeaders; return SettingsRepository.instance.appConfig.network.customHeaders;
} }
ApiClient get apiClient => _apiClient; ApiClient get apiClient => _apiClient;

View File

@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart';
@ -100,7 +100,7 @@ class AuthService {
_log.severe("Error clearing local data", error, stackTrace); _log.severe("Error clearing local data", error, stackTrace);
}); });
await MetadataRepository.instance.write(MetadataKey.backupEnabled, false); await SettingsRepository.instance.write(SettingsKey.backupEnabled, false);
} }
} }
@ -110,7 +110,7 @@ class AuthService {
/// - Authentication repository data /// - Authentication repository data
/// - Current user information /// - Current user information
/// - Access token /// - Access token
/// - Asset ETag /// - Server-specific endpoint configuration
/// ///
/// All deletions are executed in parallel using [Future.wait]. /// All deletions are executed in parallel using [Future.wait].
Future<void> clearLocalData() async { Future<void> clearLocalData() async {
@ -120,6 +120,12 @@ class AuthService {
_authRepository.clearLocalData(), _authRepository.clearLocalData(),
Store.delete(StoreKey.currentUser), Store.delete(StoreKey.currentUser),
Store.delete(StoreKey.accessToken), Store.delete(StoreKey.accessToken),
SettingsRepository.instance.clear(const [
.networkAutoEndpointSwitching,
.networkPreferredWifiName,
.networkLocalEndpoint,
.networkExternalEndpointList,
]),
]); ]);
} }

View File

@ -13,7 +13,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
@ -359,7 +359,7 @@ class BackgroundUploadService {
} }
bool _shouldRequireWiFi(LocalAsset asset) { bool _shouldRequireWiFi(LocalAsset asset) {
final backup = MetadataRepository.instance.appConfig.backup; final backup = SettingsRepository.instance.appConfig.backup;
if (asset.isVideo && backup.useCellularForVideos) { if (asset.isVideo && backup.useCellularForVideos) {
return false; return false;
} }

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