Compare commits

..

21 Commits

Author SHA1 Message Date
shenlong-tanwen a821dab412 static analysis fix 2026-05-21 11:14:46 +05:30
shenlong-tanwen b70157f85e chore: upgrade flutter to 3.44.0 2026-05-21 10:57:02 +05:30
Caltsic b414b3d32b fix: improve form control focus visibility (#28512)
* Improve form control focus visibility

* fix: align form input focus styles
2026-05-20 15:33:56 -05:00
renovate[bot] 20da7c4267 chore(deps): lock file maintenance (terragrunt) (#28488) 2026-05-20 17:20:50 +02:00
renovate[bot] 92b6778d2d fix(deps): update typescript-projects (#28371)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-05-20 16:56:27 +02:00
Daniel Dietzler 5a61e589e8 chore: always run ci when mise.toml changes and install flutter from aqua (#28521) 2026-05-20 14:43:30 +00:00
renovate[bot] 85192bb110 chore(deps): update ghcr.io/jdx/mise docker tag to v2026.5.11 (#28522) 2026-05-20 14:29:17 +00:00
Timon c7ae97fa2b chore: handle docusaurus deprecation warning (#28516) 2026-05-20 15:27:33 +02:00
Timon 8d02f3625d chore: update mobile makefile command usage instructions (#28514) 2026-05-20 15:26:24 +02:00
bo0tzz a5a7380a26 feat: use lockfile for mise tools (#28503) 2026-05-20 11:37:33 +00:00
renovate[bot] d9ce3d2046 chore(deps): update dependency @types/node to ^24.12.4 (#28490) 2026-05-20 12:41:17 +02:00
renovate[bot] 815ff677fc chore(deps): update github-actions (#28493) 2026-05-19 22:22:44 +00:00
bo0tzz 915d865ce2 chore: use custom sticky-comment action (#28505) 2026-05-19 20:25:46 +00:00
immich-tofu[bot] c28e5f90b6 chore: modify .github/workflows/org-zizmor.yml 2026-05-19 10:45:23 +00:00
Timon 4383473ed6 fix: cleanup nestjs-zod properties (#28447)
* fix: cleanup nestjs-zod properties

* lint
2026-05-18 15:31:08 -04:00
shenlong 77701dd5a3 refactor: migrate backup config (#28483) 2026-05-19 00:40:10 +05:30
shenlong d4808fdc4d refactor: migrate album config (#28482)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-18 23:28:59 +05:30
renovate[bot] 7fa967a98e chore(deps): update dependency svelte to v5.55.7 [security] (#28434)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-05-18 17:42:01 +00:00
shenlong 9cffcc9f4e refactor: migrate network config (#28471) 2026-05-18 16:22:42 +00:00
shenlong 40925f0a06 refactor: immich form and text input (#28479)
refacotr: immich form

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-18 16:21:36 +00:00
Oliver Roed Schøler 0544d22902 feat: Selectable metadata in duplicates utility with diffing (#26328) 2026-05-18 17:49:51 +02:00
466 changed files with 9985 additions and 12708 deletions
+7 -7
View File
@@ -91,7 +91,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -159,14 +159,14 @@ jobs:
- name: Comment APK download link on PR
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
APK_URL: ${{ steps.upload-apk.outputs.artifact-url }}
with:
github-token: ${{ steps.token.outputs.token }}
message-id: 'mobile-android-apk'
message: |
id: mobile-android-apk
token: ${{ steps.token.outputs.token }}
body: |
📱 **Android release APK (universal)** — `${{ env.HEAD_SHA }}`
Download: ${{ env.APK_URL }}
@@ -216,7 +216,7 @@ jobs:
persist-credentials: false
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -231,7 +231,7 @@ jobs:
run: mise //mobile:codegen:pigeon
- name: Setup Ruby
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
uses: ruby/setup-ruby@6aaa311d81eba98ae12eaffbcb63296ace0efcde # v1.307.0
with:
ruby-version: '3.3'
bundler-cache: true
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
persist-credentials: false
- name: Check for breaking API changes
uses: oasdiff/oasdiff-action/breaking@26ccb332c67a45ca649de9faf60552ef1b8260d9 # v0.0.46
uses: oasdiff/oasdiff-action/breaking@6147a58e5d1249a12f42fc864ab791d571a30015 # v0.0.47
with:
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
revision: open-api/immich-openapi-specs.json
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
+3 -3
View File
@@ -57,7 +57,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -70,7 +70,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -83,6 +83,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
with:
category: '/language:${{matrix.language}}'
+1 -1
View File
@@ -66,7 +66,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
+3 -4
View File
@@ -131,7 +131,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -213,12 +213,11 @@ jobs:
run: 'mise run //deployment:tf apply'
- name: Comment
uses: actions-cool/maintain-one-comment@909842216bc8e8658364c572ec52100f4c2cc50a # v3.3.0
uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
if: ${{ steps.parameters.outputs.event == 'pr' }}
with:
id: docs-pr-url
token: ${{ steps.token.outputs.token }}
number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}
body: |
📖 Documentation deployed to [${{ steps.docs-output.outputs.subdomain }}](https://${{ steps.docs-output.outputs.subdomain }})
emojis: 'rocket'
body-include: '<!-- Docs PR URL -->'
+3 -4
View File
@@ -29,7 +29,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -44,9 +44,8 @@ jobs:
run: 'mise run //deployment:tf destroy -- -refresh=false'
- name: Comment
uses: actions-cool/maintain-one-comment@909842216bc8e8658364c572ec52100f4c2cc50a # v3.3.0
uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
with:
id: docs-pr-url
token: ${{ steps.token.outputs.token }}
number: ${{ github.event.number }}
delete: true
body-include: '<!-- Docs PR URL -->'
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- name: Generate a token
id: generate_token
if: ${{ inputs.skip != true }}
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
+1
View File
@@ -13,3 +13,4 @@ jobs:
actions: read
contents: read
security-events: write
secrets: inherit
+2 -2
View File
@@ -62,7 +62,7 @@ jobs:
ref: main
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -119,7 +119,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
+12 -12
View File
@@ -19,11 +19,11 @@ jobs:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
- uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
with:
github-token: ${{ steps.token.outputs.token }}
message-id: 'preview-status'
message: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.build/'
id: preview-status
token: ${{ steps.token.outputs.token }}
body: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.build/'
remove-label:
runs-on: ubuntu-latest
@@ -48,16 +48,16 @@ jobs:
name: 'preview'
})
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
- uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
if: ${{ github.event.pull_request.head.repo.fork }}
with:
github-token: ${{ steps.token.outputs.token }}
message-id: 'preview-status'
message: 'PRs from forks cannot have preview environments.'
id: preview-status
token: ${{ steps.token.outputs.token }}
body: 'PRs from forks cannot have preview environments.'
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
- uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
github-token: ${{ steps.token.outputs.token }}
message-id: 'preview-status'
message: 'Preview environment has been removed.'
id: preview-status
token: ${{ steps.token.outputs.token }}
body: 'Preview environment has been removed.'
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
+1 -1
View File
@@ -61,7 +61,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
+20 -13
View File
@@ -30,25 +30,32 @@ jobs:
filters: |
i18n:
- 'i18n/**'
- 'mise.toml'
web:
- 'web/**'
- 'i18n/**'
- 'packages/sdk/**'
- 'pnpm-lock.yaml'
- 'mise.toml'
server:
- 'server/**'
- 'pnpm-lock.yaml'
- 'mise.toml'
cli:
- 'packages/cli/**'
- 'packages/sdk/**'
- 'pnpm-lock.yaml'
- 'mise.toml'
e2e:
- 'e2e/**'
- 'pnpm-lock.yaml'
- 'mise.toml'
mobile:
- 'mobile/**'
- 'mise.toml'
machine-learning:
- 'machine-learning/**'
- 'mise.toml'
.github:
- '.github/**'
force-filters: |
@@ -76,7 +83,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -107,7 +114,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -138,7 +145,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -182,7 +189,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -220,7 +227,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -248,7 +255,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -298,7 +305,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -331,7 +338,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -550,7 +557,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -587,7 +594,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -618,7 +625,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -669,7 +676,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
@@ -727,7 +734,7 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
+65
View File
@@ -0,0 +1,65 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools.opentofu]]
version = "1.11.6"
backend = "aqua:opentofu/opentofu"
[tools.opentofu."platforms.linux-arm64"]
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
[tools.opentofu."platforms.linux-arm64-musl"]
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
[tools.opentofu."platforms.linux-x64"]
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
[tools.opentofu."platforms.linux-x64-musl"]
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
[tools.opentofu."platforms.macos-arm64"]
checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz"
[tools.opentofu."platforms.macos-x64"]
checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz"
[tools.opentofu."platforms.windows-x64"]
checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
[[tools.terragrunt]]
version = "1.0.3"
backend = "aqua:gruntwork-io/terragrunt"
[tools.terragrunt."platforms.linux-arm64"]
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
[tools.terragrunt."platforms.linux-arm64-musl"]
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
[tools.terragrunt."platforms.linux-x64"]
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
[tools.terragrunt."platforms.linux-x64-musl"]
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
[tools.terragrunt."platforms.macos-arm64"]
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
[tools.terragrunt."platforms.macos-x64"]
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
[tools.terragrunt."platforms.windows-x64"]
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
+3 -1
View File
@@ -10,7 +10,6 @@ const config = {
url: 'https://docs.immich.app',
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.png',
// GitHub pages deployment config.
@@ -29,6 +28,9 @@ const config = {
// Mermaid diagrams
markdown: {
mermaid: true,
hooks: {
onBrokenMarkdownLinks: 'warn',
},
},
themes: ['@docusaurus/theme-mermaid'],
+5
View File
@@ -0,0 +1,5 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools.wrangler]]
version = "4.66.0"
backend = "npm:wrangler"
+1 -1
View File
@@ -28,4 +28,4 @@ run = "prettier --write ."
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
[tools]
wrangler = "4.66.0"
wrangler = "4.91.0"
+1 -1
View File
@@ -32,7 +32,7 @@
"@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2",
"@types/node": "^24.12.2",
"@types/node": "^24.12.4",
"@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4",
"@types/supertest": "^7.0.0",
+10 -1
View File
@@ -897,6 +897,7 @@
"date_of_birth": "Date of birth",
"date_of_birth_saved": "Date of birth saved successfully",
"date_range": "Date range",
"date_time_original": "Date/Time Original",
"day": "Day",
"days": "Days",
"deduplicate_all": "Deduplicate All",
@@ -1197,11 +1198,13 @@
"export_as_json": "Export as JSON",
"export_database": "Export Database",
"export_database_description": "Export the SQLite database",
"exposure_time": "Exposure Time",
"extension": "Extension",
"external": "External",
"external_libraries": "External Libraries",
"external_network": "External network",
"external_network_sheet_info": "When not on the preferred Wi-Fi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
"f_number": "F-Number",
"face_unassigned": "Unassigned",
"failed": "Failed",
"failed_count": "Failed: {count}",
@@ -1219,7 +1222,6 @@
"features_setting_description": "Manage the app features",
"file_name_or_extension": "File name or extension",
"file_name_text": "File name",
"file_name_with_value": "File name: {file_name}",
"file_size": "File size",
"filename": "Filename",
"filetype": "Filetype",
@@ -1232,6 +1234,7 @@
"find_them_fast": "Find them fast by name with search",
"first": "First",
"fix_incorrect_match": "Fix incorrect match",
"focal_length": "Focal Length",
"folder": "Folder",
"folder_not_found": "Folder not found",
"folders": "Folders",
@@ -1352,6 +1355,7 @@
"ios_debug_info_no_sync_yet": "No background sync job has run yet",
"ios_debug_info_processes_queued": "{count, plural, one {{count} background process queued} other {{count} background processes queued}}",
"ios_debug_info_processing_ran_at": "Processing ran {dateTime}",
"iso": "ISO",
"items_count": "{count, plural, one {# item} other {# items}}",
"jobs": "Jobs",
"json_editor": "JSON editor",
@@ -1584,6 +1588,7 @@
"mobile_app": "Mobile App",
"mobile_app_download_onboarding_note": "Download the companion mobile app using the following options",
"model": "Model",
"modify_date": "Modify Date",
"month": "Month",
"more": "More",
"motion": "Motion",
@@ -1706,6 +1711,7 @@
"organize_into_albums": "Organize into albums",
"organize_into_albums_description": "Put existing photos into albums using current sync settings",
"organize_your_library": "Organize your library",
"orientation": "Orientation",
"original": "original",
"other": "Other",
"other_devices": "Other devices",
@@ -1820,6 +1826,7 @@
"profile_drawer_readonly_mode": "Read-only mode enabled. Long-press the user avatar icon to exit.",
"profile_image_of_user": "Profile image of {user}",
"profile_picture_set": "Profile picture set.",
"projection_type": "Projection Type",
"public_album": "Public album",
"public_share": "Public Share",
"purchase_account_info": "Supporter",
@@ -2189,7 +2196,9 @@
"show_in_timeline": "Show in timeline",
"show_in_timeline_setting_description": "Show photos and videos from this user in your timeline",
"show_keyboard_shortcuts": "Show keyboard shortcuts",
"show_less": "Show less",
"show_metadata": "Show metadata",
"show_more_fields": "{count, plural, one {Show # more field} other {Show # more fields}}",
"show_or_hide_info": "Show or hide info",
"show_password": "Show password",
"show_person_options": "Show person options",
+72
View File
@@ -0,0 +1,72 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools.python]]
version = "3.11.15"
backend = "core:python"
[tools.python."platforms.linux-arm64"]
checksum = "sha256:243f794278eff6adba96ed3677ec6877175df84c25f140e17f09f9be82d0f12a"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
provenance = "github-attestations"
[tools.python."platforms.linux-arm64-musl"]
checksum = "sha256:52b4c52094ff8b383a45c694acf4c5c0e883152be6d5229a35a8186ce907c6eb"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-aarch64-unknown-linux-musl-install_only_stripped.tar.gz"
provenance = "github-attestations"
[tools.python."platforms.linux-x64"]
checksum = "sha256:171dffd8c0f66e8a0725364a7428015b22fc18dd298b24f541392e17dd0e561f"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
provenance = "github-attestations"
[tools.python."platforms.linux-x64-musl"]
checksum = "sha256:2ac90fef8917ebd14826a6d667593a06cf0ae5f745ba9b1147dc086dd35f5284"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-x86_64-unknown-linux-musl-install_only_stripped.tar.gz"
provenance = "github-attestations"
[tools.python."platforms.macos-arm64"]
checksum = "sha256:fdfc363b538662eb7441a14e06f72c4a992c56af7f401f5730ea5081f8f8ad6e"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-aarch64-apple-darwin-install_only_stripped.tar.gz"
provenance = "github-attestations"
[tools.python."platforms.macos-x64"]
checksum = "sha256:5f1eb247cbca2c0ad5ccbf6d299a4f54b31b5c63b492d74c3531dc4344a42f88"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-x86_64-apple-darwin-install_only_stripped.tar.gz"
provenance = "github-attestations"
[tools.python."platforms.windows-x64"]
checksum = "sha256:756d7f148498b8822f6aedf44a020613576f09983161f346ad36dcef6238cdc3"
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
provenance = "github-attestations"
[[tools.uv]]
version = "0.8.15"
backend = "aqua:astral-sh/uv"
[tools.uv."platforms.linux-arm64"]
checksum = "sha256:23ea21a05c62c4c307ce691f29bff2f15c94c4f07f2b83d9b356f0664bc8b3a2"
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-unknown-linux-musl.tar.gz"
[tools.uv."platforms.linux-arm64-musl"]
checksum = "sha256:23ea21a05c62c4c307ce691f29bff2f15c94c4f07f2b83d9b356f0664bc8b3a2"
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-unknown-linux-musl.tar.gz"
[tools.uv."platforms.linux-x64"]
checksum = "sha256:d0fec58f3124e05e0a1af0f6541abfce4333253cdaf23c7b6bb2e6128bf138ea"
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-unknown-linux-musl.tar.gz"
[tools.uv."platforms.linux-x64-musl"]
checksum = "sha256:d0fec58f3124e05e0a1af0f6541abfce4333253cdaf23c7b6bb2e6128bf138ea"
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-unknown-linux-musl.tar.gz"
[tools.uv."platforms.macos-arm64"]
checksum = "sha256:103367962c5cb00bf7370d84cbaa3fec5a9807be9cc833ea9d8eea400c119fa2"
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-apple-darwin.tar.gz"
[tools.uv."platforms.macos-x64"]
checksum = "sha256:2bbef70982e97dfc36454de173f35ec1a5e83ae11e3885df6a50db3fd76171cb"
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-apple-darwin.tar.gz"
[tools.uv."platforms.windows-x64"]
checksum = "sha256:459d95892a5cc5c21779532f4f41b9238594b79e312a5142da2148ecfa10e705"
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-pc-windows-msvc.zip"
+417
View File
@@ -0,0 +1,417 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools."aqua:flutter/flutter"]]
version = "3.44.0"
backend = "aqua:flutter/flutter"
[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"
[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"
[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"
[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"
[tools."aqua:flutter/flutter"."platforms.macos-arm64"]
checksum = "blake3:fb03aa5d9790205c948922ec3f0751c16e4575b09d6ae9dd4fbeb664a69f0e00"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.0-stable.zip"
[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"
[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"
[[tools.flutter]]
version = "3.41.9-stable"
backend = "asdf:flutter"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.37.0"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64"]
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64-musl"]
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64"]
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64-musl"]
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-arm64"]
checksum = "sha256:30bede64367d09067093cc57af6ec9496d7717898138ded5cb98a16ac8dd9d93"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543757"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-x64"]
checksum = "sha256:e56cb99872be7445a4de1d37e5438ca70e3bcd83be7a2b9b385e3538881f8068"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543727"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.windows-x64"]
checksum = "sha256:f133470daa3fb0427f039b424392af7e917d7e7db6b556aa2a968ab0e31587da"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-windows-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543660"
github_attestations = "unavailable"
[[tools."github:extism/cli"]]
version = "1.6.3"
backend = "github:extism/cli"
[tools."github:extism/cli"."platforms.linux-arm64"]
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.linux-arm64-musl"]
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.linux-x64"]
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.linux-x64-musl"]
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.macos-arm64"]
checksum = "sha256:b4ddbc575b5ac000115247f781723f9b9f284ed87b29c600539d72161b5b29fc"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-arm64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694029"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.macos-x64"]
checksum = "sha256:9a2f71b6e6009685a622cc3084e52d2a1a8e23c98d29ffa72e666e9dc699855f"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-amd64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694026"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.windows-x64"]
checksum = "sha256:47e4ed2782445b2b08a4d1ac127211588f8b4d1fc25fd6481d4cb65151b5213c"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-windows-amd64.zip"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694035"
github_attestations = "unavailable"
[[tools."github:extism/js-pdk"]]
version = "1.6.0"
backend = "github:extism/js-pdk"
[tools."github:extism/js-pdk"."platforms.linux-arm64"]
checksum = "sha256:15a186250e68d6bff4ec839fff275d45a90e383a69209dcc1239eb9e3aee6e1b"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-linux-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223214"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.linux-arm64-musl"]
checksum = "sha256:15a186250e68d6bff4ec839fff275d45a90e383a69209dcc1239eb9e3aee6e1b"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-linux-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223214"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.linux-x64"]
checksum = "sha256:4ded271ccf465031ccd0dc35e7a140e134d7f30721671cc4a8e1ff805d4aad68"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-linux-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223119"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.linux-x64-musl"]
checksum = "sha256:4ded271ccf465031ccd0dc35e7a140e134d7f30721671cc4a8e1ff805d4aad68"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-linux-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223119"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.macos-arm64"]
checksum = "sha256:548e25bda3971a07c32d78a249135cf8cb7b3eede101e878e06e53e01ac2e0ce"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-macos-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223215"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.macos-x64"]
checksum = "sha256:d85a875c2a071f0c29fe572764c52c3a499f157ab7f9efac8939a4364390e29b"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-macos-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223239"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.windows-x64"]
checksum = "sha256:97b7b746141e4777e1ca2b76febdeb16dc9d314ff6a4257df05a476b67228acc"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-windows-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133"
github_attestations = "unavailable"
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"]
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"]
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-x64"]
checksum = "sha256:066ede9774aaae97a18098aaeea8b7e0d286653eb8618f640476e99c59a536c2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_mac64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995889"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"]
checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094"
github_attestations = "unavailable"
[[tools."github:webassembly/binaryen"]]
version = "version_124"
backend = "github:webassembly/binaryen"
[tools."github:webassembly/binaryen"."platforms.linux-arm64"]
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.linux-arm64-musl"]
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.linux-x64"]
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.linux-x64-musl"]
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.macos-arm64"]
checksum = "sha256:86a2c960ff62c6d2ea6009d1f89745c22c70100d394a095eab45eb941bdaa24c"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-arm64-macos.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926134"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.macos-x64"]
checksum = "sha256:b389bb0731758d86c3cb266d01d28a12725c23bd3cabc3df34faa162af0887e9"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-macos.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926135"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.windows-x64"]
checksum = "sha256:b5e1d2a1ad3c03229ddc89823848f4a1c11f9c6402a51fa26f0aaa5f1d7a2203"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-windows.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288925833"
github_attestations = "unavailable"
[[tools.java]]
version = "21.0.2"
backend = "core:java"
[tools.java."platforms.linux-arm64"]
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-aarch64_bin.tar.gz"
[tools.java."platforms.linux-x64"]
checksum = "sha256:a2def047a73941e01a73739f92755f86b895811afb1f91243db214cff5bdac3f"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz"
[tools.java."platforms.macos-arm64"]
checksum = "sha256:b3d588e16ec1e0ef9805d8a696591bd518a5cea62567da8f53b5ce32d11d22e4"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-aarch64_bin.tar.gz"
[tools.java."platforms.macos-x64"]
checksum = "sha256:8fd09e15dc406387a0aba70bf5d99692874e999bf9cd9208b452b5d76ac922d3"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-x64_bin.tar.gz"
[tools.java."platforms.windows-x64"]
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
[[tools.node]]
version = "24.15.0"
backend = "core:node"
[tools.node."platforms.linux-arm64"]
checksum = "sha256:73afc234d558c24919875f51c2d1ea002a2ada4ea6f83601a383869fefa64eed"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-linux-arm64.tar.gz"
[tools.node."platforms.linux-arm64-musl"]
checksum = "sha256:31e98aa960a067da91edffd5d93bc46657b5d2a8029612c359f5f2ac0060152a"
url = "https://unofficial-builds.nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-arm64-musl.tar.gz"
[tools.node."platforms.linux-x64"]
checksum = "sha256:44836872d9aec49f1e6b52a9a922872db9a2b02d235a616a5681b6a85fec8d89"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-linux-x64.tar.gz"
[tools.node."platforms.linux-x64-musl"]
checksum = "sha256:f55af5bd489c5347b113ca6594cae00a54b30ba57ac5875324311bfc6f4762e3"
url = "https://unofficial-builds.nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-x64-musl.tar.gz"
[tools.node."platforms.macos-arm64"]
checksum = "sha256:372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-darwin-arm64.tar.gz"
[tools.node."platforms.macos-x64"]
checksum = "sha256:ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-darwin-x64.tar.gz"
[tools.node."platforms.windows-x64"]
checksum = "sha256:cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-win-x64.zip"
[[tools."npm:oazapfts"]]
version = "7.5.0"
backend = "npm:oazapfts"
[[tools.opentofu]]
version = "1.11.6"
backend = "aqua:opentofu/opentofu"
[tools.opentofu."platforms.linux-arm64"]
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
[tools.opentofu."platforms.linux-arm64-musl"]
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
[tools.opentofu."platforms.linux-x64"]
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
[tools.opentofu."platforms.linux-x64-musl"]
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
[tools.opentofu."platforms.macos-arm64"]
checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz"
[tools.opentofu."platforms.macos-x64"]
checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz"
[tools.opentofu."platforms.windows-x64"]
checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
[[tools.pnpm]]
version = "10.33.1"
backend = "aqua:pnpm/pnpm"
[tools.pnpm."platforms.linux-arm64"]
checksum = "sha256:ed8aa7901cf325f4cf5019405bdd6bf988426e4b23d08fe9b12ea4df7046f23e"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-linux-arm64"
[tools.pnpm."platforms.linux-arm64-musl"]
checksum = "sha256:ed8aa7901cf325f4cf5019405bdd6bf988426e4b23d08fe9b12ea4df7046f23e"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-linux-arm64"
[tools.pnpm."platforms.linux-x64"]
checksum = "sha256:fba950842532edd365e949b74643b64e6311089a45532dbe1e8f909a247fe3e9"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-linux-x64"
[tools.pnpm."platforms.linux-x64-musl"]
checksum = "sha256:fba950842532edd365e949b74643b64e6311089a45532dbe1e8f909a247fe3e9"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-linux-x64"
[tools.pnpm."platforms.macos-arm64"]
checksum = "sha256:909ced0038b00881d4d620ba2018c5d9691de373deea8e3c84b722b44324e47c"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-macos-arm64"
[tools.pnpm."platforms.macos-x64"]
checksum = "sha256:afdad60b83f4f482f4c95cc79325f29aef776d0922a324f023a312f40e0cc7d3"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-macos-x64"
[tools.pnpm."platforms.windows-x64"]
checksum = "sha256:67b23fd8c6800566b1cc04c446b170ff6e7977250084e4d8df9bfdbd8e6f4d02"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-win-x64.exe"
[[tools.terragrunt]]
version = "1.0.3"
backend = "aqua:gruntwork-io/terragrunt"
[tools.terragrunt."platforms.linux-arm64"]
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
[tools.terragrunt."platforms.linux-arm64-musl"]
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
[tools.terragrunt."platforms.linux-x64"]
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
[tools.terragrunt."platforms.linux-x64-musl"]
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
[tools.terragrunt."platforms.macos-arm64"]
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
[tools.terragrunt."platforms.macos-x64"]
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
[tools.terragrunt."platforms.windows-x64"]
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
+6 -5
View File
@@ -16,8 +16,8 @@ config_roots = [
[tools]
node = "24.15.0"
flutter = "3.41.9"
pnpm = "10.33.1"
"aqua:flutter/flutter" = "3.44.0"
pnpm = "10.33.4"
terragrunt = "1.0.3"
opentofu = "1.11.6"
java = "21.0.2"
@@ -50,11 +50,12 @@ macos-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
[settings]
experimental = true
pin = true
lockfile = true
[tasks.plugins]
run = [
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile",
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core build"
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core build",
]
[tasks.open-api-typescript]
@@ -76,8 +77,8 @@ run = [
{ task = "//server:install" },
{ task = "//server:build" },
{ task = "//server:sync-open-api" },
{ task = ":open-api-typescript"},
{ task = ":open-api-dart"},
{ task = ":open-api-typescript" },
{ task = ":open-api-dart" },
]
[tasks.sql]
-1
View File
@@ -1,5 +1,4 @@
{
"dart.flutterSdkPath": ".fvm/versions/3.41.9",
"dart.lineLength": 120,
"[dart]": {
"editor.rulers": [
+7
View File
@@ -89,6 +89,13 @@ flutter {
}
dependencies {
constraints {
implementation("androidx.glance:glance-appwidget") {
version { strictly libs.versions.glance.get() }
because 'home_widget requests 1.+ which can resolve to pre-releases incompatible with our compileSdk/AGP'
}
}
implementation libs.okhttp
implementation libs.cronet.embedded
implementation libs.media3.datasource.okhttp
+4
View File
@@ -5,3 +5,7 @@ android.nonTransitiveRClass=false
android.nonFinalResIds=false
org.gradle.caching=true
org.gradle.parallel=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
-123
View File
@@ -1,58 +1,23 @@
PODS:
- background_downloader (0.0.1):
- Flutter
- bonsoir_darwin (0.0.1):
- Flutter
- FlutterMacOS
- connectivity_plus (0.0.1):
- Flutter
- cupertino_http (0.0.1):
- Flutter
- FlutterMacOS
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (2.4.3):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- flutter_udid (0.0.1):
- Flutter
- KeychainAccess
- flutter_web_auth_2 (5.0.0):
- Flutter
- fluttertoast (0.0.2):
- Flutter
- geolocator_apple (1.2.0):
- Flutter
- FlutterMacOS
- home_widget (0.0.1):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- integration_test (0.0.1):
- Flutter
- KeychainAccess (4.2.2)
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
- MapLibre (6.14.0)
- maplibre_gl (0.0.1):
- Flutter
- MapLibre (= 6.14.0)
- native_video_player (1.0.0):
- Flutter
- network_info_plus (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- permission_handler_apple (9.3.0):
- Flutter
- photo_manager (3.9.0):
- Flutter
- FlutterMacOS
- share_handler_ios (0.0.14):
- Flutter
- share_handler_ios/share_handler_ios_models (= 0.0.14)
@@ -61,144 +26,56 @@ PODS:
- Flutter
- share_handler_ios_models
- share_handler_ios_models (0.0.9)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
- Flutter
DEPENDENCIES:
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- home_widget (from `.symlinks/plugins/home_widget/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/darwin`)
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
trunk:
- KeychainAccess
- MapLibre
EXTERNAL SOURCES:
background_downloader:
:path: ".symlinks/plugins/background_downloader/ios"
bonsoir_darwin:
:path: ".symlinks/plugins/bonsoir_darwin/darwin"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
cupertino_http:
:path: ".symlinks/plugins/cupertino_http/darwin"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_udid:
:path: ".symlinks/plugins/flutter_udid/ios"
flutter_web_auth_2:
:path: ".symlinks/plugins/flutter_web_auth_2/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
geolocator_apple:
:path: ".symlinks/plugins/geolocator_apple/darwin"
home_widget:
:path: ".symlinks/plugins/home_widget/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
maplibre_gl:
:path: ".symlinks/plugins/maplibre_gl/ios"
native_video_player:
:path: ".symlinks/plugins/native_video_player/ios"
network_info_plus:
:path: ".symlinks/plugins/network_info_plus/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/darwin"
share_handler_ios:
:path: ".symlinks/plugins/share_handler_ios/ios"
share_handler_ios_models:
:path: ".symlinks/plugins/share_handler_ios/ios/Models"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
flutter_web_auth_2: 646fc9df97a01c59e5eea99b237da2c6360f8439
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
PODFILE CHECKSUM: 938abbae4114b9c2140c550a2a0d8f7c674f5dfe
@@ -37,6 +37,7 @@
FEE084F82EC172460045228E /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084F72EC172460045228E /* SQLiteData */; };
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FA2EC1725A0045228E /* RawStructuredFieldValues */; };
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FC2EC1725A0045228E /* StructuredFieldValues */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -125,6 +126,7 @@
FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImagesImpl.swift; sourceTree = "<group>"; };
FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessing.swift; sourceTree = "<group>"; };
FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbhash.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -189,6 +191,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
FEE084F82EC172460045228E /* SQLiteData in Frameworks */,
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */,
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */,
@@ -243,6 +246,7 @@
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -346,6 +350,9 @@
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
@@ -449,6 +456,7 @@
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
);
@@ -1272,7 +1280,17 @@
package = FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */;
productName = StructuredFieldValues;
};
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -1,5 +1,4 @@
{
"originHash" : "9be33bfaa68721646604aefff3cabbdaf9a193da192aae024c265065671f6c49",
"pins" : [
{
"identity" : "combine-schedulers",
@@ -19,6 +18,24 @@
"version" : "7.8.0"
}
},
{
"identity" : "keychainaccess",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kishikawakatsumi/KeychainAccess",
"state" : {
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
"version" : "4.2.2"
}
},
{
"identity" : "maplibre-gl-native-distribution",
"kind" : "remoteSourceControl",
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git",
"state" : {
"revision" : "60d9bb85c94ce6e7fc4406cd32529fd12bdb7809",
"version" : "6.14.0"
}
},
{
"identity" : "sqlite-data",
"kind" : "remoteSourceControl",
@@ -146,5 +163,5 @@
}
}
],
"version" : 3
"version" : 2
}
@@ -5,6 +5,24 @@
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Immich.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
@@ -1,5 +1,4 @@
{
"originHash" : "9be33bfaa68721646604aefff3cabbdaf9a193da192aae024c265065671f6c49",
"pins" : [
{
"identity" : "combine-schedulers",
@@ -19,6 +18,24 @@
"version" : "7.9.0"
}
},
{
"identity" : "keychainaccess",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kishikawakatsumi/KeychainAccess",
"state" : {
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
"version" : "4.2.2"
}
},
{
"identity" : "maplibre-gl-native-distribution",
"kind" : "remoteSourceControl",
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git",
"state" : {
"revision" : "60d9bb85c94ce6e7fc4406cd32529fd12bdb7809",
"version" : "6.14.0"
}
},
{
"identity" : "sqlite-data",
"kind" : "remoteSourceControl",
@@ -146,5 +163,5 @@
}
}
],
"version" : 3
"version" : 2
}
@@ -0,0 +1,26 @@
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
class AlbumConfig {
final AlbumSortMode sortMode;
final bool isReverse;
final bool isGrid;
const AlbumConfig({this.sortMode = AlbumSortMode.mostRecent, this.isReverse = true, this.isGrid = false});
AlbumConfig copyWith({AlbumSortMode? sortMode, bool? isReverse, bool? isGrid}) => AlbumConfig(
sortMode: sortMode ?? this.sortMode,
isReverse: isReverse ?? this.isReverse,
isGrid: isGrid ?? this.isGrid,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is AlbumConfig && other.sortMode == sortMode && other.isReverse == isReverse && other.isGrid == isGrid);
@override
int get hashCode => Object.hash(sortMode, isReverse, isGrid);
@override
String toString() => 'AlbumConfig(sortMode: $sortMode, isReverse: $isReverse, isGrid: $isGrid)';
}
@@ -1,10 +1,12 @@
import 'package:immich_mobile/domain/models/config/album_config.dart';
import 'package:immich_mobile/domain/models/config/backup_config.dart';
import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
import 'package:immich_mobile/domain/models/config/image_config.dart';
import 'package:immich_mobile/domain/models/config/map_config.dart';
import 'package:immich_mobile/domain/models/config/slideshow_config.dart';
import 'package:immich_mobile/domain/models/config/theme_config.dart';
import 'package:immich_mobile/domain/models/config/timeline_config.dart';
import 'package:immich_mobile/domain/models/config/viewer_config.dart';
import 'package:immich_mobile/domain/models/config/slideshow_config.dart';
class AppConfig {
final ThemeConfig theme;
@@ -14,6 +16,8 @@ class AppConfig {
final ImageConfig image;
final ViewerConfig viewer;
final SlideshowConfig slideshow;
final AlbumConfig album;
final BackupConfig backup;
const AppConfig({
this.theme = const .new(),
@@ -23,6 +27,8 @@ class AppConfig {
this.image = const .new(),
this.viewer = const .new(),
this.slideshow = const .new(),
this.album = const .new(),
this.backup = const .new(),
});
AppConfig copyWith({
@@ -33,6 +39,8 @@ class AppConfig {
ImageConfig? image,
ViewerConfig? viewer,
SlideshowConfig? slideshow,
AlbumConfig? album,
BackupConfig? backup,
}) => .new(
theme: theme ?? this.theme,
cleanup: cleanup ?? this.cleanup,
@@ -41,6 +49,8 @@ class AppConfig {
image: image ?? this.image,
viewer: viewer ?? this.viewer,
slideshow: slideshow ?? this.slideshow,
album: album ?? this.album,
backup: backup ?? this.backup,
);
@override
@@ -53,12 +63,14 @@ class AppConfig {
other.timeline == timeline &&
other.image == image &&
other.viewer == viewer &&
other.slideshow == slideshow);
other.slideshow == slideshow &&
other.album == album &&
other.backup == backup);
@override
int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, slideshow);
int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, slideshow, album, backup);
@override
String toString() =>
'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow)';
'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup)';
}
@@ -0,0 +1,52 @@
class BackupConfig {
final bool enabled;
final bool useCellularForVideos;
final bool useCellularForPhotos;
final bool requireCharging;
final int triggerDelay;
final bool syncAlbums;
const BackupConfig({
this.enabled = false,
this.useCellularForVideos = false,
this.useCellularForPhotos = false,
this.requireCharging = false,
this.triggerDelay = 30,
this.syncAlbums = false,
});
BackupConfig copyWith({
bool? enabled,
bool? useCellularForVideos,
bool? useCellularForPhotos,
bool? requireCharging,
int? triggerDelay,
bool? syncAlbums,
}) => BackupConfig(
enabled: enabled ?? this.enabled,
useCellularForVideos: useCellularForVideos ?? this.useCellularForVideos,
useCellularForPhotos: useCellularForPhotos ?? this.useCellularForPhotos,
requireCharging: requireCharging ?? this.requireCharging,
triggerDelay: triggerDelay ?? this.triggerDelay,
syncAlbums: syncAlbums ?? this.syncAlbums,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is BackupConfig &&
other.enabled == enabled &&
other.useCellularForVideos == useCellularForVideos &&
other.useCellularForPhotos == useCellularForPhotos &&
other.requireCharging == requireCharging &&
other.triggerDelay == triggerDelay &&
other.syncAlbums == syncAlbums);
@override
int get hashCode =>
Object.hash(enabled, useCellularForVideos, useCellularForPhotos, requireCharging, triggerDelay, syncAlbums);
@override
String toString() =>
'BackupConfig(enabled: $enabled, useCellularForVideos: $useCellularForVideos, useCellularForPhotos: $useCellularForPhotos, requireCharging: $requireCharging, triggerDelay: $triggerDelay, syncAlbums: $syncAlbums)';
}
@@ -0,0 +1,54 @@
import 'package:flutter/foundation.dart';
class NetworkConfig {
final bool autoEndpointSwitching;
final String? preferredWifiName;
final String? localEndpoint;
final List<String> externalEndpointList;
final Map<String, String> customHeaders;
const NetworkConfig({
this.autoEndpointSwitching = false,
this.preferredWifiName,
this.localEndpoint,
this.externalEndpointList = const [],
this.customHeaders = const {},
});
NetworkConfig copyWith({
bool? autoEndpointSwitching,
String? preferredWifiName,
String? localEndpoint,
List<String>? externalEndpointList,
Map<String, String>? customHeaders,
}) => NetworkConfig(
autoEndpointSwitching: autoEndpointSwitching ?? this.autoEndpointSwitching,
preferredWifiName: preferredWifiName ?? this.preferredWifiName,
localEndpoint: localEndpoint ?? this.localEndpoint,
externalEndpointList: externalEndpointList ?? this.externalEndpointList,
customHeaders: customHeaders ?? this.customHeaders,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is NetworkConfig &&
other.autoEndpointSwitching == autoEndpointSwitching &&
other.preferredWifiName == preferredWifiName &&
other.localEndpoint == localEndpoint &&
listEquals(other.externalEndpointList, externalEndpointList) &&
mapEquals(other.customHeaders, customHeaders));
@override
int get hashCode => Object.hash(
autoEndpointSwitching,
preferredWifiName,
localEndpoint,
Object.hashAll(externalEndpointList),
Object.hashAllUnordered(customHeaders.entries.map((e) => Object.hash(e.key, e.value))),
);
@override
String toString() =>
'NetworkConfig(autoEndpointSwitching: $autoEndpointSwitching, preferredWifiName: $preferredWifiName, localEndpoint: $localEndpoint, externalEndpointList: $externalEndpointList, customHeaders: $customHeaders)';
}
@@ -1,18 +1,22 @@
import 'package:immich_mobile/domain/models/config/network_config.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
class SystemConfig {
final LogLevel logLevel;
final NetworkConfig network;
const SystemConfig({this.logLevel = .info});
const SystemConfig({this.logLevel = .info, this.network = const .new()});
SystemConfig copyWith({LogLevel? logLevel}) => SystemConfig(logLevel: logLevel ?? this.logLevel);
SystemConfig copyWith({LogLevel? logLevel, NetworkConfig? network}) =>
SystemConfig(logLevel: logLevel ?? this.logLevel, network: network ?? this.network);
@override
bool operator ==(Object other) => identical(this, other) || (other is SystemConfig && other.logLevel == logLevel);
bool operator ==(Object other) =>
identical(this, other) || (other is SystemConfig && other.logLevel == logLevel && other.network == network);
@override
int get hashCode => logLevel.hashCode;
int get hashCode => Object.hash(logLevel, network);
@override
String toString() => 'SystemConfig(logLevel: $logLevel)';
String toString() => 'SystemConfig(logLevel: $logLevel, network: $network)';
}
@@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/config/system_config.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
enum MetadataDomain<T extends Object> {
appConfig<AppConfig>('config.app'),
@@ -34,6 +35,41 @@ enum MetadataKey<T extends Object> {
viewerAutoPlayVideo<bool>(.appConfig, 'viewer.autoPlayVideo', true),
viewerTapToNavigate<bool>(.appConfig, 'viewer.tapToNavigate', false),
// Network
networkAutoEndpointSwitching<bool>(.systemConfig, 'network.autoEndpointSwitching', false),
networkPreferredWifiName<String>(.systemConfig, 'network.preferredWifiName', ''),
networkLocalEndpoint<String>(.systemConfig, 'network.localEndpoint', ''),
networkExternalEndpointList<List<String>>(
.systemConfig,
'network.externalEndpointList',
[],
_ListCodec(_PrimitiveCodec.string),
),
networkCustomHeaders<Map<String, String>>(
.systemConfig,
'network.customHeaders',
{},
_MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string),
),
// Album
albumSortMode<AlbumSortMode>(
.appConfig,
'album.sortMode',
AlbumSortMode.mostRecent,
_EnumCodec(AlbumSortMode.values),
),
albumIsReverse<bool>(.appConfig, 'album.isReverse', true),
albumIsGrid<bool>(.appConfig, 'album.isGrid', false),
// Backup
backupEnabled<bool>(.appConfig, 'backup.enabled', false),
backupUseCellularForVideos<bool>(.appConfig, 'backup.useCellularForVideos', false),
backupUseCellularForPhotos<bool>(.appConfig, 'backup.useCellularForPhotos', false),
backupRequireCharging<bool>(.appConfig, 'backup.requireCharging', false),
backupTriggerDelay<int>(.appConfig, 'backup.triggerDelay', 30),
backupSyncAlbums<bool>(.appConfig, 'backup.syncAlbums', false),
// Timeline
timelineTilesPerRow<int>(.appConfig, 'timeline.tilesPerRow', 4),
timelineGroupAssetsBy<GroupAssetsBy>(
@@ -143,6 +179,47 @@ final class _DateTimeCodec extends _MetadataCodec<DateTime> {
DateTime? decode(String raw) => DateTime.tryParse(raw);
}
final class _MapCodec<K extends Object, V extends Object> extends _MetadataCodec<Map<K, V>> {
final _MetadataCodec<K> _keyCodec;
final _MetadataCodec<V> _valueCodec;
const _MapCodec(this._keyCodec, this._valueCodec);
@override
String encode(Map<K, V> value) {
final entries = <String, String>{};
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
return jsonEncode(entries);
}
@override
Map<K, V>? decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! Map) {
return null;
}
final result = <K, V>{};
for (final entry in decoded.entries) {
final rawKey = entry.key;
final rawValue = entry.value;
if (rawKey is! String || rawValue is! String) {
return null;
}
final k = _keyCodec.decode(rawKey);
final v = _valueCodec.decode(rawValue);
if (k == null || v == null) {
return null;
}
result[k] = v;
}
return result;
} on FormatException {
return null;
}
}
}
final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
final _MetadataCodec<T> _elementCodec;
+1 -2
View File
@@ -1,8 +1,7 @@
import 'package:immich_mobile/domain/models/store.model.dart';
enum Setting<T> {
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
enableBackup<bool>(StoreKey.enableBackup, false);
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false);
const Setting(this.storeKey, this.defaultValue);
+14 -20
View File
@@ -6,39 +6,33 @@ enum StoreKey<T> {
version<int>._(0),
currentUser<UserDto>._(2),
deviceId<String>._(4),
backupRequireCharging<bool>._(7),
backupTriggerDelay<int>._(8),
serverUrl<String>._(10),
accessToken<String>._(11),
serverEndpoint<String>._(12),
selectedAlbumSortOrder<int>._(113),
advancedTroubleshooting<bool>._(114),
selectedAlbumSortReverse<bool>._(123),
enableHapticFeedback<bool>._(126),
customHeaders<String>._(127),
syncAlbums<bool>._(131),
// Auto endpoint switching
autoEndpointSwitching<bool>._(132),
preferredWifiName<String>._(133),
localEndpoint<String>._(134),
externalEndpointList<String>._(135),
manageLocalMediaAndroid<bool>._(137),
// Read-only Mode settings
readonlyModeEnabled<bool>._(138),
albumGridView<bool>._(140),
// Image viewer navigation settings
tapToNavigate<bool>._(141),
// Experimental stuff
enableBackup<bool>._(1003),
useWifiForUploadVideos<bool>._(1004),
useWifiForUploadPhotos<bool>._(1005),
syncMigrationStatus<String>._(1013),
// Legacy keys that have been migrated to the new metadata store
legacyBackupRequireCharging<bool>._(7),
legacyBackupTriggerDelay<int>._(8),
legacySyncAlbums<bool>._(131),
legacyEnableBackup<bool>._(1003),
legacyUseWifiForUploadVideos<bool>._(1004),
legacyUseWifiForUploadPhotos<bool>._(1005),
legacySelectedAlbumSortOrder<int>._(113),
legacySelectedAlbumSortReverse<bool>._(123),
legacyAlbumGridView<bool>._(140),
legacyAutoEndpointSwitching<bool>._(132),
legacyPreferredWifiName<String>._(133),
legacyLocalEndpoint<String>._(134),
legacyExternalEndpointList<String>._(135),
legacyCustomHeaders<String>._(127),
legacyLoopVideo<bool>._(117),
legacyLoadOriginalVideo<bool>._(136),
legacyAutoPlayVideo<bool>._(139),
@@ -8,11 +8,7 @@ class AssetService {
final RemoteAssetRepository _remoteAssetRepository;
final DriftLocalAssetRepository _localAssetRepository;
const AssetService({
required RemoteAssetRepository remoteAssetRepository,
required DriftLocalAssetRepository localAssetRepository,
}) : _remoteAssetRepository = remoteAssetRepository,
_localAssetRepository = localAssetRepository;
const AssetService({required this._remoteAssetRepository, required this._localAssetRepository});
Future<BaseAsset?> getAsset(BaseAsset asset) {
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
@@ -11,15 +11,14 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.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/metadata.repository.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/providers/app_settings.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/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart' show nativeSyncApiProvider;
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/services/auth.service.dart';
import 'package:immich_mobile/services/foreground_upload.service.dart';
import 'package:immich_mobile/services/localization.service.dart';
@@ -39,16 +38,15 @@ class BackgroundWorkerFgService {
Future<void> saveNotificationMessage(String title, String body) =>
_foregroundHostApi.saveNotificationMessage(title, body);
Future<void> configure({int? minimumDelaySeconds, bool? requireCharging}) => _foregroundHostApi.configure(
BackgroundWorkerSettings(
minimumDelaySeconds:
minimumDelaySeconds ??
Store.get(AppSettingsEnum.backupTriggerDelay.storeKey, AppSettingsEnum.backupTriggerDelay.defaultValue),
requiresCharging:
requireCharging ??
Store.get(AppSettingsEnum.backupRequireCharging.storeKey, AppSettingsEnum.backupRequireCharging.defaultValue),
),
);
Future<void> configure({int? minimumDelaySeconds, bool? requireCharging}) {
final backup = MetadataRepository.instance.appConfig.backup;
return _foregroundHostApi.configure(
BackgroundWorkerSettings(
minimumDelaySeconds: minimumDelaySeconds ?? backup.triggerDelay,
requiresCharging: requireCharging ?? backup.requireCharging,
),
);
}
Future<void> disable() => _foregroundHostApi.disable();
}
@@ -63,15 +61,13 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
bool _isCleanedUp = false;
BackgroundWorkerBgService({required Drift drift, required DriftLogger driftLogger})
: _drift = drift,
_driftLogger = driftLogger,
_backgroundHostApi = BackgroundWorkerBgHostApi() {
_ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(drift))]);
BackgroundWorkerBgService({required this._drift, required this._driftLogger})
: _backgroundHostApi = BackgroundWorkerBgHostApi() {
_ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(_drift))]);
BackgroundWorkerFlutterApi.setUp(this);
}
bool get _isBackupEnabled => _ref?.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup) ?? false;
bool get _isBackupEnabled => MetadataRepository.instance.appConfig.backup.enabled;
Future<void> init() async {
try {
+6 -11
View File
@@ -21,18 +21,13 @@ class HashService {
final _log = Logger('HashService');
HashService({
required DriftLocalAlbumRepository localAlbumRepository,
required DriftLocalAssetRepository localAssetRepository,
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required NativeSyncApi nativeSyncApi,
bool Function()? cancelChecker,
required this._localAlbumRepository,
required this._localAssetRepository,
required this._trashedLocalAssetRepository,
required this._nativeSyncApi,
this._cancelChecker,
int? batchSize,
}) : _localAlbumRepository = localAlbumRepository,
_localAssetRepository = localAssetRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository,
_cancelChecker = cancelChecker,
_nativeSyncApi = nativeSyncApi,
_batchSize = batchSize ?? kBatchHashFileLimit;
}) : _batchSize = batchSize ?? kBatchHashFileLimit;
bool get isCancelled => _cancelChecker?.call() ?? false;
@@ -28,18 +28,13 @@ class LocalSyncService {
final Logger _log = Logger("DeviceSyncService");
LocalSyncService({
required DriftLocalAlbumRepository localAlbumRepository,
required DriftLocalAssetRepository localAssetRepository,
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required LocalFilesManagerRepository localFilesManager,
required StorageRepository storageRepository,
required NativeSyncApi nativeSyncApi,
}) : _localAlbumRepository = localAlbumRepository,
_localAssetRepository = localAssetRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository,
_localFilesManager = localFilesManager,
_storageRepository = storageRepository,
_nativeSyncApi = nativeSyncApi;
required this._localAlbumRepository,
required this._localAssetRepository,
required this._trashedLocalAssetRepository,
required this._localFilesManager,
required this._storageRepository,
required this._nativeSyncApi,
});
Future<void> sync({bool full = false}) async {
final Stopwatch stopwatch = Stopwatch()..start();
+1 -1
View File
@@ -10,7 +10,7 @@ typedef MapQuery = ({MapMarkerSource markerSource});
class MapFactory {
final DriftMapRepository _mapRepository;
const MapFactory({required DriftMapRepository mapRepository}) : _mapRepository = mapRepository;
const MapFactory({required this._mapRepository});
MapService remote(List<String> ownerIds, TimelineMapOptions options) =>
MapService(_mapRepository.remote(ownerIds, options));
@@ -9,7 +9,7 @@ final AppSetting = SettingsService(storeService: StoreService.I);
class SettingsService {
final StoreService _storeService;
const SettingsService({required StoreService storeService}) : _storeService = storeService;
const SettingsService({required this._storeService});
T get<T>(Setting<T> setting) => _storeService.get(setting.storeKey, setting.defaultValue);
@@ -41,24 +41,16 @@ class SyncStreamService {
final bool Function()? _cancelChecker;
SyncStreamService({
required SyncApiRepository syncApiRepository,
required SyncStreamRepository syncStreamRepository,
required DriftLocalAssetRepository localAssetRepository,
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required LocalFilesManagerRepository localFilesManager,
required StorageRepository storageRepository,
required SyncMigrationRepository syncMigrationRepository,
required ApiService api,
bool Function()? cancelChecker,
}) : _syncApiRepository = syncApiRepository,
_syncStreamRepository = syncStreamRepository,
_localAssetRepository = localAssetRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository,
_localFilesManager = localFilesManager,
_storageRepository = storageRepository,
_syncMigrationRepository = syncMigrationRepository,
_api = api,
_cancelChecker = cancelChecker;
required this._syncApiRepository,
required this._syncStreamRepository,
required this._localAssetRepository,
required this._trashedLocalAssetRepository,
required this._localFilesManager,
required this._storageRepository,
required this._syncMigrationRepository,
required this._api,
this._cancelChecker,
});
bool get isCancelled => _cancelChecker?.call() ?? false;
@@ -41,11 +41,7 @@ class TimelineFactory {
final DriftTimelineRepository _timelineRepository;
final MetadataRepository _metadataRepository;
const TimelineFactory({
required DriftTimelineRepository timelineRepository,
required MetadataRepository metadataRepository,
}) : _timelineRepository = timelineRepository,
_metadataRepository = metadataRepository;
const TimelineFactory({required this._timelineRepository, required this._metadataRepository});
GroupAssetsBy get groupBy {
final group = _metadataRepository.appConfig.timeline.groupAssetsBy;
@@ -108,12 +104,7 @@ class TimelineService {
TimelineService(TimelineQuery query)
: this._(assetSource: query.assetSource, bucketSource: query.bucketSource, origin: query.origin);
TimelineService._({
required TimelineAssetSource assetSource,
required TimelineBucketSource bucketSource,
required this.origin,
}) : _assetSource = assetSource,
_bucketSource = bucketSource {
TimelineService._({required this._assetSource, required this._bucketSource, required this.origin}) {
_bucketSubscription = _bucketSource().listen((buckets) {
_mutex.run(() async {
final totalAssets = buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
+1 -3
View File
@@ -12,9 +12,7 @@ class UserService {
final UserApiRepository _userApiRepository;
final StoreService _storeService;
UserService({required UserApiRepository userApiRepository, required StoreService storeService})
: _userApiRepository = userApiRepository,
_storeService = storeService;
UserService({required this._userApiRepository, required this._storeService});
UserDto getMyUser() {
return _storeService.get(StoreKey.currentUser);
+5 -5
View File
@@ -18,11 +18,11 @@ extension DTOToAsset on api.AssetResponseDto {
height: height?.toInt(),
width: width?.toInt(),
isFavorite: isFavorite,
livePhotoVideoId: livePhotoVideoId.orElse(null),
livePhotoVideoId: livePhotoVideoId,
thumbHash: thumbhash,
localId: null,
type: type.toAssetType(),
stackId: stack.orElse(null)?.id,
stackId: stack?.id,
isEdited: isEdited,
);
}
@@ -41,13 +41,13 @@ extension DTOToAsset on api.AssetResponseDto {
height: height?.toInt(),
width: width?.toInt(),
isFavorite: isFavorite,
livePhotoVideoId: livePhotoVideoId.orElse(null),
livePhotoVideoId: livePhotoVideoId,
thumbHash: thumbhash,
localId: null,
type: type.toAssetType(),
stackId: stack.orElse(null)?.id,
stackId: stack?.id,
isEdited: isEdited,
exifInfo: exifInfo.orElse(null) != null ? ExifDtoConverter.fromDto(exifInfo.orElse(null)!) : const ExifInfo(),
exifInfo: exifInfo != null ? ExifDtoConverter.fromDto(exifInfo!) : const ExifInfo(),
);
}
}
@@ -4,6 +4,8 @@ extension StringExtension on String {
String capitalize() {
return split(" ").map((str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1)).join(" ");
}
String? get nullIfEmpty => isEmpty ? null : this;
}
extension DurationExtension on String {
@@ -2,6 +2,7 @@ import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/config/system_config.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
@@ -146,9 +147,31 @@ extension<T extends Object> on MetadataDomain<T> {
look: repo._read(.slideshowLook),
direction: repo._read(.slideshowDirection),
),
album: .new(
sortMode: repo._read(.albumSortMode),
isReverse: repo._read(.albumIsReverse),
isGrid: repo._read(.albumIsGrid),
),
backup: .new(
enabled: repo._read(.backupEnabled),
useCellularForVideos: repo._read(.backupUseCellularForVideos),
useCellularForPhotos: repo._read(.backupUseCellularForPhotos),
requireCharging: repo._read(.backupRequireCharging),
triggerDelay: repo._read(.backupTriggerDelay),
syncAlbums: repo._read(.backupSyncAlbums),
),
);
case .systemConfig:
repo._systemConfig = .new(logLevel: repo._read(.logLevel));
repo._systemConfig = .new(
logLevel: repo._read(.logLevel),
network: .new(
autoEndpointSwitching: repo._read(.networkAutoEndpointSwitching),
preferredWifiName: repo._read(.networkPreferredWifiName).nullIfEmpty,
localEndpoint: repo._read(.networkLocalEndpoint).nullIfEmpty,
externalEndpointList: repo._read(.networkExternalEndpointList),
customHeaders: repo._read(.networkCustomHeaders),
),
);
}
}
}
@@ -20,64 +20,50 @@ class SearchApiRepository extends ApiRepository {
(filter.assetId != null && filter.assetId!.isNotEmpty)) {
return _api.searchSmart(
SmartSearchDto(
query: filter.context == null ? const Optional.absent() : Optional.present(filter.context!),
queryAssetId: filter.assetId == null ? const Optional.absent() : Optional.present(filter.assetId!),
language: filter.language == null ? const Optional.absent() : Optional.present(filter.language!),
country: filter.location.country == null
? const Optional.absent()
: Optional.present(filter.location.country!),
state: filter.location.state == null ? const Optional.absent() : Optional.present(filter.location.state!),
city: filter.location.city == null ? const Optional.absent() : Optional.present(filter.location.city!),
make: filter.camera.make == null ? const Optional.absent() : Optional.present(filter.camera.make!),
model: filter.camera.model == null ? const Optional.absent() : Optional.present(filter.camera.model!),
takenAfter: filter.date.takenAfter == null
? const Optional.absent()
: Optional.present(filter.date.takenAfter!),
takenBefore: filter.date.takenBefore == null
? const Optional.absent()
: Optional.present(filter.date.takenBefore!),
visibility: Optional.present(filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline),
rating: filter.rating.rating == null ? const Optional.absent() : Optional.present(filter.rating.rating!),
isFavorite: filter.display.isFavorite ? const Optional.present(true) : const Optional.absent(),
isNotInAlbum: filter.display.isNotInAlbum ? const Optional.present(true) : const Optional.absent(),
personIds: Optional.present(filter.people.map((e) => e.id).toList()),
tagIds: filter.tagIds == null ? const Optional.absent() : Optional.present(filter.tagIds!),
type: type == null ? const Optional.absent() : Optional.present(type),
page: Optional.present(page),
size: const Optional.present(100),
query: filter.context,
queryAssetId: filter.assetId,
language: filter.language,
country: filter.location.country,
state: filter.location.state,
city: filter.location.city,
make: filter.camera.make,
model: filter.camera.model,
takenAfter: filter.date.takenAfter,
takenBefore: filter.date.takenBefore,
visibility: filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline,
rating: filter.rating.rating,
isFavorite: filter.display.isFavorite ? true : null,
isNotInAlbum: filter.display.isNotInAlbum ? true : null,
personIds: filter.people.map((e) => e.id).toList(),
tagIds: filter.tagIds,
type: type,
page: page,
size: 100,
),
);
}
return _api.searchAssets(
MetadataSearchDto(
originalFileName: filter.filename != null && filter.filename!.isNotEmpty
? Optional.present(filter.filename!)
: const Optional.absent(),
country: filter.location.country == null ? const Optional.absent() : Optional.present(filter.location.country!),
description: filter.description != null && filter.description!.isNotEmpty
? Optional.present(filter.description!)
: const Optional.absent(),
ocr: filter.ocr != null && filter.ocr!.isNotEmpty ? Optional.present(filter.ocr!) : const Optional.absent(),
state: filter.location.state == null ? const Optional.absent() : Optional.present(filter.location.state!),
city: filter.location.city == null ? const Optional.absent() : Optional.present(filter.location.city!),
make: filter.camera.make == null ? const Optional.absent() : Optional.present(filter.camera.make!),
model: filter.camera.model == null ? const Optional.absent() : Optional.present(filter.camera.model!),
takenAfter: filter.date.takenAfter == null
? const Optional.absent()
: Optional.present(filter.date.takenAfter!),
takenBefore: filter.date.takenBefore == null
? const Optional.absent()
: Optional.present(filter.date.takenBefore!),
visibility: Optional.present(filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline),
rating: filter.rating.rating == null ? const Optional.absent() : Optional.present(filter.rating.rating!),
isFavorite: filter.display.isFavorite ? const Optional.present(true) : const Optional.absent(),
isNotInAlbum: filter.display.isNotInAlbum ? const Optional.present(true) : const Optional.absent(),
personIds: Optional.present(filter.people.map((e) => e.id).toList()),
tagIds: filter.tagIds == null ? const Optional.absent() : Optional.present(filter.tagIds!),
type: type == null ? const Optional.absent() : Optional.present(type),
page: Optional.present(page),
size: const Optional.present(1000),
originalFileName: filter.filename != null && filter.filename!.isNotEmpty ? filter.filename : null,
country: filter.location.country,
description: filter.description != null && filter.description!.isNotEmpty ? filter.description : null,
ocr: filter.ocr != null && filter.ocr!.isNotEmpty ? filter.ocr : null,
state: filter.location.state,
city: filter.location.city,
make: filter.camera.make,
model: filter.camera.model,
takenAfter: filter.date.takenAfter,
takenBefore: filter.date.takenBefore,
visibility: filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline,
rating: filter.rating.rating,
isFavorite: filter.display.isFavorite ? true : null,
isNotInAlbum: filter.display.isNotInAlbum ? true : null,
personIds: filter.people.map((e) => e.id).toList(),
tagIds: filter.tagIds,
type: type,
page: page,
size: 1000,
),
);
}
@@ -20,7 +20,7 @@ class SyncApiRepository {
}
Future<void> deleteSyncAck(List<SyncEntityType> types) {
return _api.syncApi.deleteSyncAck(SyncAckDeleteDto(types: Optional.present(types)));
return _api.syncApi.deleteSyncAck(SyncAckDeleteDto(types: types));
}
Future<void> streamChanges(
@@ -91,7 +91,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
email: Value(user.email),
hasProfileImage: Value(user.hasProfileImage),
profileChangedAt: Value(user.profileChangedAt),
avatarColor: Value(user.avatarColor.orElse(null)?.toAvatarColor() ?? AvatarColor.primary),
avatarColor: Value(user.avatarColor?.toAvatarColor() ?? AvatarColor.primary),
isAdmin: Value(user.isAdmin),
pinCode: Value(user.pinCode),
quotaSizeInBytes: Value(user.quotaSizeInBytes ?? 0),
@@ -133,7 +133,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
email: Value(user.email),
hasProfileImage: Value(user.hasProfileImage),
profileChangedAt: Value(user.profileChangedAt),
avatarColor: Value(user.avatarColor.orElse(null)?.toAvatarColor() ?? AvatarColor.primary),
avatarColor: Value(user.avatarColor?.toAvatarColor() ?? AvatarColor.primary),
);
batch.insert(_db.userEntity, companion.copyWith(id: Value(user.id)), onConflict: DoUpdate((_) => companion));
@@ -5,24 +5,24 @@ import 'package:openapi/api.dart';
abstract final class ExifDtoConverter {
static ExifInfo fromDto(ExifResponseDto dto) {
return ExifInfo(
fileSize: dto.fileSizeInByte.orElse(null),
description: dto.description.orElse(null),
orientation: dto.orientation.orElse(null),
timeZone: dto.timeZone.orElse(null),
dateTimeOriginal: dto.dateTimeOriginal.orElse(null),
isFlipped: isOrientationFlipped(dto.orientation.orElse(null)),
latitude: dto.latitude.orElse(null)?.toDouble(),
longitude: dto.longitude.orElse(null)?.toDouble(),
city: dto.city.orElse(null),
state: dto.state.orElse(null),
country: dto.country.orElse(null),
make: dto.make.orElse(null),
model: dto.model.orElse(null),
lens: dto.lensModel.orElse(null),
f: dto.fNumber.orElse(null)?.toDouble(),
mm: dto.focalLength.orElse(null)?.toDouble(),
iso: dto.iso.orElse(null)?.toInt(),
exposureSeconds: exposureTimeToSeconds(dto.exposureTime.orElse(null)),
fileSize: dto.fileSizeInByte,
description: dto.description,
orientation: dto.orientation,
timeZone: dto.timeZone,
dateTimeOriginal: dto.dateTimeOriginal,
isFlipped: isOrientationFlipped(dto.orientation),
latitude: dto.latitude?.toDouble(),
longitude: dto.longitude?.toDouble(),
city: dto.city,
state: dto.state,
country: dto.country,
make: dto.make,
model: dto.model,
lens: dto.lensModel,
f: dto.fNumber?.toDouble(),
mm: dto.focalLength?.toDouble(),
iso: dto.iso?.toInt(),
exposureSeconds: exposureTimeToSeconds(dto.exposureTime),
);
}
@@ -40,7 +40,7 @@ abstract final class UserConverter {
updatedAt: DateTime.now(),
avatarColor: dto.avatarColor.toAvatarColor(),
memoryEnabled: false,
inTimeline: dto.inTimeline.orElse(null) ?? false,
inTimeline: dto.inTimeline ?? false,
isPartnerSharedBy: false,
isPartnerSharedWith: false,
profileChangedAt: dto.profileChangedAt,
@@ -73,10 +73,10 @@ class SharedLink {
slug = dto.slug,
type = dto.type == SharedLinkType.ALBUM ? SharedLinkSource.album : SharedLinkSource.individual,
title = dto.type == SharedLinkType.ALBUM
? dto.album.orElse(null)?.albumName.toUpperCase() ?? "UNKNOWN SHARE"
? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE"
: "INDIVIDUAL SHARE",
thumbAssetId = dto.type == SharedLinkType.ALBUM
? dto.album.orElse(null)?.albumThumbnailAssetId
? dto.album?.albumThumbnailAssetId
: dto.assets.isNotEmpty
? dto.assets[0].id
: null;
@@ -8,13 +8,13 @@ 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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.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/drift_backup.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/user.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
import 'package:immich_mobile/widgets/common/search_field.dart';
import 'package:logging/logging.dart';
@@ -43,7 +43,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
_searchController = TextEditingController();
_searchFocusNode = FocusNode();
_enableSyncUploadAlbum.value = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
_enableSyncUploadAlbum.value = ref.read(metadataProvider).appConfig.backup.syncAlbums;
ref.read(backupAlbumProvider.notifier).getAll();
_initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
@@ -55,7 +55,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
return;
}
final enableSyncUploadAlbum = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
final enableSyncUploadAlbum = ref.read(metadataProvider).appConfig.backup.syncAlbums;
final selectedAlbums = ref
.read(backupAlbumProvider)
.where((a) => a.backupSelection == BackupSelection.selected)
@@ -103,7 +103,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
return;
}
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled;
await ref.read(driftBackupProvider.notifier).getBackupStatus(user.id);
final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
final totalChanged = currentTotalAssetCount != _initialTotalAssetCount;
@@ -3,14 +3,12 @@ import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.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/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
import 'package:logging/logging.dart';
@@ -21,18 +19,20 @@ class DriftBackupOptionsPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
bool hasPopped = false;
final previousWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false;
final previousWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false;
final previousBackup = ref.read(metadataProvider).appConfig.backup;
final previousCellularForVideos = previousBackup.useCellularForVideos;
final previousCellularForPhotos = previousBackup.useCellularForPhotos;
return PopScope(
onPopInvokedWithResult: (didPop, result) async {
// There is an issue with Flutter where the pop event
// can be triggered multiple times, so we guard it with _hasPopped
final currentWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false;
final currentWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false;
final currentBackup = ref.read(metadataProvider).appConfig.backup;
final currentCellularForVideos = currentBackup.useCellularForVideos;
final currentCellularForPhotos = currentBackup.useCellularForPhotos;
if (currentWifiReqForVideos == previousWifiReqForVideos &&
currentWifiReqForPhotos == previousWifiReqForPhotos) {
if (currentCellularForVideos == previousCellularForVideos &&
currentCellularForPhotos == previousCellularForPhotos) {
return;
}
@@ -45,7 +45,7 @@ class DriftBackupOptionsPage extends ConsumerWidget {
}
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled;
if (!isBackupEnabled) {
return;
}
@@ -1,14 +1,12 @@
import 'dart:convert';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
class SettingsHeader {
String key = "";
@@ -24,17 +22,14 @@ class HeaderSettingsPage extends HookConsumerWidget {
final headers = useState<List<SettingsHeader>>([]);
final setInitialHeaders = useState(false);
var headersStr = Store.get(StoreKey.customHeaders, "");
final storedHeaders = ref.read(metadataProvider).systemConfig.network.customHeaders;
if (!setInitialHeaders.value) {
if (headersStr.isNotEmpty) {
var customHeaders = jsonDecode(headersStr) as Map;
customHeaders.forEach((k, v) {
final header = SettingsHeader();
header.key = k;
header.value = v;
headers.value.add(header);
});
}
storedHeaders.forEach((k, v) {
final header = SettingsHeader();
header.key = k;
header.value = v;
headers.value.add(header);
});
// add first one to help the user
if (headers.value.isEmpty) {
@@ -88,8 +83,8 @@ class HeaderSettingsPage extends HookConsumerWidget {
}
saveHeaders(WidgetRef ref, List<SettingsHeader> headers) async {
final headersMap = {};
for (var header in headers) {
final headersMap = <String, String>{};
for (final header in headers) {
final key = header.key.trim();
final value = header.value.trim();
@@ -99,8 +94,7 @@ class HeaderSettingsPage extends HookConsumerWidget {
headersMap[key] = value;
}
var encoded = jsonEncode(headersMap);
await Store.put(StoreKey.customHeaders, encoded);
await ref.read(metadataProvider).write(MetadataKey.networkCustomHeaders, headersMap);
await ref.read(apiServiceProvider).updateHeaders();
}
}
@@ -12,6 +12,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
@@ -340,7 +341,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
await backgroundManager.hashAssets();
}
if (Store.get(StoreKey.syncAlbums, false)) {
if (MetadataRepository.instance.appConfig.backup.syncAlbums) {
await backgroundManager.syncLinkedAlbum();
}
} catch (e) {
@@ -369,7 +370,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
}
Future<void> _resumeBackup(DriftBackupNotifier notifier) async {
final isEnableBackup = Store.get(StoreKey.enableBackup, false);
final isEnableBackup = MetadataRepository.instance.appConfig.backup.enabled;
if (isEnableBackup) {
final currentUser = Store.tryGet(StoreKey.currentUser);
@@ -15,15 +15,15 @@ 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/new_album_name_modal.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/app_settings.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/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/album_filter.utils.dart';
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -58,19 +58,11 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final appSettings = ref.read(appSettingsServiceProvider);
final savedSortMode = appSettings.getSetting(AppSettingsEnum.selectedAlbumSortOrder);
final savedIsReverse = appSettings.getSetting(AppSettingsEnum.selectedAlbumSortReverse);
final savedIsGrid = appSettings.getSetting(AppSettingsEnum.albumGridView);
final albumSortMode = AlbumSortMode.values.firstWhere(
(e) => e.storeIndex == savedSortMode,
orElse: () => AlbumSortMode.lastModified,
);
final albumConfig = ref.read(metadataProvider).appConfig.album;
setState(() {
sort = AlbumSort(mode: albumSortMode, isReverse: savedIsReverse);
isGrid = savedIsGrid;
sort = AlbumSort(mode: albumConfig.sortMode, isReverse: albumConfig.isReverse);
isGrid = albumConfig.isGrid;
});
ref.read(remoteAlbumProvider.notifier).refresh();
@@ -102,7 +94,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
setState(() {
isGrid = !isGrid;
});
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.albumGridView, isGrid);
ref.read(metadataProvider).write(MetadataKey.albumIsGrid, isGrid);
}
void changeFilter(QuickFilterMode mode) {
@@ -118,9 +110,9 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
this.sort = sort;
});
final appSettings = ref.read(appSettingsServiceProvider);
await appSettings.setSetting(AppSettingsEnum.selectedAlbumSortOrder, sort.mode.storeIndex);
await appSettings.setSetting(AppSettingsEnum.selectedAlbumSortReverse, sort.isReverse);
final metadata = ref.read(metadataProvider);
await metadata.write(MetadataKey.albumSortMode, sort.mode);
await metadata.write(MetadataKey.albumIsReverse, sort.isReverse);
await sortAlbums();
}
@@ -1,10 +1,10 @@
import 'package:flutter/material.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/translate_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
class BackupToggleButton extends ConsumerStatefulWidget {
final VoidCallback onStart;
@@ -31,7 +31,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
end: 1,
).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_isEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
_isEnabled = ref.read(metadataProvider).appConfig.backup.enabled;
}
@override
@@ -41,7 +41,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
}
Future<void> _onToggle(bool value) async {
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.enableBackup, value);
await ref.read(metadataProvider).write(MetadataKey.backupEnabled, value);
setState(() {
_isEnabled = value;
@@ -296,16 +296,12 @@ class _ThumbnailRenderBox extends RenderBox {
bool isRepaintBoundary = true;
_ThumbnailRenderBox({
required ui.Image? image,
required ui.Image? previousImage,
required double fadeValue,
required BoxFit fit,
required Gradient placeholderGradient,
}) : _image = image,
_previousImage = previousImage,
_fadeValue = fadeValue,
_fit = fit,
_placeholderGradient = placeholderGradient;
required this._image,
required this._previousImage,
required this._fadeValue,
required this._fit,
required this._placeholderGradient,
});
@override
void paint(PaintingContext context, Offset offset) {
@@ -62,14 +62,11 @@ class RenderFixedRow extends RenderBox
RenderBoxContainerDefaultsMixin<RenderBox, _RowParentData> {
RenderFixedRow({
List<RenderBox>? children,
required double height,
required List<double> widths,
required double spacing,
required TextDirection textDirection,
}) : _height = height,
_widths = widths,
_spacing = spacing,
_textDirection = textDirection {
required this._height,
required this._widths,
required this._spacing,
required this._textDirection,
}) {
addAll(children);
}
@@ -578,9 +578,7 @@ class _SlideFadeTransition extends StatelessWidget {
final Animation<double> _animation;
final Widget _child;
const _SlideFadeTransition({required Animation<double> animation, required Widget child})
: _animation = animation,
_child = child;
const _SlideFadeTransition({required this._animation, required this._child});
@override
Widget build(BuildContext context) {
@@ -397,7 +397,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
final grid = CustomScrollView(
primary: true,
physics: _scrollPhysics,
cacheExtent: maxHeight * 2,
scrollCacheExtent: .pixels(maxHeight * 2),
slivers: [
if (isSelectionMode) const SelectionSliverAppBar() else if (widget.appBar != null) widget.appBar!,
if (widget.topSliverWidget != null) widget.topSliverWidget!,
@@ -503,7 +503,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
class _SliverSegmentedList extends SliverMultiBoxAdaptorWidget {
final List<Segment> _segments;
const _SliverSegmentedList({required List<Segment> segments, required super.delegate}) : _segments = segments;
const _SliverSegmentedList({required this._segments, required super.delegate});
@override
_RenderSliverTimelineBoxAdaptor createRenderObject(BuildContext context) =>
@@ -527,8 +527,7 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
markNeedsLayout();
}
_RenderSliverTimelineBoxAdaptor({required super.childManager, required List<Segment> segments})
: _segments = segments;
_RenderSliverTimelineBoxAdaptor({required super.childManager, required this._segments});
int getMinChildIndexForScrollOffset(double offset) =>
_segments.findByOffset(offset)?.getMinChildIndexForScrollOffset(offset) ?? 0;
@@ -5,16 +5,15 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.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/notification_permission.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:logging/logging.dart';
enum AppLifeCycleEnum { active, inactive, paused, resumed, detached, hidden }
@@ -108,7 +107,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
await Future.delayed(const Duration(milliseconds: 500));
final backgroundManager = _ref.read(backgroundSyncProvider);
final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
final isAlbumLinkedSyncEnable = _ref.read(metadataProvider).appConfig.backup.syncAlbums;
try {
bool syncSuccess = false;
@@ -138,7 +137,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
}
Future<void> _resumeBackup() async {
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
final isEnableBackup = _ref.read(metadataProvider).appConfig.backup.enabled;
if (isEnableBackup) {
final currentUser = Store.tryGet(StoreKey.currentUser);
+10 -5
View File
@@ -1,6 +1,9 @@
import 'dart:convert';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
@@ -8,6 +11,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/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/auth.service.dart';
@@ -126,7 +130,8 @@ class AuthNotifier extends StateNotifier<AuthState> {
await _apiService.updateHeaders();
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final customHeaders = Store.tryGet(StoreKey.customHeaders);
final headerMap = _ref.read(metadataProvider).systemConfig.network.customHeaders;
final customHeaders = headerMap.isEmpty ? null : jsonEncode(headerMap);
await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders);
// Get the deviceid from the store if it exists, otherwise generate a new one
@@ -174,19 +179,19 @@ class AuthNotifier extends StateNotifier<AuthState> {
}
Future<void> saveWifiName(String wifiName) async {
await Store.put(StoreKey.preferredWifiName, wifiName);
await _ref.read(metadataProvider).write(MetadataKey.networkPreferredWifiName, wifiName);
}
Future<void> saveLocalEndpoint(String url) async {
await Store.put(StoreKey.localEndpoint, url);
await _ref.read(metadataProvider).write(MetadataKey.networkLocalEndpoint, url);
}
String? getSavedWifiName() {
return Store.tryGet(StoreKey.preferredWifiName);
return _ref.read(metadataProvider).systemConfig.network.preferredWifiName;
}
String? getSavedLocalEndpoint() {
return Store.tryGet(StoreKey.localEndpoint);
return _ref.read(metadataProvider).systemConfig.network.localEndpoint;
}
/// Returns the current server endpoint (with /api) URL from the store
@@ -29,7 +29,7 @@ final getAllPlacesProvider = FutureProvider.autoDispose<List<SearchCuratedConten
}
final curatedContent = assetPlaces
.map((data) => SearchCuratedContent(label: data.exifInfo.orElse(null)!.city.orElse(null)!, id: data.id))
.map((data) => SearchCuratedContent(label: data.exifInfo!.city!, id: data.id))
.toList();
return curatedContent;
+3 -2
View File
@@ -7,6 +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/providers/auth.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/server_info.provider.dart';
import 'package:immich_mobile/utils/debounce.dart';
import 'package:immich_mobile/utils/debug_print.dart';
@@ -192,7 +193,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
return;
}
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums;
try {
unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) {
@@ -213,7 +214,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
return;
}
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums;
try {
unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) {
@@ -23,8 +23,8 @@ class ActivityApiRepository extends ApiRepository {
final dto = ActivityCreateDto(
albumId: albumId,
type: type == ActivityType.comment ? ReactionType.comment : ReactionType.like,
assetId: assetId == null ? const Optional.absent() : Optional.present(assetId),
comment: comment == null ? const Optional.absent() : Optional.present(comment),
assetId: assetId,
comment: comment,
);
final response = await checkNull(_api.createActivity(dto));
return _toActivity(response);
@@ -45,6 +45,6 @@ class ActivityApiRepository extends ApiRepository {
type: dto.type == ReactionType.comment ? ActivityType.comment : ActivityType.like,
user: UserConverter.fromSimpleUserDto(dto.user),
assetId: dto.assetId,
comment: dto.comment.orElse(null),
comment: dto.comment,
);
}
@@ -24,7 +24,7 @@ class AssetApiRepository extends ApiRepository {
AssetApiRepository(this._api, this._stacksApi, this._trashApi);
Future<void> delete(List<String> ids, bool force) async {
return _api.deleteAssets(AssetBulkDeleteDto(ids: ids, force: Optional.present(force)));
return _api.deleteAssets(AssetBulkDeleteDto(ids: ids, force: force));
}
Future<void> restoreTrash(List<String> ids) async {
@@ -42,27 +42,19 @@ class AssetApiRepository extends ApiRepository {
}
Future<void> updateVisibility(List<String> ids, AssetVisibilityEnum visibility) async {
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, visibility: Optional.present(_mapVisibility(visibility))));
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, visibility: _mapVisibility(visibility)));
}
Future<void> updateFavorite(List<String> ids, bool isFavorite) async {
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: Optional.present(isFavorite)));
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: isFavorite));
}
Future<void> updateLocation(List<String> ids, LatLng location) async {
return _api.updateAssets(
AssetBulkUpdateDto(
ids: ids,
latitude: Optional.present(location.latitude),
longitude: Optional.present(location.longitude),
),
);
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, latitude: location.latitude, longitude: location.longitude));
}
Future<void> updateDateTime(List<String> ids, DateTime dateTime) async {
return _api.updateAssets(
AssetBulkUpdateDto(ids: ids, dateTimeOriginal: Optional.present(dateTime.toIso8601String())),
);
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, dateTimeOriginal: dateTime.toIso8601String()));
}
Future<StackResponse> stack(List<String> ids) async {
@@ -90,15 +82,15 @@ class AssetApiRepository extends ApiRepository {
final response = await checkNull(_api.getAssetInfo(assetId));
// we need to get the MIME of the thumbnail once that gets added to the API
return response.originalMimeType.orElse(null);
return response.originalMimeType;
}
Future<void> updateDescription(String assetId, String description) {
return _api.updateAsset(assetId, UpdateAssetDto(description: Optional.present(description)));
return _api.updateAsset(assetId, UpdateAssetDto(description: description));
}
Future<void> updateRating(String assetId, int rating) {
return _api.updateAsset(assetId, UpdateAssetDto(rating: Optional.present(rating)));
return _api.updateAsset(assetId, UpdateAssetDto(rating: rating));
}
Future<AssetEditsResponseDto?> editAsset(String assetId, List<AssetEdit> edits) {
+13 -19
View File
@@ -1,46 +1,40 @@
import 'dart:convert';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.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/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
final authRepositoryProvider = Provider<AuthRepository>((ref) => AuthRepository(ref.watch(driftProvider)));
final authRepositoryProvider = Provider<AuthRepository>(
(ref) => AuthRepository(ref.watch(driftProvider), ref.watch(metadataProvider)),
);
class AuthRepository {
final Drift _drift;
final MetadataRepository _metadata;
const AuthRepository(this._drift);
const AuthRepository(this._drift, this._metadata);
Future<void> clearLocalData() async {
await SyncStreamRepository(_drift).reset();
}
bool getEndpointSwitchingFeature() {
return Store.tryGet(StoreKey.autoEndpointSwitching) ?? false;
return _metadata.systemConfig.network.autoEndpointSwitching;
}
String? getPreferredWifiName() {
return Store.tryGet(StoreKey.preferredWifiName);
return _metadata.systemConfig.network.preferredWifiName;
}
String? getLocalEndpoint() {
return Store.tryGet(StoreKey.localEndpoint);
return _metadata.systemConfig.network.localEndpoint;
}
List<AuxilaryEndpoint> getExternalEndpointList() {
final jsonString = Store.tryGet(StoreKey.externalEndpointList);
if (jsonString == null) {
return [];
}
final List<dynamic> jsonList = jsonDecode(jsonString);
final endpointList = jsonList.map((e) => AuxilaryEndpoint.fromJson(e)).toList();
return endpointList;
return _metadata.systemConfig.network.externalEndpointList
.map((url) => AuxilaryEndpoint(url: url, status: .valid))
.toList();
}
}
@@ -13,7 +13,7 @@ class AuthApiRepository extends ApiRepository {
AuthApiRepository(this._apiService);
Future<void> changePassword(String newPassword) async {
await _apiService.usersApi.updateMyUser(UserUpdateMeDto(password: Optional.present(newPassword)));
await _apiService.usersApi.updateMyUser(UserUpdateMeDto(password: newPassword));
}
Future<LoginResponse> login(String email, String password) async {
@@ -46,7 +46,7 @@ class AuthApiRepository extends ApiRepository {
Future<bool> unlockPinCode(String pinCode) async {
try {
await _apiService.authenticationApi.unlockAuthSession(SessionUnlockDto(pinCode: Optional.present(pinCode)));
await _apiService.authenticationApi.unlockAuthSession(SessionUnlockDto(pinCode: pinCode));
return true;
} catch (_) {
return false;
@@ -22,13 +22,7 @@ class DriftAlbumApiRepository extends ApiRepository {
String? description,
}) async {
final responseDto = await checkNull(
_api.createAlbum(
CreateAlbumDto(
albumName: name,
description: description == null ? const Optional.absent() : Optional.present(description),
assetIds: Optional.present(assetIds.toList()),
),
),
_api.createAlbum(CreateAlbumDto(albumName: name, description: description, assetIds: assetIds.toList())),
);
return responseDto.toRemoteAlbum(owner);
@@ -79,13 +73,11 @@ class DriftAlbumApiRepository extends ApiRepository {
_api.updateAlbumInfo(
albumId,
UpdateAlbumDto(
albumName: name == null ? const Optional.absent() : Optional.present(name),
description: description == null ? const Optional.absent() : Optional.present(description),
albumThumbnailAssetId: thumbnailAssetId == null
? const Optional.absent()
: Optional.present(thumbnailAssetId),
isActivityEnabled: isActivityEnabled == null ? const Optional.absent() : Optional.present(isActivityEnabled),
order: apiOrder == null ? const Optional.absent() : Optional.present(apiOrder),
albumName: name,
description: description,
albumThumbnailAssetId: thumbnailAssetId,
isActivityEnabled: isActivityEnabled,
order: apiOrder,
),
),
);
@@ -107,9 +99,7 @@ class DriftAlbumApiRepository extends ApiRepository {
}
Future<bool> setActivityStatus(String albumId, bool isEnabled) async {
final response = await checkNull(
_api.updateAlbumInfo(albumId, UpdateAlbumDto(isActivityEnabled: Optional.present(isEnabled))),
);
final response = await checkNull(_api.updateAlbumInfo(albumId, UpdateAlbumDto(isActivityEnabled: isEnabled)));
return response.isActivityEnabled;
}
}
@@ -126,7 +116,7 @@ extension on AlbumResponseDto {
updatedAt: updatedAt,
thumbnailAssetId: albumThumbnailAssetId,
isActivityEnabled: isActivityEnabled,
order: order.orElse(null) == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc,
order: order == AssetOrder.asc ? AlbumAssetOrder.asc : AlbumAssetOrder.desc,
assetCount: assetCount,
isShared: albumUsers.length > 2,
);
@@ -16,7 +16,7 @@ class PartnerApiRepository extends ApiRepository {
Future<List<UserDto>> getAll(Direction direction) async {
final response = await checkNull(
_api.getPartners(direction == Direction.sharedByMe ? PartnerDirection.sharedBy : PartnerDirection.sharedWith),
_api.getPartners(direction == Direction.sharedByMe ? PartnerDirection.by : PartnerDirection.with_),
);
return response.map(UserConverter.fromPartnerDto).toList();
}
@@ -18,10 +18,7 @@ class PersonApiRepository extends ApiRepository {
Future<PersonDto> update(String id, {String? name, DateTime? birthday}) async {
final birthdayUtc = birthday == null ? null : DateTime.utc(birthday.year, birthday.month, birthday.day);
final dto = PersonUpdateDto(
name: name == null ? const Optional.absent() : Optional.present(name),
birthDate: birthdayUtc == null ? const Optional.absent() : Optional.present(birthdayUtc),
);
final dto = PersonUpdateDto(name: name, birthDate: birthdayUtc);
final response = await checkNull(_api.updatePerson(id, dto));
return _toPerson(response);
}
@@ -15,13 +15,7 @@ class SessionsAPIRepository extends ApiRepository {
Future<SessionCreateResponse> createSession(String deviceType, String deviceOS, {int? duration}) async {
final dto = await checkNull(
_api.createSession(
SessionCreateDto(
deviceType: Optional.present(deviceType),
deviceOS: Optional.present(deviceOS),
duration: duration == null ? const Optional.absent() : Optional.present(duration),
),
),
_api.createSession(SessionCreateDto(deviceType: deviceType, deviceOS: deviceOS, duration: duration)),
);
return SessionCreateResponse(
@@ -29,7 +23,7 @@ class SessionsAPIRepository extends ApiRepository {
current: dto.current,
deviceType: deviceType,
deviceOS: deviceOS,
expiresAt: dto.expiresAt.orElse(null),
expiresAt: dto.expiresAt,
createdAt: dto.createdAt,
updatedAt: dto.updatedAt,
token: dto.token,
+1 -1
View File
@@ -55,7 +55,7 @@ class LockedGuard extends AutoRouteGuard {
return;
}
await _apiService.authenticationApi.unlockAuthSession(SessionUnlockDto(pinCode: Optional.present(securePinCode)));
await _apiService.authenticationApi.unlockAuthSession(SessionUnlockDto(pinCode: securePinCode));
resolver.next(true);
} on PlatformException catch (error) {
+8 -17
View File
@@ -5,8 +5,8 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.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/utils/debug_print.dart';
import 'package:immich_mobile/utils/url_helper.dart';
import 'package:logging/logging.dart';
@@ -177,30 +177,21 @@ class ApiService {
if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
urls.add(serverEndpoint);
}
final localEndpoint = Store.tryGet(StoreKey.localEndpoint);
if (localEndpoint != null && localEndpoint.isNotEmpty) {
final network = MetadataRepository.instance.systemConfig.network;
final localEndpoint = network.localEndpoint;
if (localEndpoint != null) {
urls.add(localEndpoint);
}
final externalJson = Store.tryGet(StoreKey.externalEndpointList);
if (externalJson != null) {
final List<dynamic> list = jsonDecode(externalJson);
for (final entry in list) {
final url = AuxilaryEndpoint.fromJson(entry).url;
if (url.isNotEmpty) {
urls.add(url);
}
for (final url in network.externalEndpointList) {
if (url.isNotEmpty) {
urls.add(url);
}
}
return urls;
}
static Map<String, String> getRequestHeaders() {
var customHeadersStr = Store.get(StoreKey.customHeaders, "");
if (customHeadersStr.isEmpty) {
return const {};
}
return (jsonDecode(customHeadersStr) as Map).cast<String, String>();
return MetadataRepository.instance.systemConfig.network.customHeaders;
}
ApiClient get apiClient => _apiClient;
+1 -11
View File
@@ -2,20 +2,10 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
enum AppSettingsEnum<T> {
selectedAlbumSortOrder<int>(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 2),
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
selectedAlbumSortReverse<bool>(StoreKey.selectedAlbumSortReverse, null, true),
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
syncAlbums<bool>(StoreKey.syncAlbums, null, false),
autoEndpointSwitching<bool>(StoreKey.autoEndpointSwitching, null, false),
enableBackup<bool>(StoreKey.enableBackup, null, false),
useCellularForUploadVideos<bool>(StoreKey.useWifiForUploadVideos, null, false),
useCellularForUploadPhotos<bool>(StoreKey.useWifiForUploadPhotos, null, false),
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false),
albumGridView<bool>(StoreKey.albumGridView, "albumGridView", false),
backupRequireCharging<bool>(StoreKey.backupRequireCharging, null, false),
backupTriggerDelay<int>(StoreKey.backupTriggerDelay, null, 30);
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false);
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
+3 -10
View File
@@ -1,19 +1,19 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.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/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/repositories/auth.repository.dart';
import 'package:immich_mobile/repositories/auth_api.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/services/network.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@@ -25,7 +25,6 @@ final authServiceProvider = Provider(
ref.watch(apiServiceProvider),
ref.watch(networkServiceProvider),
ref.watch(backgroundSyncProvider),
ref.watch(appSettingsServiceProvider),
),
);
@@ -35,7 +34,6 @@ class AuthService {
final ApiService _apiService;
final NetworkService _networkService;
final BackgroundSyncManager _backgroundSyncManager;
final AppSettingsService _appSettingsService;
final _log = Logger("AuthService");
AuthService(
@@ -44,7 +42,6 @@ class AuthService {
this._apiService,
this._networkService,
this._backgroundSyncManager,
this._appSettingsService,
);
/// Validates the provided server URL by resolving and setting the endpoint.
@@ -103,7 +100,7 @@ class AuthService {
_log.severe("Error clearing local data", error, stackTrace);
});
await _appSettingsService.setSetting(AppSettingsEnum.enableBackup, false);
await MetadataRepository.instance.write(MetadataKey.backupEnabled, false);
}
}
@@ -123,10 +120,6 @@ class AuthService {
_authRepository.clearLocalData(),
Store.delete(StoreKey.currentUser),
Store.delete(StoreKey.accessToken),
Store.delete(StoreKey.autoEndpointSwitching),
Store.delete(StoreKey.preferredWifiName),
Store.delete(StoreKey.localEndpoint),
Store.delete(StoreKey.externalEndpointList),
]);
}
@@ -13,14 +13,13 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.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/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/upload.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
@@ -31,7 +30,6 @@ final backgroundUploadServiceProvider = Provider((ref) {
ref.watch(storageRepositoryProvider),
ref.watch(localAssetRepository),
ref.watch(backupRepositoryProvider),
ref.watch(appSettingsServiceProvider),
ref.watch(assetMediaRepositoryProvider),
);
@@ -105,7 +103,6 @@ class BackgroundUploadService {
this._storageRepository,
this._localAssetRepository,
this._backupRepository,
this._appSettingsService,
this._assetMediaRepository,
) {
_uploadRepository.onUploadStatus = _onUploadCallback;
@@ -116,7 +113,6 @@ class BackgroundUploadService {
final StorageRepository _storageRepository;
final DriftLocalAssetRepository _localAssetRepository;
final DriftBackupRepository _backupRepository;
final AppSettingsService _appSettingsService;
final AssetMediaRepository _assetMediaRepository;
final Logger _logger = Logger('BackgroundUploadService');
@@ -363,15 +359,14 @@ class BackgroundUploadService {
}
bool _shouldRequireWiFi(LocalAsset asset) {
bool requiresWiFi = true;
if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) {
requiresWiFi = false;
} else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) {
requiresWiFi = false;
final backup = MetadataRepository.instance.appConfig.backup;
if (asset.isVideo && backup.useCellularForVideos) {
return false;
}
return requiresWiFi;
if (!asset.isVideo && backup.useCellularForPhotos) {
return false;
}
return true;
}
Future<UploadTask> buildUploadTask(
@@ -7,18 +7,17 @@ import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/platform/connectivity_api.g.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/upload.repository.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
@@ -39,7 +38,6 @@ final foregroundUploadServiceProvider = Provider((ref) {
ref.watch(storageRepositoryProvider),
ref.watch(backupRepositoryProvider),
ref.watch(connectivityApiProvider),
ref.watch(appSettingsServiceProvider),
ref.watch(assetMediaRepositoryProvider),
);
});
@@ -55,7 +53,6 @@ class ForegroundUploadService {
this._storageRepository,
this._backupRepository,
this._connectivityApi,
this._appSettingsService,
this._assetMediaRepository,
);
@@ -63,7 +60,6 @@ class ForegroundUploadService {
final StorageRepository _storageRepository;
final DriftBackupRepository _backupRepository;
final ConnectivityApi _connectivityApi;
final AppSettingsService _appSettingsService;
final AssetMediaRepository _assetMediaRepository;
final Logger _logger = Logger('ForegroundUploadService');
@@ -455,14 +451,13 @@ class ForegroundUploadService {
}
bool _shouldRequireWiFi(LocalAsset asset) {
bool requiresWiFi = true;
if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) {
requiresWiFi = false;
} else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) {
requiresWiFi = false;
final backup = MetadataRepository.instance.appConfig.backup;
if (asset.isVideo && backup.useCellularForVideos) {
return false;
}
return requiresWiFi;
if (!asset.isVideo && backup.useCellularForPhotos) {
return false;
}
return true;
}
}
+2 -6
View File
@@ -18,11 +18,7 @@ class OAuthService {
log.info("Starting OAuth flow with redirect URI: $redirectUri");
final dto = await _apiService.oAuthApi.startOAuth(
OAuthConfigDto(
redirectUri: redirectUri,
state: Optional.present(state),
codeChallenge: Optional.present(codeChallenge),
),
OAuthConfigDto(redirectUri: redirectUri, state: state, codeChallenge: codeChallenge),
);
final authUrl = dto?.url;
@@ -41,7 +37,7 @@ class OAuthService {
}
return await _apiService.oAuthApi.finishOAuth(
OAuthCallbackDto(url: result, state: Optional.present(state), codeVerifier: Optional.present(codeVerifier)),
OAuthCallbackDto(url: result, state: state, codeVerifier: codeVerifier),
);
}
}
+24 -24
View File
@@ -48,26 +48,26 @@ class SharedLinkService {
if (type == SharedLinkType.ALBUM) {
dto = SharedLinkCreateDto(
type: type,
albumId: albumId == null ? const Optional.absent() : Optional.present(albumId),
showMetadata: Optional.present(showMeta),
allowDownload: Optional.present(allowDownload),
allowUpload: Optional.present(allowUpload),
expiresAt: expiresAt == null ? const Optional.absent() : Optional.present(expiresAt),
description: description == null ? const Optional.absent() : Optional.present(description),
password: password == null ? const Optional.absent() : Optional.present(password),
slug: slug == null ? const Optional.absent() : Optional.present(slug),
albumId: albumId,
showMetadata: showMeta,
allowDownload: allowDownload,
allowUpload: allowUpload,
expiresAt: expiresAt,
description: description,
password: password,
slug: slug,
);
} else if (assetIds != null) {
dto = SharedLinkCreateDto(
type: type,
showMetadata: Optional.present(showMeta),
allowDownload: Optional.present(allowDownload),
allowUpload: Optional.present(allowUpload),
expiresAt: expiresAt == null ? const Optional.absent() : Optional.present(expiresAt),
description: description == null ? const Optional.absent() : Optional.present(description),
password: password == null ? const Optional.absent() : Optional.present(password),
slug: slug == null ? const Optional.absent() : Optional.present(slug),
assetIds: Optional.present(assetIds),
showMetadata: showMeta,
allowDownload: allowDownload,
allowUpload: allowUpload,
expiresAt: expiresAt,
description: description,
password: password,
slug: slug,
assetIds: assetIds,
);
}
@@ -98,14 +98,14 @@ class SharedLinkService {
final responseDto = await _apiService.sharedLinksApi.updateSharedLink(
id,
SharedLinkEditDto(
showMetadata: showMeta == null ? const Optional.absent() : Optional.present(showMeta),
allowDownload: allowDownload == null ? const Optional.absent() : Optional.present(allowDownload),
allowUpload: allowUpload == null ? const Optional.absent() : Optional.present(allowUpload),
expiresAt: expiresAt == null ? const Optional.absent() : Optional.present(expiresAt),
description: description == null ? const Optional.absent() : Optional.present(description),
password: password == null ? const Optional.absent() : Optional.present(password),
slug: slug == null ? const Optional.absent() : Optional.present(slug),
changeExpiryTime: changeExpiry == null ? const Optional.absent() : Optional.present(changeExpiry),
showMetadata: showMeta,
allowDownload: allowDownload,
allowUpload: allowUpload,
expiresAt: expiresAt,
description: description,
password: password,
slug: slug,
changeExpiryTime: changeExpiry,
),
);
if (responseDto != null) {
+123 -12
View File
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
@@ -12,7 +13,8 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
const int targetVersion = 26;
@@ -37,12 +39,35 @@ Future<void> _migrateTo25() async {
return;
}
final serverUrls = ApiService.getServerUrls();
if (serverUrls.isEmpty) {
final urls = <String>[];
final serverEndpoint = Store.tryGet(StoreKey.serverEndpoint);
if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
urls.add(serverEndpoint);
}
final localEndpoint = Store.tryGet(StoreKey.legacyLocalEndpoint);
if (localEndpoint != null && localEndpoint.isNotEmpty) {
urls.add(localEndpoint);
}
final externalJson = Store.tryGet(StoreKey.legacyExternalEndpointList);
if (externalJson != null) {
final List<dynamic> list = jsonDecode(externalJson);
for (final entry in list) {
final url = AuxilaryEndpoint.fromJson(entry).url;
if (url.isNotEmpty) {
urls.add(url);
}
}
}
if (urls.isEmpty) {
return;
}
await NetworkRepository.setHeaders(ApiService.getRequestHeaders(), serverUrls, token: accessToken);
final customHeadersStr = Store.get(StoreKey.legacyCustomHeaders, "");
final headers = customHeadersStr.isEmpty
? const <String, String>{}
: (jsonDecode(customHeadersStr) as Map).cast<String, String>();
await NetworkRepository.setHeaders(headers, urls, token: accessToken);
}
Future<void> _migrateTo26(Drift drift) async {
@@ -57,14 +82,7 @@ Future<void> _migrateTo26(Drift drift) async {
final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(StoreKey.legacyCleanupKeepAlbumIds.id);
if (cleanupKeepAlbumIds != null) {
final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList();
await drift.metadataEntity.insertOnConflictUpdate(
MetadataEntityCompanion.insert(
key: MetadataKey.cleanupKeepAlbumIds.key,
value: MetadataKey.cleanupKeepAlbumIds.encode(ids),
updatedAt: Value(DateTime.now()),
),
);
await migrator.deleteLegacyStoreRows([StoreKey.legacyCleanupKeepAlbumIds.id]);
migrator.stage(StoreKey.legacyCleanupKeepAlbumIds, MetadataKey.cleanupKeepAlbumIds, ids);
}
await migrator.migrateBool(StoreKey.legacyCleanupKeepFavorites, MetadataKey.cleanupKeepFavorites);
await migrator.migrateEnumIndex(
@@ -96,9 +114,87 @@ Future<void> _migrateTo26(Drift drift) async {
await migrator.migrateBool(StoreKey.legacyLoadOriginalVideo, MetadataKey.viewerLoadOriginalVideo);
await migrator.migrateBool(StoreKey.legacyAutoPlayVideo, MetadataKey.viewerAutoPlayVideo);
await migrator.migrateBool(StoreKey.legacyTapToNavigate, MetadataKey.viewerTapToNavigate);
// Network
await migrator.migrateBool(StoreKey.legacyAutoEndpointSwitching, MetadataKey.networkAutoEndpointSwitching);
await migrator.migrateString(StoreKey.legacyPreferredWifiName, MetadataKey.networkPreferredWifiName);
await migrator.migrateString(StoreKey.legacyLocalEndpoint, MetadataKey.networkLocalEndpoint);
await _migrateExternalEndpointList(migrator);
await _migrateCustomHeaders(migrator);
// Album
await _migrateAlbumSortMode(migrator);
await migrator.migrateBool(StoreKey.legacySelectedAlbumSortReverse, MetadataKey.albumIsReverse);
await migrator.migrateBool(StoreKey.legacyAlbumGridView, MetadataKey.albumIsGrid);
// Backup
await migrator.migrateBool(StoreKey.legacyEnableBackup, MetadataKey.backupEnabled);
await migrator.migrateBool(StoreKey.legacyUseWifiForUploadVideos, MetadataKey.backupUseCellularForVideos);
await migrator.migrateBool(StoreKey.legacyUseWifiForUploadPhotos, MetadataKey.backupUseCellularForPhotos);
await migrator.migrateBool(StoreKey.legacyBackupRequireCharging, MetadataKey.backupRequireCharging);
await migrator.migrateInt(StoreKey.legacyBackupTriggerDelay, MetadataKey.backupTriggerDelay);
await migrator.migrateBool(StoreKey.legacySyncAlbums, MetadataKey.backupSyncAlbums);
await migrator.complete();
}
Future<void> _migrateAlbumSortMode(_StoreMigrator migrator) async {
final raw = await migrator.readLegacyStoreInt(StoreKey.legacySelectedAlbumSortOrder.id);
if (raw == null) {
return;
}
final mode = AlbumSortMode.values.firstWhere(
(e) => e.storeIndex == raw,
orElse: () => MetadataKey.albumSortMode.defaultValue,
);
migrator.stage(StoreKey.legacySelectedAlbumSortOrder, MetadataKey.albumSortMode, mode);
}
Future<void> _migrateExternalEndpointList(_StoreMigrator migrator) async {
final raw = await migrator.readLegacyStoreString(StoreKey.legacyExternalEndpointList.id);
if (raw == null) {
return;
}
final urls = <String>[];
try {
final decoded = jsonDecode(raw);
if (decoded is List) {
for (final entry in decoded) {
final url = AuxilaryEndpoint.fromJson(entry).url;
if (url.isNotEmpty) {
urls.add(url);
}
}
}
} on FormatException {
// ignore invalid entries
}
migrator.stage(StoreKey.legacyExternalEndpointList, MetadataKey.networkExternalEndpointList, urls);
}
Future<void> _migrateCustomHeaders(_StoreMigrator migrator) async {
final raw = await migrator.readLegacyStoreString(StoreKey.legacyCustomHeaders.id);
if (raw == null) {
return;
}
final headers = <String, String>{};
try {
final decoded = jsonDecode(raw);
if (decoded is Map) {
decoded.forEach((key, value) {
if (key is String && value is String) {
headers[key] = value;
}
});
}
} on FormatException {
// ignore invalid entries
}
migrator.stage(StoreKey.legacyCustomHeaders, MetadataKey.networkCustomHeaders, headers);
}
class _StoreMigrator {
final Drift _db;
final Map<MetadataKey<Object>, Object> _cache = {};
@@ -153,6 +249,21 @@ class _StoreMigrator {
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateString(StoreKey<String> legacyKey, MetadataKey<String> newKey) async {
final value = await readLegacyStoreString(legacyKey.id);
if (value == null) {
return;
}
_cache[newKey] = value;
_migratedStoreIds.add(legacyKey.id);
}
void stage<T extends Object>(StoreKey legacyKey, MetadataKey<T> newKey, T value) {
_cache[newKey] = value;
_migratedStoreIds.add(legacyKey.id);
}
Future<void> complete() async {
await _db.batch((batch) {
for (final entry in _cache.entries) {
@@ -6,13 +6,12 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/server_info/server_info.model.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
@@ -193,64 +192,51 @@ class _BackupIndicator extends ConsumerWidget {
}
Widget? _getBackupBadgeIcon(BuildContext context, WidgetRef ref) {
final backupStateStream = ref.watch(settingsProvider).watch(Setting.enableBackup);
final backupEnabled = ref.watch(appConfigProvider.select((c) => c.backup.enabled));
final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none));
final isDarkTheme = context.isDarkTheme;
final iconColor = isDarkTheme ? Colors.white : Colors.black;
final isUploading = ref.watch(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty));
return StreamBuilder(
stream: backupStateStream,
initialData: false,
builder: (ctx, snapshot) {
final backupEnabled = snapshot.data ?? false;
if (!backupEnabled) {
return _BadgeLabel(
Icon(Icons.cloud_off_rounded, size: 9, color: iconColor, semanticLabel: 'backup_controller_page_backup'.tr()),
);
}
if (!backupEnabled) {
return _BadgeLabel(
Icon(
Icons.cloud_off_rounded,
size: 9,
color: iconColor,
semanticLabel: 'backup_controller_page_backup'.tr(),
if (hasError) {
return _BadgeLabel(
Icon(
Icons.warning_rounded,
size: 12,
color: context.colorScheme.error,
semanticLabel: 'backup_controller_page_backup'.tr(),
),
backgroundColor: context.colorScheme.errorContainer,
);
}
if (isUploading) {
return _BadgeLabel(
Container(
padding: const EdgeInsets.all(3.5),
child: Theme(
data: context.themeData.copyWith(
progressIndicatorTheme: context.themeData.progressIndicatorTheme.copyWith(year2023: true),
),
);
}
if (hasError) {
return _BadgeLabel(
Icon(
Icons.warning_rounded,
size: 12,
color: context.colorScheme.error,
semanticLabel: 'backup_controller_page_backup'.tr(),
child: CircularProgressIndicator(
strokeWidth: 2,
strokeCap: StrokeCap.round,
valueColor: AlwaysStoppedAnimation<Color>(iconColor),
semanticsLabel: 'backup_controller_page_backup'.tr(),
),
backgroundColor: context.colorScheme.errorContainer,
);
}
),
),
);
}
if (isUploading) {
return _BadgeLabel(
Container(
padding: const EdgeInsets.all(3.5),
child: Theme(
data: context.themeData.copyWith(
progressIndicatorTheme: context.themeData.progressIndicatorTheme.copyWith(year2023: true),
),
child: CircularProgressIndicator(
strokeWidth: 2,
strokeCap: StrokeCap.round,
valueColor: AlwaysStoppedAnimation<Color>(iconColor),
semanticsLabel: 'backup_controller_page_backup'.tr(),
),
),
),
);
}
return _BadgeLabel(
Icon(Icons.check_outlined, size: 9, color: iconColor, semanticLabel: 'backup_controller_page_backup'.tr()),
);
},
return _BadgeLabel(
Icon(Icons.check_outlined, size: 9, color: iconColor, semanticLabel: 'backup_controller_page_backup'.tr()),
);
}
}
+11 -10
View File
@@ -15,6 +15,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
@@ -186,7 +187,7 @@ class LoginForm extends HookConsumerWidget {
await backgroundManager.syncRemote();
await backgroundManager.hashAssets();
if (Store.get(StoreKey.syncAlbums, false)) {
if (MetadataRepository.instance.appConfig.backup.syncAlbums) {
await backgroundManager.syncLinkedAlbum();
}
}
@@ -397,16 +398,16 @@ class LoginForm extends HookConsumerWidget {
mainAxisSize: MainAxisSize.max,
children: [
ImmichForm(
onSubmit: getServerAuthSettings,
submitText: 'next'.t(context: context),
submitIcon: Icons.arrow_forward_rounded,
onSubmit: getServerAuthSettings,
child: ImmichURLInput(
builder: (_, form) => ImmichURLInput(
controller: serverEndpointController,
label: 'login_form_endpoint_url'.t(context: context),
hintText: 'login_form_endpoint_hint'.t(context: context),
validator: _validateUrl,
keyboardAction: .next,
onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(),
onSubmit: (_) => form.submit(),
),
),
ImmichTextButton(
@@ -434,10 +435,10 @@ class LoginForm extends HookConsumerWidget {
),
if (isPasswordLoginEnable.value)
ImmichForm(
onSubmit: login,
submitText: 'login'.t(context: context),
submitIcon: Icons.login_rounded,
onSubmit: login,
child: Column(
builder: (context, form) => Column(
spacing: ImmichSpacing.md,
children: [
ImmichTextInput(
@@ -448,7 +449,7 @@ class LoginForm extends HookConsumerWidget {
keyboardAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
onSubmit: (_, _) => passwordFocusNode.requestFocus(),
onSubmit: (_) => passwordFocusNode.requestFocus(),
),
ImmichPasswordInput(
controller: passwordController,
@@ -456,17 +457,17 @@ class LoginForm extends HookConsumerWidget {
label: 'password'.t(context: context),
hintText: 'login_form_password_hint'.t(context: context),
keyboardAction: TextInputAction.go,
onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(),
onSubmit: (_) => form.submit(),
),
],
),
),
if (isOauthEnable.value)
ImmichForm(
onSubmit: oAuthLogin,
submitText: oAuthButtonLabel.value,
submitIcon: Icons.pin_outlined,
onSubmit: oAuthLogin,
child: isPasswordLoginEnable.value
builder: (context, _) => isPasswordLoginEnable.value
? Padding(
padding: const EdgeInsets.only(left: 18.0, right: 18.0, top: 12.0),
child: Divider(color: context.isDarkTheme ? Colors.white : Colors.black, height: 5),
@@ -4,18 +4,17 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup_album.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/user.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
import 'package:immich_mobile/widgets/settings/setting_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
@@ -31,8 +30,8 @@ class DriftBackupSettings extends ConsumerWidget {
title: "network_requirements".t(context: context),
icon: Icons.cell_tower,
),
const _UseWifiForUploadVideosButton(),
const _UseWifiForUploadPhotosButton(),
const _UseCellularForVideosButton(),
const _UseCellularForPhotosButton(),
if (CurrentPlatform.isAndroid) ...[
const Divider(),
SettingGroupTitle(
@@ -99,64 +98,58 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton>
@override
Widget build(BuildContext context) {
final albumSyncEnable = ref.watch(appConfigProvider.select((c) => c.backup.syncAlbums));
return Padding(
padding: const EdgeInsets.only(left: 8.0),
child: ListView(
shrinkWrap: true,
children: [
StreamBuilder(
stream: Store.watch(StoreKey.syncAlbums),
initialData: Store.tryGet(StoreKey.syncAlbums) ?? false,
builder: (context, snapshot) {
final albumSyncEnable = snapshot.data ?? false;
return Column(
children: [
SettingListTile(
title: "sync_albums".t(context: context),
subtitle: "sync_upload_album_setting_subtitle".t(context: context),
trailing: Switch(
value: albumSyncEnable,
onChanged: (bool newValue) async {
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.syncAlbums, newValue);
Column(
children: [
SettingListTile(
title: "sync_albums".t(context: context),
subtitle: "sync_upload_album_setting_subtitle".t(context: context),
trailing: Switch(
value: albumSyncEnable,
onChanged: (bool newValue) async {
await ref.read(metadataProvider).write(MetadataKey.backupSyncAlbums, newValue);
if (newValue == true) {
await _manageLinkedAlbums();
}
},
),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: albumSyncEnable ? 1.0 : 0.0,
child: albumSyncEnable
? SettingListTile(
onTap: _manualSyncAlbums,
contentPadding: const EdgeInsets.only(left: 32, right: 16),
title: "organize_into_albums".t(context: context),
subtitle: "organize_into_albums_description".t(context: context),
trailing: isAlbumSyncInProgress
? const SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
)
: IconButton(
onPressed: _manualSyncAlbums,
icon: const Icon(Icons.sync_rounded),
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
iconSize: 20,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
),
)
: const SizedBox.shrink(),
),
),
],
);
},
if (newValue == true) {
await _manageLinkedAlbums();
}
},
),
),
AnimatedSize(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: albumSyncEnable ? 1.0 : 0.0,
child: albumSyncEnable
? SettingListTile(
onTap: _manualSyncAlbums,
contentPadding: const EdgeInsets.only(left: 32, right: 16),
title: "organize_into_albums".t(context: context),
subtitle: "organize_into_albums_description".t(context: context),
trailing: isAlbumSyncInProgress
? const SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
)
: IconButton(
onPressed: _manualSyncAlbums,
icon: const Icon(Icons.sync_rounded),
color: context.colorScheme.onSurface.withValues(alpha: 0.7),
iconSize: 20,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
),
)
: const SizedBox.shrink(),
),
),
],
),
],
),
@@ -164,60 +157,34 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton>
}
}
class _SettingsSwitchTile extends ConsumerStatefulWidget {
final AppSettingsEnum<bool> appSettingsEnum;
class _BackupSwitchTile extends ConsumerWidget {
final MetadataKey<bool> metadataKey;
final bool Function(AppConfig) selector;
final String titleKey;
final String subtitleKey;
final void Function(bool?)? onChanged;
final void Function(bool)? onChanged;
const _SettingsSwitchTile({
required this.appSettingsEnum,
const _BackupSwitchTile({
required this.metadataKey,
required this.selector,
required this.titleKey,
required this.subtitleKey,
this.onChanged,
});
@override
ConsumerState createState() => _SettingsSwitchTileState();
}
class _SettingsSwitchTileState extends ConsumerState<_SettingsSwitchTile> {
late final Stream<bool?> valueStream;
late final StreamSubscription<bool?> subscription;
@override
void initState() {
super.initState();
valueStream = Store.watch(widget.appSettingsEnum.storeKey).asBroadcastStream();
subscription = valueStream.listen((value) {
widget.onChanged?.call(value);
});
}
@override
void dispose() {
subscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final value = ref.watch(appConfigProvider.select(selector));
return Padding(
padding: const EdgeInsets.only(left: 8.0),
child: SettingListTile(
title: widget.titleKey.t(context: context),
subtitle: widget.subtitleKey.t(context: context),
trailing: StreamBuilder(
stream: valueStream,
initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue,
builder: (context, snapshot) {
final value = snapshot.data ?? false;
return Switch(
value: value,
onChanged: (bool newValue) async {
await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue);
},
);
title: titleKey.t(context: context),
subtitle: subtitleKey.t(context: context),
trailing: Switch(
value: value,
onChanged: (bool newValue) async {
await ref.read(metadataProvider).write(metadataKey, newValue);
onChanged?.call(newValue);
},
),
),
@@ -225,26 +192,28 @@ class _SettingsSwitchTileState extends ConsumerState<_SettingsSwitchTile> {
}
}
class _UseWifiForUploadVideosButton extends ConsumerWidget {
const _UseWifiForUploadVideosButton();
class _UseCellularForVideosButton extends StatelessWidget {
const _UseCellularForVideosButton();
@override
Widget build(BuildContext context, WidgetRef ref) {
return const _SettingsSwitchTile(
appSettingsEnum: AppSettingsEnum.useCellularForUploadVideos,
Widget build(BuildContext context) {
return _BackupSwitchTile(
metadataKey: MetadataKey.backupUseCellularForVideos,
selector: (c) => c.backup.useCellularForVideos,
titleKey: "videos",
subtitleKey: "network_requirement_videos_upload",
);
}
}
class _UseWifiForUploadPhotosButton extends ConsumerWidget {
const _UseWifiForUploadPhotosButton();
class _UseCellularForPhotosButton extends StatelessWidget {
const _UseCellularForPhotosButton();
@override
Widget build(BuildContext context, WidgetRef ref) {
return const _SettingsSwitchTile(
appSettingsEnum: AppSettingsEnum.useCellularForUploadPhotos,
Widget build(BuildContext context) {
return _BackupSwitchTile(
metadataKey: MetadataKey.backupUseCellularForPhotos,
selector: (c) => c.backup.useCellularForPhotos,
titleKey: "photos",
subtitleKey: "network_requirement_photos_upload",
);
@@ -256,29 +225,22 @@ class _BackupOnlyWhenChargingButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return _SettingsSwitchTile(
appSettingsEnum: AppSettingsEnum.backupRequireCharging,
final fgService = ref.read(backgroundWorkerFgServiceProvider);
return _BackupSwitchTile(
metadataKey: MetadataKey.backupRequireCharging,
selector: (c) => c.backup.requireCharging,
titleKey: "charging",
subtitleKey: "charging_requirement_mobile_backup",
onChanged: (value) {
ref.read(backgroundWorkerFgServiceProvider).configure(requireCharging: value ?? false);
fgService.configure(requireCharging: value);
},
);
}
}
class _BackupDelaySlider extends ConsumerStatefulWidget {
class _BackupDelaySlider extends ConsumerWidget {
const _BackupDelaySlider();
@override
ConsumerState<_BackupDelaySlider> createState() => _BackupDelaySliderState();
}
class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> {
late final Stream<int?> valueStream;
late final StreamSubscription<int?> subscription;
late int currentValue;
static int backupDelayToSliderValue(int ms) => switch (ms) {
5 => 0,
30 => 1,
@@ -301,30 +263,9 @@ class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> {
};
@override
void initState() {
super.initState();
final initialValue =
Store.tryGet(AppSettingsEnum.backupTriggerDelay.storeKey) ?? AppSettingsEnum.backupTriggerDelay.defaultValue;
currentValue = backupDelayToSliderValue(initialValue);
valueStream = Store.watch(AppSettingsEnum.backupTriggerDelay.storeKey).asBroadcastStream();
subscription = valueStream.listen((value) {
if (mounted && value != null) {
setState(() {
currentValue = backupDelayToSliderValue(value);
});
}
});
}
@override
void dispose() {
subscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final triggerDelay = ref.watch(appConfigProvider.select((c) => c.backup.triggerDelay));
final currentValue = backupDelayToSliderValue(triggerDelay);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -339,14 +280,13 @@ class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> {
),
Slider(
value: currentValue.toDouble(),
onChanged: (double v) {
setState(() {
currentValue = v.toInt();
});
onChanged: (double v) async {
final seconds = backupDelayToSeconds(v.toInt());
await ref.read(metadataProvider).write(MetadataKey.backupTriggerDelay, seconds);
},
onChangeEnd: (double v) async {
final milliseconds = backupDelayToSeconds(v.toInt());
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.backupTriggerDelay, milliseconds);
final seconds = backupDelayToSeconds(v.toInt());
await ref.read(metadataProvider).write(MetadataKey.backupTriggerDelay, seconds);
},
max: 3.0,
min: 0.0,
@@ -1,12 +1,13 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/services/localization.service.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/widgets/common/search_field.dart';
class LanguageSettings extends HookConsumerWidget {
@@ -84,7 +85,7 @@ class LanguageSettings extends HookConsumerWidget {
padding: const EdgeInsets.all(8),
itemCount: filteredLocaleEntries.value.length,
itemExtent: 64.0,
cacheExtent: 100,
scrollCacheExtent: const .pixels(100),
itemBuilder: (context, index) {
final countryName = filteredLocaleEntries.value[index].key;
final localeValue = filteredLocaleEntries.value[index].value;
@@ -1,13 +1,11 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/widgets/settings/networking_settings/endpoint_input.dart';
class ExternalNetworkPreference extends HookConsumerWidget {
@@ -23,11 +21,12 @@ class ExternalNetworkPreference extends HookConsumerWidget {
saveEndpointList() {
canSave.value = entries.value.every((e) => e.status == AuxCheckStatus.valid);
final endpointList = entries.value.where((url) => url.status == AuxCheckStatus.valid).toList();
final urls = entries.value
.where((e) => e.status == AuxCheckStatus.valid && e.url.isNotEmpty)
.map((e) => e.url)
.toList();
final jsonString = jsonEncode(endpointList);
Store.put(StoreKey.externalEndpointList, jsonString);
ref.read(metadataProvider).write(MetadataKey.networkExternalEndpointList, urls);
}
updateValidationStatus(String url, int index, AuxCheckStatus status) {
@@ -37,10 +36,6 @@ class ExternalNetworkPreference extends HookConsumerWidget {
}
handleReorder(int oldIndex, int newIndex) {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final entry = entries.value.removeAt(oldIndex);
entries.value.insert(newIndex, entry);
entries.value = [...entries.value];
@@ -69,14 +64,13 @@ class ExternalNetworkPreference extends HookConsumerWidget {
}
useEffect(() {
final jsonString = Store.tryGet(StoreKey.externalEndpointList);
final urls = ref.read(metadataProvider).systemConfig.network.externalEndpointList;
if (jsonString == null) {
if (urls.isEmpty) {
return null;
}
final List<dynamic> jsonList = jsonDecode(jsonString);
entries.value = jsonList.map((e) => AuxilaryEndpoint.fromJson(e)).toList();
entries.value = urls.map((url) => AuxilaryEndpoint(url: url, status: .valid)).toList();
return null;
}, const []);
@@ -115,7 +109,7 @@ class ExternalNetworkPreference extends HookConsumerWidget {
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: entries.value.length,
onReorder: handleReorder,
onReorderItem: handleReorder,
itemBuilder: (context, index) {
return EndpointInput(
key: Key(index.toString()),
@@ -1,13 +1,12 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/network.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/utils/url_helper.dart';
import 'package:immich_mobile/widgets/settings/networking_settings/external_network_preference.dart';
import 'package:immich_mobile/widgets/settings/networking_settings/local_network_preference.dart';
@@ -20,7 +19,10 @@ class NetworkingSettings extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentEndpoint = getServerUrl();
final featureEnabled = useAppSettingsState(AppSettingsEnum.autoEndpointSwitching);
final featureEnabled = useState(ref.read(systemConfigProvider).network.autoEndpointSwitching);
useValueChanged<bool, void>(featureEnabled.value, (_, __) {
ref.read(metadataProvider).write(.networkAutoEndpointSwitching, featureEnabled.value);
});
Future<void> checkWifiReadPermission() async {
final [hasLocationInUse, hasLocationAlways] = await Future.wait([
+8 -8
View File
@@ -1,26 +1,26 @@
.PHONY: build watch create_app_icon create_splash build_release_android pigeon test analyze format migration translation
build:
@printf "This command has been removed. Please use:\n\n mise codegen # or mise //:mobile:codegen:dart from another directory\n\n" >&2 && exit 1
@printf "This command has been removed. Please use:\n\n mise codegen # or mise //mobile:codegen:dart from another directory\n\n" >&2 && exit 1
pigeon:
@printf "This command has been removed. Please use:\n\n mise pigeon # or mise //:mobile:codegen:pigeon from another directory\n\n" >&2 && exit 1
@printf "This command has been removed. Please use:\n\n mise pigeon # or mise //mobile:codegen:pigeon from another directory\n\n" >&2 && exit 1
build_release_android:
@printf "This command has been removed. Please use:\n\n mise run build:android # or mise //:mobile:build:android from another directory\n\n" >&2 && exit 1
@printf "This command has been removed. Please use:\n\n mise run build:android # or mise //mobile:build:android from another directory\n\n" >&2 && exit 1
migration:
@printf "This command has been removed. Please use:\n\n mise migration # or mise //:mobile:drift:migration from another directory\n\n" >&2 && exit 1
@printf "This command has been removed. Please use:\n\n mise migration # or mise //mobile:drift:migration from another directory\n\n" >&2 && exit 1
translation:
@printf "This command has been removed. Please use:\n\n mise translation # or mise //:mobile:codegen:translation from another directory\n\n" >&2 && exit 1
@printf "This command has been removed. Please use:\n\n mise translation # or mise //mobile:codegen:translation from another directory\n\n" >&2 && exit 1
analyze:
@printf "This command has been removed. Please use:\n\n mise analyze # or mise //:mobile:lint from another directory\n\n" >&2 && exit 1
@printf "This command has been removed. Please use:\n\n mise analyze # or mise //mobile:lint from another directory\n\n" >&2 && exit 1
format:
@printf "This command has been removed. Please use:\n\n mise format # or mise //:mobile:format from another directory\n\n" >&2 && exit 1
@printf "This command has been removed. Please use:\n\n mise format # or mise //mobile:format from another directory\n\n" >&2 && exit 1
test:
@printf "This command has been removed. Please use:\n\n mise test # or mise //:mobile:test from another directory\n\n" >&2 && exit 1
@printf "This command has been removed. Please use:\n\n mise test # or mise //mobile:test from another directory\n\n" >&2 && exit 1
+1 -1
View File
@@ -1 +1 @@
7.22.0
7.8.0
+1 -1
View File
@@ -4,7 +4,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 3.0.0
- Generator version: 7.22.0
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
-1
View File
@@ -29,7 +29,6 @@ part 'auth/api_key_auth.dart';
part 'auth/oauth.dart';
part 'auth/http_basic_auth.dart';
part 'auth/http_bearer_auth.dart';
part 'optional.dart';
part 'api/api_keys_api.dart';
part 'api/activities_api.dart';
+3 -3
View File
@@ -143,19 +143,19 @@ class ApiClient {
);
}
Future<dynamic> deserializeAsync(String value, String targetType, {bool growable = false,}) async =>
Future<dynamic> deserializeAsync(String value, String targetType, {bool growable = false,}) =>
// ignore: deprecated_member_use_from_same_package
deserialize(value, targetType, growable: growable);
@Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.')
dynamic deserialize(String value, String targetType, {bool growable = false,}) {
Future<dynamic> deserialize(String value, String targetType, {bool growable = false,}) async {
// Remove all spaces. Necessary for regular expressions as well.
targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments
// If the expected target type is String, nothing to do...
return targetType == 'String'
? value
: fromJson(json.decode(value), targetType, growable: growable);
: fromJson(await compute((String j) => json.decode(j), value), targetType, growable: growable);
}
// ignore: deprecated_member_use_from_same_package

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