Compare commits

...

25 Commits

Author SHA1 Message Date
bwees faaeaace1d fix: memory viewer bar 2026-05-22 16:50:18 -05:00
bwees 2837de2029 chore: ci fix 2026-05-22 14:01:11 -05:00
bwees eb27635f22 refactor: use ControlBar UI Library component 2026-05-22 11:35:16 -05:00
Peter Ombodi 78ac0ade01 feat(mobile): add manage media APIs to NativeSyncApi (#28441)
* feat(mobile): add manage media APIs to NativeSyncApi

* fix(mobile): remove legacy local file manager from trash sync

* refactor(mobile): move media permission methods to PermissionApi

* cleanup

---------

Co-authored-by: Peter Ombodi <peter.ombodi@gmail.com>
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-22 17:40:11 +05:30
Mert 7b9dab872b fix(mobile): separate group ids for separate app installs (#28448)
* separate group ids

* remove pigeon method

* Revert "remove pigeon method"

This reverts commit d699ff2094.
2026-05-21 12:25:20 -05:00
Daniel Dietzler 6413495fb8 fix: mise lockfile (#28541) 2026-05-21 13:13:37 +02:00
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
162 changed files with 7473 additions and 5453 deletions
+7 -8
View File
@@ -91,7 +91,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -159,14 +159,14 @@ jobs:
- name: Comment APK download link on PR - name: Comment APK download link on PR
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} 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: env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }}
APK_URL: ${{ steps.upload-apk.outputs.artifact-url }} APK_URL: ${{ steps.upload-apk.outputs.artifact-url }}
with: with:
github-token: ${{ steps.token.outputs.token }} id: mobile-android-apk
message-id: 'mobile-android-apk' token: ${{ steps.token.outputs.token }}
message: | body: |
📱 **Android release APK (universal)** — `${{ env.HEAD_SHA }}` 📱 **Android release APK (universal)** — `${{ env.HEAD_SHA }}`
Download: ${{ env.APK_URL }} Download: ${{ env.APK_URL }}
@@ -216,7 +216,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -231,7 +231,7 @@ jobs:
run: mise //mobile:codegen:pigeon run: mise //mobile:codegen:pigeon
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0 uses: ruby/setup-ruby@6aaa311d81eba98ae12eaffbcb63296ace0efcde # v1.307.0
with: with:
ruby-version: '3.3' ruby-version: '3.3'
bundler-cache: true bundler-cache: true
@@ -288,7 +288,6 @@ jobs:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
ENVIRONMENT: ${{ inputs.environment || 'development' }} ENVIRONMENT: ${{ inputs.environment || 'development' }}
BUNDLE_ID_SUFFIX: ${{ inputs.environment == 'production' && '' || 'development' }}
GITHUB_REF: ${{ github.ref }} GITHUB_REF: ${{ github.ref }}
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120 FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120
FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 6 FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 6
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Check for breaking API changes - name: Check for breaking API changes
uses: oasdiff/oasdiff-action/breaking@26ccb332c67a45ca649de9faf60552ef1b8260d9 # v0.0.46 uses: oasdiff/oasdiff-action/breaking@6147a58e5d1249a12f42fc864ab791d571a30015 # v0.0.47
with: with:
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
revision: open-api/immich-openapi-specs.json revision: open-api/immich-openapi-specs.json
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
+3 -3
View File
@@ -57,7 +57,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -70,7 +70,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
# ️ Command-line programs to run using the OS shell. # ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -83,6 +83,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'
+1 -1
View File
@@ -66,7 +66,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
+3 -4
View File
@@ -131,7 +131,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -213,12 +213,11 @@ jobs:
run: 'mise run //deployment:tf apply' run: 'mise run //deployment:tf apply'
- name: Comment - 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' }} if: ${{ steps.parameters.outputs.event == 'pr' }}
with: with:
id: docs-pr-url
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }} number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}
body: | body: |
📖 Documentation deployed to [${{ steps.docs-output.outputs.subdomain }}](https://${{ steps.docs-output.outputs.subdomain }}) 📖 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 }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -44,9 +44,8 @@ jobs:
run: 'mise run //deployment:tf destroy -- -refresh=false' run: 'mise run //deployment:tf destroy -- -refresh=false'
- name: Comment - 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: with:
id: docs-pr-url
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
number: ${{ github.event.number }}
delete: true delete: true
body-include: '<!-- Docs PR URL -->'
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- name: Generate a token - name: Generate a token
id: generate_token id: generate_token
if: ${{ inputs.skip != true }} 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: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
+1
View File
@@ -13,3 +13,4 @@ jobs:
actions: read actions: read
contents: read contents: read
security-events: write security-events: write
secrets: inherit
+2 -2
View File
@@ -62,7 +62,7 @@ jobs:
ref: main ref: main
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -119,7 +119,7 @@ jobs:
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-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: with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
+12 -12
View File
@@ -19,11 +19,11 @@ jobs:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }} client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0 - uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
with: with:
github-token: ${{ steps.token.outputs.token }} id: preview-status
message-id: 'preview-status' token: ${{ steps.token.outputs.token }}
message: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.build/' body: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.build/'
remove-label: remove-label:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -48,16 +48,16 @@ jobs:
name: 'preview' 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 }} if: ${{ github.event.pull_request.head.repo.fork }}
with: with:
github-token: ${{ steps.token.outputs.token }} id: preview-status
message-id: 'preview-status' token: ${{ steps.token.outputs.token }}
message: 'PRs from forks cannot have preview environments.' 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 }} if: ${{ !github.event.pull_request.head.repo.fork }}
with: with:
github-token: ${{ steps.token.outputs.token }} id: preview-status
message-id: 'preview-status' token: ${{ steps.token.outputs.token }}
message: 'Preview environment has been removed.' body: 'Preview environment has been removed.'
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
+1 -1
View File
@@ -61,7 +61,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
+20 -13
View File
@@ -30,25 +30,32 @@ jobs:
filters: | filters: |
i18n: i18n:
- 'i18n/**' - 'i18n/**'
- 'mise.toml'
web: web:
- 'web/**' - 'web/**'
- 'i18n/**' - 'i18n/**'
- 'packages/sdk/**' - 'packages/sdk/**'
- 'pnpm-lock.yaml' - 'pnpm-lock.yaml'
- 'mise.toml'
server: server:
- 'server/**' - 'server/**'
- 'pnpm-lock.yaml' - 'pnpm-lock.yaml'
- 'mise.toml'
cli: cli:
- 'packages/cli/**' - 'packages/cli/**'
- 'packages/sdk/**' - 'packages/sdk/**'
- 'pnpm-lock.yaml' - 'pnpm-lock.yaml'
- 'mise.toml'
e2e: e2e:
- 'e2e/**' - 'e2e/**'
- 'pnpm-lock.yaml' - 'pnpm-lock.yaml'
- 'mise.toml'
mobile: mobile:
- 'mobile/**' - 'mobile/**'
- 'mise.toml'
machine-learning: machine-learning:
- 'machine-learning/**' - 'machine-learning/**'
- 'mise.toml'
.github: .github:
- '.github/**' - '.github/**'
force-filters: | force-filters: |
@@ -76,7 +83,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -107,7 +114,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -138,7 +145,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -182,7 +189,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -220,7 +227,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -248,7 +255,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -298,7 +305,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -331,7 +338,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -550,7 +557,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -587,7 +594,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -618,7 +625,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -669,7 +676,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} github_token: ${{ steps.token.outputs.token }}
@@ -727,7 +734,7 @@ jobs:
token: ${{ steps.token.outputs.token }} token: ${{ steps.token.outputs.token }}
- name: Setup Mise - 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: with:
github_token: ${{ steps.token.outputs.token }} 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', url: 'https://docs.immich.app',
baseUrl: '/', baseUrl: '/',
onBrokenLinks: 'throw', onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.png', favicon: 'img/favicon.png',
// GitHub pages deployment config. // GitHub pages deployment config.
@@ -29,6 +28,9 @@ const config = {
// Mermaid diagrams // Mermaid diagrams
markdown: { markdown: {
mermaid: true, mermaid: true,
hooks: {
onBrokenMarkdownLinks: 'warn',
},
}, },
themes: ['@docusaurus/theme-mermaid'], 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}" run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
[tools] [tools]
wrangler = "4.66.0" wrangler = "4.91.0"
+1 -1
View File
@@ -32,7 +32,7 @@
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2", "@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^24.12.2", "@types/node": "^24.12.4",
"@types/pg": "^8.15.1", "@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/supertest": "^7.0.0", "@types/supertest": "^7.0.0",
@@ -536,7 +536,7 @@ test.describe('Timeline', () => {
force: false, force: false,
ids: [assetToTrash.id], ids: [assetToTrash.id],
}); });
await page.locator('#asset-selection-app-bar').getByLabel('Close').click(); await page.keyboard.press('Escape');
await page.getByText('Trash', { exact: true }).click(); await page.getByText('Trash', { exact: true }).click();
await timelineUtils.waitForTimelineLoad(page); await timelineUtils.waitForTimelineLoad(page);
await thumbnailUtils.expectInViewport(page, assetToTrash.id); await thumbnailUtils.expectInViewport(page, assetToTrash.id);
@@ -676,7 +676,7 @@ test.describe('Timeline', () => {
ids: [assetToArchive.id], ids: [assetToArchive.id],
}); });
await thumbnailUtils.expectThumbnailIsArchive(page, assetToArchive.id); await thumbnailUtils.expectThumbnailIsArchive(page, assetToArchive.id);
await page.locator('#asset-selection-app-bar').getByLabel('Close').click(); await page.keyboard.press('Escape');
await page.getByRole('link').getByText('Archive').click(); await page.getByRole('link').getByText('Archive').click();
await timelineUtils.waitForTimelineLoad(page); await timelineUtils.waitForTimelineLoad(page);
await thumbnailUtils.expectInViewport(page, assetToArchive.id); await thumbnailUtils.expectInViewport(page, assetToArchive.id);
@@ -823,7 +823,7 @@ test.describe('Timeline', () => {
}); });
// ensure thumbnail still exists and has favorite icon // ensure thumbnail still exists and has favorite icon
await thumbnailUtils.expectThumbnailIsFavorite(page, assetToFavorite.id); await thumbnailUtils.expectThumbnailIsFavorite(page, assetToFavorite.id);
await page.locator('#asset-selection-app-bar').getByLabel('Close').click(); await page.keyboard.press('Escape');
await page.getByRole('link').getByText('Favorites').click(); await page.getByRole('link').getByText('Favorites').click();
await timelineUtils.waitForTimelineLoad(page); await timelineUtils.waitForTimelineLoad(page);
await pageUtils.goToAsset(page, assetToFavorite.fileCreatedAt); await pageUtils.goToAsset(page, assetToFavorite.fileCreatedAt);
+10 -1
View File
@@ -897,6 +897,7 @@
"date_of_birth": "Date of birth", "date_of_birth": "Date of birth",
"date_of_birth_saved": "Date of birth saved successfully", "date_of_birth_saved": "Date of birth saved successfully",
"date_range": "Date range", "date_range": "Date range",
"date_time_original": "Date/Time Original",
"day": "Day", "day": "Day",
"days": "Days", "days": "Days",
"deduplicate_all": "Deduplicate All", "deduplicate_all": "Deduplicate All",
@@ -1197,11 +1198,13 @@
"export_as_json": "Export as JSON", "export_as_json": "Export as JSON",
"export_database": "Export Database", "export_database": "Export Database",
"export_database_description": "Export the SQLite database", "export_database_description": "Export the SQLite database",
"exposure_time": "Exposure Time",
"extension": "Extension", "extension": "Extension",
"external": "External", "external": "External",
"external_libraries": "External Libraries", "external_libraries": "External Libraries",
"external_network": "External network", "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", "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", "face_unassigned": "Unassigned",
"failed": "Failed", "failed": "Failed",
"failed_count": "Failed: {count}", "failed_count": "Failed: {count}",
@@ -1219,7 +1222,6 @@
"features_setting_description": "Manage the app features", "features_setting_description": "Manage the app features",
"file_name_or_extension": "File name or extension", "file_name_or_extension": "File name or extension",
"file_name_text": "File name", "file_name_text": "File name",
"file_name_with_value": "File name: {file_name}",
"file_size": "File size", "file_size": "File size",
"filename": "Filename", "filename": "Filename",
"filetype": "Filetype", "filetype": "Filetype",
@@ -1232,6 +1234,7 @@
"find_them_fast": "Find them fast by name with search", "find_them_fast": "Find them fast by name with search",
"first": "First", "first": "First",
"fix_incorrect_match": "Fix incorrect match", "fix_incorrect_match": "Fix incorrect match",
"focal_length": "Focal Length",
"folder": "Folder", "folder": "Folder",
"folder_not_found": "Folder not found", "folder_not_found": "Folder not found",
"folders": "Folders", "folders": "Folders",
@@ -1352,6 +1355,7 @@
"ios_debug_info_no_sync_yet": "No background sync job has run yet", "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_processes_queued": "{count, plural, one {{count} background process queued} other {{count} background processes queued}}",
"ios_debug_info_processing_ran_at": "Processing ran {dateTime}", "ios_debug_info_processing_ran_at": "Processing ran {dateTime}",
"iso": "ISO",
"items_count": "{count, plural, one {# item} other {# items}}", "items_count": "{count, plural, one {# item} other {# items}}",
"jobs": "Jobs", "jobs": "Jobs",
"json_editor": "JSON editor", "json_editor": "JSON editor",
@@ -1584,6 +1588,7 @@
"mobile_app": "Mobile App", "mobile_app": "Mobile App",
"mobile_app_download_onboarding_note": "Download the companion mobile app using the following options", "mobile_app_download_onboarding_note": "Download the companion mobile app using the following options",
"model": "Model", "model": "Model",
"modify_date": "Modify Date",
"month": "Month", "month": "Month",
"more": "More", "more": "More",
"motion": "Motion", "motion": "Motion",
@@ -1706,6 +1711,7 @@
"organize_into_albums": "Organize into albums", "organize_into_albums": "Organize into albums",
"organize_into_albums_description": "Put existing photos into albums using current sync settings", "organize_into_albums_description": "Put existing photos into albums using current sync settings",
"organize_your_library": "Organize your library", "organize_your_library": "Organize your library",
"orientation": "Orientation",
"original": "original", "original": "original",
"other": "Other", "other": "Other",
"other_devices": "Other devices", "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_drawer_readonly_mode": "Read-only mode enabled. Long-press the user avatar icon to exit.",
"profile_image_of_user": "Profile image of {user}", "profile_image_of_user": "Profile image of {user}",
"profile_picture_set": "Profile picture set.", "profile_picture_set": "Profile picture set.",
"projection_type": "Projection Type",
"public_album": "Public album", "public_album": "Public album",
"public_share": "Public Share", "public_share": "Public Share",
"purchase_account_info": "Supporter", "purchase_account_info": "Supporter",
@@ -2189,7 +2196,9 @@
"show_in_timeline": "Show in timeline", "show_in_timeline": "Show in timeline",
"show_in_timeline_setting_description": "Show photos and videos from this user in your timeline", "show_in_timeline_setting_description": "Show photos and videos from this user in your timeline",
"show_keyboard_shortcuts": "Show keyboard shortcuts", "show_keyboard_shortcuts": "Show keyboard shortcuts",
"show_less": "Show less",
"show_metadata": "Show metadata", "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_or_hide_info": "Show or hide info",
"show_password": "Show password", "show_password": "Show password",
"show_person_options": "Show person options", "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"
+332
View File
@@ -0,0 +1,332 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools."aqua:flutter/flutter"]]
version = "3.41.9"
backend = "aqua:flutter/flutter"
[[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"
[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"
[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"
[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"
[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"
[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"
[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"
[[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"
[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"
[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"
[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"
[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"
[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"
[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"
[[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"
[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"
[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"
[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"
[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"
[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"
[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"
[[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"
[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"
[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"
[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"
[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"
[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"
[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"
[[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"
[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"
[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"
[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"
[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"
[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"
[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"
[[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.4"
backend = "aqua:pnpm/pnpm"
[[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] [tools]
node = "24.15.0" node = "24.15.0"
flutter = "3.41.9" "aqua:flutter/flutter" = "3.41.9"
pnpm = "10.33.1" pnpm = "10.33.4"
terragrunt = "1.0.3" terragrunt = "1.0.3"
opentofu = "1.11.6" opentofu = "1.11.6"
java = "21.0.2" java = "21.0.2"
@@ -50,11 +50,12 @@ macos-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
[settings] [settings]
experimental = true experimental = true
pin = true pin = true
lockfile = true
[tasks.plugins] [tasks.plugins]
run = [ run = [
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile", "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] [tasks.open-api-typescript]
@@ -76,8 +77,8 @@ run = [
{ task = "//server:install" }, { task = "//server:install" },
{ task = "//server:build" }, { task = "//server:build" },
{ task = "//server:sync-open-api" }, { task = "//server:sync-open-api" },
{ task = ":open-api-typescript"}, { task = ":open-api-typescript" },
{ task = ":open-api-dart"}, { task = ":open-api-dart" },
] ]
[tasks.sql] [tasks.sql]
+7
View File
@@ -89,6 +89,13 @@ flutter {
} }
dependencies { 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.okhttp
implementation libs.cronet.embedded implementation libs.cronet.embedded
implementation libs.media3.datasource.okhttp implementation libs.media3.datasource.okhttp
@@ -17,6 +17,8 @@ import app.alextran.immich.images.LocalImageApi
import app.alextran.immich.images.LocalImagesImpl import app.alextran.immich.images.LocalImagesImpl
import app.alextran.immich.images.RemoteImageApi import app.alextran.immich.images.RemoteImageApi
import app.alextran.immich.images.RemoteImagesImpl import app.alextran.immich.images.RemoteImagesImpl
import app.alextran.immich.permission.PermissionApi
import app.alextran.immich.permission.PermissionApiImpl
import app.alextran.immich.sync.NativeSyncApi import app.alextran.immich.sync.NativeSyncApi
import app.alextran.immich.sync.NativeSyncApiImpl26 import app.alextran.immich.sync.NativeSyncApiImpl26
import app.alextran.immich.sync.NativeSyncApiImpl30 import app.alextran.immich.sync.NativeSyncApiImpl30
@@ -44,7 +46,9 @@ class MainActivity : FlutterFragmentActivity() {
} else { } else {
NativeSyncApiImpl30(ctx) NativeSyncApiImpl30(ctx)
} }
val permissionApiImpl = PermissionApiImpl(ctx)
NativeSyncApi.setUp(messenger, nativeSyncApiImpl) NativeSyncApi.setUp(messenger, nativeSyncApiImpl)
PermissionApi.setUp(messenger, permissionApiImpl)
LocalImageApi.setUp(messenger, LocalImagesImpl(ctx)) LocalImageApi.setUp(messenger, LocalImagesImpl(ctx))
RemoteImageApi.setUp(messenger, RemoteImagesImpl(ctx)) RemoteImageApi.setUp(messenger, RemoteImagesImpl(ctx))
@@ -53,6 +57,7 @@ class MainActivity : FlutterFragmentActivity() {
flutterEngine.plugins.add(backgroundEngineLockImpl) flutterEngine.plugins.add(backgroundEngineLockImpl)
flutterEngine.plugins.add(nativeSyncApiImpl) flutterEngine.plugins.add(nativeSyncApiImpl)
flutterEngine.plugins.add(permissionApiImpl)
} }
fun cancelPlugins(flutterEngine: FlutterEngine) { fun cancelPlugins(flutterEngine: FlutterEngine) {
@@ -60,6 +65,8 @@ class MainActivity : FlutterFragmentActivity() {
flutterEngine.plugins.get(NativeSyncApiImpl26::class.java) as ImmichPlugin? flutterEngine.plugins.get(NativeSyncApiImpl26::class.java) as ImmichPlugin?
?: flutterEngine.plugins.get(NativeSyncApiImpl30::class.java) as ImmichPlugin? ?: flutterEngine.plugins.get(NativeSyncApiImpl30::class.java) as ImmichPlugin?
nativeApi?.detachFromEngine() nativeApi?.detachFromEngine()
val permissionApi = flutterEngine.plugins.get(PermissionApiImpl::class.java) as ImmichPlugin?
permissionApi?.detachFromEngine()
} }
} }
} }
@@ -315,6 +315,7 @@ interface NetworkApi {
fun hasCertificate(): Boolean fun hasCertificate(): Boolean
fun getClientPointer(): Long fun getClientPointer(): Long
fun setRequestHeaders(headers: Map<String, String>, serverUrls: List<String>, token: String?) fun setRequestHeaders(headers: Map<String, String>, serverUrls: List<String>, token: String?)
fun getAppGroupId(): String
companion object { companion object {
/** The codec used by NetworkApi. */ /** The codec used by NetworkApi. */
@@ -430,6 +431,21 @@ interface NetworkApi {
channel.setMessageHandler(null) channel.setMessageHandler(null)
} }
} }
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.getAppGroupId())
} catch (exception: Throwable) {
NetworkPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
} }
} }
} }
@@ -13,7 +13,7 @@ class NetworkApiPlugin : FlutterPlugin, ActivityAware {
private var networkApi: NetworkApiImpl? = null private var networkApi: NetworkApiImpl? = null
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
networkApi = NetworkApiImpl() networkApi = NetworkApiImpl(binding.applicationContext)
NetworkApi.setUp(binding.binaryMessenger, networkApi) NetworkApi.setUp(binding.binaryMessenger, networkApi)
} }
@@ -39,9 +39,11 @@ class NetworkApiPlugin : FlutterPlugin, ActivityAware {
} }
} }
private class NetworkApiImpl : NetworkApi { private class NetworkApiImpl(private val context: Context) : NetworkApi {
var activity: Activity? = null var activity: Activity? = null
override fun getAppGroupId(): String = context.packageName
override fun addCertificate(clientData: ClientCertData, callback: (Result<Unit>) -> Unit) { override fun addCertificate(clientData: ClientCertData, callback: (Result<Unit>) -> Unit) {
try { try {
HttpClientManager.setKeyEntry(clientData.data, clientData.password.toCharArray()) HttpClientManager.setKeyEntry(clientData.data, clientData.password.toCharArray())
@@ -0,0 +1,96 @@
package app.alextran.immich.permission
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.MediaStore
import android.provider.Settings
import androidx.core.net.toUri
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.PluginRegistry
class ManageMediaPermissionDelegate(
context: Context,
private val requestCode: Int = 1003,
) : PluginRegistry.ActivityResultListener {
private val ctx = context.applicationContext
private var activityBinding: ActivityPluginBinding? = null
private var pendingResult: ((Result<Boolean>) -> Unit)? = null
fun hasManageMediaPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaStore.canManageMedia(ctx)
} else {
false
}
}
fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit) {
if (hasManageMediaPermission()) {
callback(Result.success(true))
return
}
openManageMediaPermissionSettings(callback)
}
fun manageMediaPermission(callback: (Result<Boolean>) -> Unit) {
openManageMediaPermissionSettings(callback)
}
private fun openManageMediaPermissionSettings(callback: (Result<Boolean>) -> Unit) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
callback(Result.success(false))
return
}
val activity = activityBinding?.activity
if (activity == null) {
callback(Result.failure(FlutterError("NO_ACTIVITY", "Activity not available", null)))
return
}
pendingResult = callback
val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA).apply {
data = "package:${activity.packageName}".toUri()
}
try {
activity.startActivityForResult(intent, requestCode)
} catch (e: Exception) {
pendingResult = null
callback(
Result.failure(
FlutterError("ACTIVITY_LAUNCH_FAILED", "Failed to launch MANAGE_MEDIA settings", e.toString())
)
)
}
}
fun onAttachedToActivity(binding: ActivityPluginBinding) {
activityBinding = binding
binding.addActivityResultListener(this)
}
fun onDetachedFromActivity() {
failPending("ACTIVITY_DETACHED", "Activity detached before MANAGE_MEDIA result")
activityBinding?.removeActivityResultListener(this)
activityBinding = null
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == this.requestCode) {
val callback = pendingResult
pendingResult = null
callback?.invoke(Result.success(hasManageMediaPermission()))
return true
}
return false
}
private fun failPending(code: String, message: String) {
val callback = pendingResult ?: return
pendingResult = null
callback(Result.failure(FlutterError(code, message, null)))
}
}
@@ -0,0 +1,128 @@
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
package app.alextran.immich.permission
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMethodCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private object PermissionApiPigeonUtils {
fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
fun wrapError(exception: Throwable): List<Any?> {
return if (exception is FlutterError) {
listOf(
exception.code,
exception.message,
exception.details
)
} else {
listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
)
}
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : RuntimeException()
private open class PermissionApiPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return super.readValueOfType(type, buffer)
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
super.writeValue(stream, value)
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface PermissionApi {
fun hasManageMediaPermission(): Boolean
fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit)
fun manageMediaPermission(callback: (Result<Boolean>) -> Unit)
companion object {
/** The codec used by PermissionApi. */
val codec: MessageCodec<Any?> by lazy {
PermissionApiPigeonCodec()
}
/** Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.hasManageMediaPermission())
} catch (exception: Throwable) {
PermissionApiPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.requestManageMediaPermission{ result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(PermissionApiPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(PermissionApiPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.manageMediaPermission{ result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(PermissionApiPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(PermissionApiPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
@@ -0,0 +1,37 @@
package app.alextran.immich.permission
import android.content.Context
import app.alextran.immich.core.ImmichPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
class PermissionApiImpl(context: Context) : ImmichPlugin(), PermissionApi, ActivityAware {
private val manageMediaPermissionDelegate = ManageMediaPermissionDelegate(context)
override fun hasManageMediaPermission(): Boolean =
manageMediaPermissionDelegate.hasManageMediaPermission()
override fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit) {
manageMediaPermissionDelegate.requestManageMediaPermission { completeWhenActive(callback, it) }
}
override fun manageMediaPermission(callback: (Result<Boolean>) -> Unit) {
manageMediaPermissionDelegate.manageMediaPermission { completeWhenActive(callback, it) }
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
manageMediaPermissionDelegate.onAttachedToActivity(binding)
}
override fun onDetachedFromActivityForConfigChanges() {
manageMediaPermissionDelegate.onDetachedFromActivity()
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
manageMediaPermissionDelegate.onAttachedToActivity(binding)
}
override fun onDetachedFromActivity() {
manageMediaPermissionDelegate.onDetachedFromActivity()
}
}
@@ -0,0 +1,133 @@
package app.alextran.immich.sync
import android.app.Activity
import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.PluginRegistry
class MediaTrashDelegate(
context: Context,
private val trashRequestCode: Int = 1002,
) : PluginRegistry.ActivityResultListener {
private val ctx = context.applicationContext
private var activityBinding: ActivityPluginBinding? = null
private var pendingResult: ((Result<Boolean>) -> Unit)? = null
private fun hasManageMediaPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaStore.canManageMedia(ctx)
} else {
false
}
}
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || !hasManageMediaPermission()) {
callback(Result.failure(FlutterError("PERMISSION_DENIED", "Media permission required", null)))
return
}
val id = mediaId.toLongOrNull()
if (id == null) {
callback(Result.failure(FlutterError("INVALID_ID", "The file id is not a valid number: $mediaId", null)))
return
}
if (!isInTrash(id)) {
callback(Result.failure(FlutterError("TRASH_NOT_FOUND", "Item with id=$id not found in trash", null)))
return
}
restoreUri(ContentUris.withAppendedId(contentUriForType(type.toInt()), id), callback)
}
@RequiresApi(Build.VERSION_CODES.R)
private fun restoreUri(
contentUri: Uri,
callback: (Result<Boolean>) -> Unit,
) {
val activity = activityBinding?.activity
if (activity == null) {
callback(Result.failure(FlutterError("NO_ACTIVITY", "Activity not available", null)))
return
}
try {
val pendingIntent = MediaStore.createTrashRequest(ctx.contentResolver, listOf(contentUri), false)
pendingResult = callback
activity.startIntentSenderForResult(
pendingIntent.intentSender,
trashRequestCode,
null,
0,
0,
0,
)
} catch (e: Exception) {
pendingResult = null
callback(
Result.failure(
FlutterError("TRASH_ERROR", "Error creating or starting trash request", e.toString())
)
)
}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun isInTrash(id: Long): Boolean {
val filesUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
val args = Bundle().apply {
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns._ID}=?")
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(id.toString()))
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
putInt(ContentResolver.QUERY_ARG_LIMIT, 1)
}
return ctx.contentResolver.query(filesUri, arrayOf(MediaStore.Files.FileColumns._ID), args, null)
?.use { it.moveToFirst() } == true
}
private fun contentUriForType(type: Int): Uri =
when (type) {
// Same order as AssetType from Dart.
1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
else -> MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
}
fun onAttachedToActivity(binding: ActivityPluginBinding) {
activityBinding = binding
binding.addActivityResultListener(this)
}
fun onDetachedFromActivity() {
failPending("ACTIVITY_DETACHED", "Activity detached before trash result")
activityBinding?.removeActivityResultListener(this)
activityBinding = null
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == trashRequestCode) {
val callback = pendingResult
pendingResult = null
callback?.invoke(Result.success(resultCode == Activity.RESULT_OK))
return true
}
return false
}
private fun failPending(code: String, message: String) {
val callback = pendingResult ?: return
pendingResult = null
callback(Result.failure(FlutterError(code, message, null)))
}
}
@@ -553,6 +553,7 @@ interface NativeSyncApi {
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit) fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
fun cancelHashing() fun cancelHashing()
fun getTrashedAssets(): Map<String, List<PlatformAsset>> fun getTrashedAssets(): Map<String, List<PlatformAsset>>
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit)
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult> fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult>
companion object { companion object {
@@ -747,6 +748,27 @@ interface NativeSyncApi {
channel.setMessageHandler(null) channel.setMessageHandler(null)
} }
} }
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val mediaIdArg = args[0] as String
val typeArg = args[1] as Long
api.restoreFromTrashById(mediaIdArg, typeArg) { result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run { run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$separatedMessageChannelSuffix", codec, taskQueue) val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) { if (api != null) {
@@ -17,6 +17,8 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.ImageHeaderParser import com.bumptech.glide.load.ImageHeaderParser
import com.bumptech.glide.load.ImageHeaderParserUtils import com.bumptech.glide.load.ImageHeaderParserUtils
import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -39,10 +41,11 @@ sealed class AssetResult {
private const val TAG = "NativeSyncApiImplBase" private const val TAG = "NativeSyncApiImplBase"
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() { open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAware {
private val ctx: Context = context.applicationContext private val ctx: Context = context.applicationContext
private var hashTask: Job? = null private var hashTask: Job? = null
private val mediaTrashDelegate = MediaTrashDelegate(ctx)
companion object { companion object {
private const val MAX_CONCURRENT_HASH_OPERATIONS = 16 private const val MAX_CONCURRENT_HASH_OPERATIONS = 16
@@ -448,6 +451,26 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
hashTask = null hashTask = null
} }
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit) {
mediaTrashDelegate.restoreFromTrashById(mediaId, type) { completeWhenActive(callback, it) }
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
mediaTrashDelegate.onAttachedToActivity(binding)
}
override fun onDetachedFromActivityForConfigChanges() {
mediaTrashDelegate.onDetachedFromActivity()
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
mediaTrashDelegate.onAttachedToActivity(binding)
}
override fun onDetachedFromActivity() {
mediaTrashDelegate.onDetachedFromActivity()
}
// This method is only implemented on iOS; on Android, we do not have a concept of cloud IDs // This method is only implemented on iOS; on Android, we do not have a concept of cloud IDs
@Suppress("unused", "UNUSED_PARAMETER") @Suppress("unused", "UNUSED_PARAMETER")
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult> { fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult> {
+19 -6
View File
@@ -19,6 +19,8 @@
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; }; B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; };
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; }; B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; };
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */; }; B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */; };
B2EE00022E72CA15008B6CA7 /* PermissionApi.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */; };
B2EE00042E72CA15008B6CA7 /* PermissionApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */; };
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; }; B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; };
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; }; D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; };
F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
@@ -105,6 +107,8 @@
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; }; B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = "<group>"; }; B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = "<group>"; };
B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityApiImpl.swift; sourceTree = "<group>"; }; B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityApiImpl.swift; sourceTree = "<group>"; };
B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApi.g.swift; sourceTree = "<group>"; };
B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApiImpl.swift; sourceTree = "<group>"; };
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; }; B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; };
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -283,6 +287,7 @@
B25D37792E72CA15008B6CA7 /* Connectivity */, B25D37792E72CA15008B6CA7 /* Connectivity */,
B21E34A62E5AF9760031FDB9 /* Background */, B21E34A62E5AF9760031FDB9 /* Background */,
B2CF7F8C2DDE4EBB00744BF6 /* Sync */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
B2EE00052E72CA15008B6CA7 /* Permission */,
FA9973382CF6DF4B000EF859 /* Runner.entitlements */, FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */, FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FA1CF9000F007C117D /* Main.storyboard */,
@@ -317,6 +322,15 @@
path = Connectivity; path = Connectivity;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
B2EE00052E72CA15008B6CA7 /* Permission */ = {
isa = PBXGroup;
children = (
B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */,
B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */,
);
path = Permission;
sourceTree = "<group>";
};
FAC6F8B62D287F120078CB2F /* ShareExtension */ = { FAC6F8B62D287F120078CB2F /* ShareExtension */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -619,6 +633,8 @@
FE5499F42F1197D8006016CB /* RemoteImages.g.swift in Sources */, FE5499F42F1197D8006016CB /* RemoteImages.g.swift in Sources */,
FE5FE4AE2F30FBC000A71243 /* ImageProcessing.swift in Sources */, FE5FE4AE2F30FBC000A71243 /* ImageProcessing.swift in Sources */,
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */, B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */,
B2EE00022E72CA15008B6CA7 /* PermissionApi.g.swift in Sources */,
B2EE00042E72CA15008B6CA7 /* PermissionApiImpl.swift in Sources */,
FE5499F82F1198E2006016CB /* RemoteImagesImpl.swift in Sources */, FE5499F82F1198E2006016CB /* RemoteImagesImpl.swift in Sources */,
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */, FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */,
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */, B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */,
@@ -718,6 +734,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CUSTOM_GROUP_ID = group.app.immich.share.profile;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -750,7 +767,6 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240; CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2W7AC6T8T5; DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -801,6 +817,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CUSTOM_GROUP_ID = group.app.immich.share.debug;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
@@ -860,6 +877,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CUSTOM_GROUP_ID = group.app.immich.share;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -894,7 +912,6 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240; CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2W7AC6T8T5; DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -924,7 +941,6 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240; CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2W7AC6T8T5; DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -1080,7 +1096,6 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240; CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2W7AC6T8T5; DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1124,7 +1139,6 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240; CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2W7AC6T8T5; DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1165,7 +1179,6 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240; CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2W7AC6T8T5; DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
+1
View File
@@ -26,6 +26,7 @@ import native_video_player
public static func registerPlugins(with registry: FlutterPluginRegistry, messenger: FlutterBinaryMessenger) { public static func registerPlugins(with registry: FlutterPluginRegistry, messenger: FlutterBinaryMessenger) {
NativeSyncApiImpl.register(with: registry.registrar(forPlugin: NativeSyncApiImpl.name)!) NativeSyncApiImpl.register(with: registry.registrar(forPlugin: NativeSyncApiImpl.name)!)
PermissionApiSetup.setUp(binaryMessenger: messenger, api: PermissionApiImpl())
LocalImageApiSetup.setUp(binaryMessenger: messenger, api: LocalImageApiImpl()) LocalImageApiSetup.setUp(binaryMessenger: messenger, api: LocalImageApiImpl())
RemoteImageApiSetup.setUp(binaryMessenger: messenger, api: RemoteImageApiImpl()) RemoteImageApiSetup.setUp(binaryMessenger: messenger, api: RemoteImageApiImpl())
BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: messenger, api: BackgroundWorkerApiImpl()) BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: messenger, api: BackgroundWorkerApiImpl())
+14
View File
@@ -288,6 +288,7 @@ protocol NetworkApi {
func hasCertificate() throws -> Bool func hasCertificate() throws -> Bool
func getClientPointer() throws -> Int64 func getClientPointer() throws -> Int64
func setRequestHeaders(headers: [String: String], serverUrls: [String], token: String?) throws func setRequestHeaders(headers: [String: String], serverUrls: [String], token: String?) throws
func getAppGroupId() throws -> String
} }
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -388,5 +389,18 @@ class NetworkApiSetup {
} else { } else {
setRequestHeadersChannel.setMessageHandler(nil) setRequestHeadersChannel.setMessageHandler(nil)
} }
let getAppGroupIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
getAppGroupIdChannel.setMessageHandler { _, reply in
do {
let result = try api.getAppGroupId()
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getAppGroupIdChannel.setMessageHandler(nil)
}
} }
} }
@@ -61,6 +61,10 @@ class NetworkApiImpl: NetworkApi {
return Int64(Int(bitPattern: pointer)) return Int64(Int(bitPattern: pointer))
} }
func getAppGroupId() throws -> String {
return Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String
}
func setRequestHeaders(headers: [String : String], serverUrls: [String], token: String?) throws { func setRequestHeaders(headers: [String : String], serverUrls: [String], token: String?) throws {
URLSessionManager.setServerUrls(serverUrls) URLSessionManager.setServerUrls(serverUrls)
@@ -4,7 +4,7 @@ import native_video_player
let CLIENT_CERT_LABEL = "app.alextran.immich.client_identity" let CLIENT_CERT_LABEL = "app.alextran.immich.client_identity"
let HEADERS_KEY = "immich.request_headers" let HEADERS_KEY = "immich.request_headers"
let SERVER_URLS_KEY = "immich.server_urls" let SERVER_URLS_KEY = "immich.server_urls"
let APP_GROUP = "group.app.immich.share" let APP_GROUP = Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String
let COOKIE_EXPIRY_DAYS: TimeInterval = 400 let COOKIE_EXPIRY_DAYS: TimeInterval = 400
enum AuthCookie: CaseIterable { enum AuthCookie: CaseIterable {
+106
View File
@@ -0,0 +1,106 @@
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
#if os(iOS)
import Flutter
#elseif os(macOS)
import FlutterMacOS
#else
#error("Unsupported platform.")
#endif
private func wrapResult(_ result: Any?) -> [Any?] {
return [result]
}
private func wrapError(_ error: Any) -> [Any?] {
if let pigeonError = error as? PigeonError {
return [
pigeonError.code,
pigeonError.message,
pigeonError.details,
]
}
if let flutterError = error as? FlutterError {
return [
flutterError.code,
flutterError.message,
flutterError.details,
]
}
return [
"\(error)",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
private func isNullish(_ value: Any?) -> Bool {
return value is NSNull || value == nil
}
private func nilOrValue<T>(_ value: Any?) -> T? {
if value is NSNull { return nil }
return value as! T?
}
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol PermissionApi {
func hasManageMediaPermission() throws -> Bool
func requestManageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
func manageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
class PermissionApiSetup {
static var codec: FlutterStandardMessageCodec { FlutterStandardMessageCodec.sharedInstance() }
/// Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`.
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
let hasManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
hasManageMediaPermissionChannel.setMessageHandler { _, reply in
do {
let result = try api.hasManageMediaPermission()
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
hasManageMediaPermissionChannel.setMessageHandler(nil)
}
let requestManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
requestManageMediaPermissionChannel.setMessageHandler { _, reply in
api.requestManageMediaPermission { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
requestManageMediaPermissionChannel.setMessageHandler(nil)
}
let manageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
manageMediaPermissionChannel.setMessageHandler { _, reply in
api.manageMediaPermission { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
manageMediaPermissionChannel.setMessageHandler(nil)
}
}
}
@@ -0,0 +1,15 @@
import Foundation
class PermissionApiImpl: PermissionApi {
func hasManageMediaPermission() throws -> Bool {
return false
}
func requestManageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void) {
completion(.success(false))
}
func manageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void) {
completion(.success(false))
}
}
+1 -1
View File
@@ -10,7 +10,7 @@
<true/> <true/>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.app.immich.share</string> <string>$(CUSTOM_GROUP_ID)</string>
</array> </array>
</dict> </dict>
</plist> </plist>
+1 -1
View File
@@ -12,7 +12,7 @@
<true/> <true/>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.app.immich.share</string> <string>$(CUSTOM_GROUP_ID)</string>
</array> </array>
</dict> </dict>
</plist> </plist>
+19
View File
@@ -537,6 +537,7 @@ protocol NativeSyncApi {
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void) func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
func cancelHashing() throws func cancelHashing() throws
func getTrashedAssets() throws -> [String: [PlatformAsset]] func getTrashedAssets() throws -> [String: [PlatformAsset]]
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void)
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult] func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult]
} }
@@ -721,6 +722,24 @@ class NativeSyncApiSetup {
} else { } else {
getTrashedAssetsChannel.setMessageHandler(nil) getTrashedAssetsChannel.setMessageHandler(nil)
} }
let restoreFromTrashByIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
restoreFromTrashByIdChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let mediaIdArg = args[0] as! String
let typeArg = args[1] as! Int64
api.restoreFromTrashById(mediaId: mediaIdArg, type: typeArg) { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
restoreFromTrashByIdChannel.setMessageHandler(nil)
}
let getCloudIdForAssetIdsChannel = taskQueue == nil let getCloudIdForAssetIdsChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
@@ -383,6 +383,10 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil) throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil)
} }
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void) {
completion(.success(false))
}
private func getAssetsFromAlbum(in album: PHAssetCollection, options: PHFetchOptions) -> PHFetchResult<PHAsset> { private func getAssetsFromAlbum(in album: PHAssetCollection, options: PHFetchOptions) -> PHFetchResult<PHAsset> {
// Ensure to actually getting all assets for the Recents album // Ensure to actually getting all assets for the Recents album
if (album.assetCollectionSubtype == .smartAlbumUserLibrary) { if (album.assetCollectionSubtype == .smartAlbumUserLibrary) {
@@ -4,7 +4,7 @@
<dict> <dict>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.app.immich.share</string> <string>$(CUSTOM_GROUP_ID)</string>
</array> </array>
</dict> </dict>
</plist> </plist>
+1 -1
View File
@@ -2,7 +2,7 @@ import Foundation
import SwiftUI import SwiftUI
import WidgetKit import WidgetKit
let IMMICH_SHARE_GROUP = "group.app.immich.share" let IMMICH_SHARE_GROUP = Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String
enum WidgetError: Error, Codable { enum WidgetError: Error, Codable {
case noLogin case noLogin
+2
View File
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
@@ -4,7 +4,7 @@
<dict> <dict>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.app.immich.share</string> <string>$(CUSTOM_GROUP_ID)</string>
</array> </array>
</dict> </dict>
</plist> </plist>
+14 -4
View File
@@ -21,6 +21,7 @@ platform :ios do
CODE_SIGN_IDENTITY = "Apple Distribution: FUTO Holdings, Inc. (#{TEAM_ID})" CODE_SIGN_IDENTITY = "Apple Distribution: FUTO Holdings, Inc. (#{TEAM_ID})"
BASE_BUNDLE_ID = "app.alextran.immich" BASE_BUNDLE_ID = "app.alextran.immich"
DEV_BUNDLE_ID = "tech.futo.immich.testflight" DEV_BUNDLE_ID = "tech.futo.immich.testflight"
DEV_GROUP_ID = "group.app.immich.share.testflight"
# Helper method to get App Store Connect API key # Helper method to get App Store Connect API key
def get_api_key def get_api_key
@@ -33,6 +34,13 @@ platform :ios do
) )
end end
# Helper method to assemble xcargs with optional CUSTOM_GROUP_ID override
def build_xcargs(group_id: nil)
args = "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual"
args += " CUSTOM_GROUP_ID='#{group_id}'" if group_id
args
end
# Helper method to get version from pubspec.yaml # Helper method to get version from pubspec.yaml
def get_version_from_pubspec def get_version_from_pubspec
require 'yaml' require 'yaml'
@@ -89,7 +97,8 @@ end
version_number: nil, version_number: nil,
profile_name_main:, profile_name_main:,
profile_name_share:, profile_name_share:,
profile_name_widget: profile_name_widget:,
group_id: nil
) )
app_identifier = base_bundle_id app_identifier = base_bundle_id
@@ -113,7 +122,7 @@ end
workspace: "Runner.xcworkspace", workspace: "Runner.xcworkspace",
configuration: configuration, configuration: configuration,
export_method: "app-store", export_method: "app-store",
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual", xcargs: build_xcargs(group_id: group_id),
export_options: { export_options: {
provisioningProfiles: { provisioningProfiles: {
"#{app_identifier}" => profile_name_main, "#{app_identifier}" => profile_name_main,
@@ -165,7 +174,8 @@ end
distribute_external: false, distribute_external: false,
profile_name_main: main_profile_name, profile_name_main: main_profile_name,
profile_name_share: share_profile_name, profile_name_share: share_profile_name,
profile_name_widget: widget_profile_name profile_name_widget: widget_profile_name,
group_id: DEV_GROUP_ID
) )
end end
@@ -274,7 +284,7 @@ end
configuration: "Release", configuration: "Release",
export_method: "app-store", export_method: "app-store",
skip_package_ipa: true, skip_package_ipa: true,
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual", xcargs: build_xcargs(group_id: DEV_GROUP_ID),
export_options: { export_options: {
provisioningProfiles: { provisioningProfiles: {
DEV_BUNDLE_ID => main_profile_name, DEV_BUNDLE_ID => main_profile_name,
-1
View File
@@ -30,7 +30,6 @@ const int kTimelineAssetLoadBatchSize = 1024;
const int kTimelineAssetLoadOppositeSize = 64; const int kTimelineAssetLoadOppositeSize = 64;
// Widget keys // Widget keys
const String appShareGroupId = "group.app.immich.share";
const String kWidgetAuthToken = "widget_auth_token"; const String kWidgetAuthToken = "widget_auth_token";
const String kWidgetServerEndpoint = "widget_server_url"; const String kWidgetServerEndpoint = "widget_server_url";
const String kWidgetCustomHeaders = "widget_custom_headers"; const String kWidgetCustomHeaders = "widget_custom_headers";
@@ -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/cleanup_config.dart';
import 'package:immich_mobile/domain/models/config/image_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/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/theme_config.dart';
import 'package:immich_mobile/domain/models/config/timeline_config.dart'; import 'package:immich_mobile/domain/models/config/timeline_config.dart';
import 'package:immich_mobile/domain/models/config/viewer_config.dart'; import 'package:immich_mobile/domain/models/config/viewer_config.dart';
import 'package:immich_mobile/domain/models/config/slideshow_config.dart';
class AppConfig { class AppConfig {
final ThemeConfig theme; final ThemeConfig theme;
@@ -14,6 +16,8 @@ class AppConfig {
final ImageConfig image; final ImageConfig image;
final ViewerConfig viewer; final ViewerConfig viewer;
final SlideshowConfig slideshow; final SlideshowConfig slideshow;
final AlbumConfig album;
final BackupConfig backup;
const AppConfig({ const AppConfig({
this.theme = const .new(), this.theme = const .new(),
@@ -23,6 +27,8 @@ class AppConfig {
this.image = const .new(), this.image = const .new(),
this.viewer = const .new(), this.viewer = const .new(),
this.slideshow = const .new(), this.slideshow = const .new(),
this.album = const .new(),
this.backup = const .new(),
}); });
AppConfig copyWith({ AppConfig copyWith({
@@ -33,6 +39,8 @@ class AppConfig {
ImageConfig? image, ImageConfig? image,
ViewerConfig? viewer, ViewerConfig? viewer,
SlideshowConfig? slideshow, SlideshowConfig? slideshow,
AlbumConfig? album,
BackupConfig? backup,
}) => .new( }) => .new(
theme: theme ?? this.theme, theme: theme ?? this.theme,
cleanup: cleanup ?? this.cleanup, cleanup: cleanup ?? this.cleanup,
@@ -41,6 +49,8 @@ class AppConfig {
image: image ?? this.image, image: image ?? this.image,
viewer: viewer ?? this.viewer, viewer: viewer ?? this.viewer,
slideshow: slideshow ?? this.slideshow, slideshow: slideshow ?? this.slideshow,
album: album ?? this.album,
backup: backup ?? this.backup,
); );
@override @override
@@ -53,12 +63,14 @@ class AppConfig {
other.timeline == timeline && other.timeline == timeline &&
other.image == image && other.image == image &&
other.viewer == viewer && other.viewer == viewer &&
other.slideshow == slideshow); other.slideshow == slideshow &&
other.album == album &&
other.backup == backup);
@override @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 @override
String toString() => 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'; import 'package:immich_mobile/domain/models/log.model.dart';
class SystemConfig { class SystemConfig {
final LogLevel logLevel; 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 @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 @override
int get hashCode => logLevel.hashCode; int get hashCode => Object.hash(logLevel, network);
@override @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/config/system_config.dart';
import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/timeline.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> { enum MetadataDomain<T extends Object> {
appConfig<AppConfig>('config.app'), appConfig<AppConfig>('config.app'),
@@ -34,6 +35,41 @@ enum MetadataKey<T extends Object> {
viewerAutoPlayVideo<bool>(.appConfig, 'viewer.autoPlayVideo', true), viewerAutoPlayVideo<bool>(.appConfig, 'viewer.autoPlayVideo', true),
viewerTapToNavigate<bool>(.appConfig, 'viewer.tapToNavigate', false), 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 // Timeline
timelineTilesPerRow<int>(.appConfig, 'timeline.tilesPerRow', 4), timelineTilesPerRow<int>(.appConfig, 'timeline.tilesPerRow', 4),
timelineGroupAssetsBy<GroupAssetsBy>( timelineGroupAssetsBy<GroupAssetsBy>(
@@ -143,6 +179,47 @@ final class _DateTimeCodec extends _MetadataCodec<DateTime> {
DateTime? decode(String raw) => DateTime.tryParse(raw); 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 class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
final _MetadataCodec<T> _elementCodec; final _MetadataCodec<T> _elementCodec;
+1 -2
View File
@@ -1,8 +1,7 @@
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
enum Setting<T> { enum Setting<T> {
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false), advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false);
enableBackup<bool>(StoreKey.enableBackup, false);
const Setting(this.storeKey, this.defaultValue); const Setting(this.storeKey, this.defaultValue);
+14 -20
View File
@@ -6,39 +6,33 @@ enum StoreKey<T> {
version<int>._(0), version<int>._(0),
currentUser<UserDto>._(2), currentUser<UserDto>._(2),
deviceId<String>._(4), deviceId<String>._(4),
backupRequireCharging<bool>._(7),
backupTriggerDelay<int>._(8),
serverUrl<String>._(10), serverUrl<String>._(10),
accessToken<String>._(11), accessToken<String>._(11),
serverEndpoint<String>._(12), serverEndpoint<String>._(12),
selectedAlbumSortOrder<int>._(113),
advancedTroubleshooting<bool>._(114), advancedTroubleshooting<bool>._(114),
selectedAlbumSortReverse<bool>._(123),
enableHapticFeedback<bool>._(126), 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), manageLocalMediaAndroid<bool>._(137),
// Read-only Mode settings // Read-only Mode settings
readonlyModeEnabled<bool>._(138), 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), syncMigrationStatus<String>._(1013),
// Legacy keys that have been migrated to the new metadata store // 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), legacyLoopVideo<bool>._(117),
legacyLoadOriginalVideo<bool>._(136), legacyLoadOriginalVideo<bool>._(136),
legacyAutoPlayVideo<bool>._(139), legacyAutoPlayVideo<bool>._(139),
@@ -11,15 +11,14 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart'; import 'package:immich_mobile/platform/background_worker_api.g.dart';
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.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/infrastructure/platform.provider.dart' show nativeSyncApiProvider;
import 'package:immich_mobile/providers/user.provider.dart'; 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/auth.service.dart';
import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart';
import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/services/localization.service.dart';
@@ -39,16 +38,15 @@ class BackgroundWorkerFgService {
Future<void> saveNotificationMessage(String title, String body) => Future<void> saveNotificationMessage(String title, String body) =>
_foregroundHostApi.saveNotificationMessage(title, body); _foregroundHostApi.saveNotificationMessage(title, body);
Future<void> configure({int? minimumDelaySeconds, bool? requireCharging}) => _foregroundHostApi.configure( Future<void> configure({int? minimumDelaySeconds, bool? requireCharging}) {
BackgroundWorkerSettings( final backup = MetadataRepository.instance.appConfig.backup;
minimumDelaySeconds: return _foregroundHostApi.configure(
minimumDelaySeconds ?? BackgroundWorkerSettings(
Store.get(AppSettingsEnum.backupTriggerDelay.storeKey, AppSettingsEnum.backupTriggerDelay.defaultValue), minimumDelaySeconds: minimumDelaySeconds ?? backup.triggerDelay,
requiresCharging: requiresCharging: requireCharging ?? backup.requireCharging,
requireCharging ?? ),
Store.get(AppSettingsEnum.backupRequireCharging.storeKey, AppSettingsEnum.backupRequireCharging.defaultValue), );
), }
);
Future<void> disable() => _foregroundHostApi.disable(); Future<void> disable() => _foregroundHostApi.disable();
} }
@@ -71,7 +69,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
BackgroundWorkerFlutterApi.setUp(this); 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 { Future<void> init() async {
try { try {
@@ -9,10 +9,10 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/permission.repository.dart';
import 'package:immich_mobile/utils/datetime_helpers.dart'; import 'package:immich_mobile/utils/datetime_helpers.dart';
import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/diff.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@@ -23,29 +23,29 @@ class LocalSyncService {
final DriftLocalAssetRepository _localAssetRepository; final DriftLocalAssetRepository _localAssetRepository;
final NativeSyncApi _nativeSyncApi; final NativeSyncApi _nativeSyncApi;
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final LocalFilesManagerRepository _localFilesManager; final AssetMediaRepository _assetMediaRepository;
final StorageRepository _storageRepository; final IPermissionRepository _permissionRepository;
final Logger _log = Logger("DeviceSyncService"); final Logger _log = Logger("DeviceSyncService");
LocalSyncService({ LocalSyncService({
required DriftLocalAlbumRepository localAlbumRepository, required DriftLocalAlbumRepository localAlbumRepository,
required DriftLocalAssetRepository localAssetRepository, required DriftLocalAssetRepository localAssetRepository,
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository, required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required LocalFilesManagerRepository localFilesManager, required AssetMediaRepository assetMediaRepository,
required StorageRepository storageRepository, required IPermissionRepository permissionRepository,
required NativeSyncApi nativeSyncApi, required NativeSyncApi nativeSyncApi,
}) : _localAlbumRepository = localAlbumRepository, }) : _localAlbumRepository = localAlbumRepository,
_localAssetRepository = localAssetRepository, _localAssetRepository = localAssetRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository, _trashedLocalAssetRepository = trashedLocalAssetRepository,
_localFilesManager = localFilesManager, _assetMediaRepository = assetMediaRepository,
_storageRepository = storageRepository, _permissionRepository = permissionRepository,
_nativeSyncApi = nativeSyncApi; _nativeSyncApi = nativeSyncApi;
Future<void> sync({bool full = false}) async { Future<void> sync({bool full = false}) async {
final Stopwatch stopwatch = Stopwatch()..start(); final Stopwatch stopwatch = Stopwatch()..start();
try { try {
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
final hasPermission = await _localFilesManager.hasManageMediaPermission(); final hasPermission = await _permissionRepository.hasManageMediaPermission();
if (hasPermission) { if (hasPermission) {
await _syncTrashedAssets(); await _syncTrashedAssets();
} else { } else {
@@ -373,7 +373,7 @@ class LocalSyncService {
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore(); final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
if (assetsToRestore.isNotEmpty) { if (assetsToRestore.isNotEmpty) {
final restoredIds = await _localFilesManager.restoreAssetsFromTrash(assetsToRestore); final restoredIds = await _assetMediaRepository.restoreAssetsFromTrash(assetsToRestore);
await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds); await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
} else { } else {
_log.info("syncTrashedAssets, No remote assets found for restoration"); _log.info("syncTrashedAssets, No remote assets found for restoration");
@@ -381,15 +381,15 @@ class LocalSyncService {
final localAssetsToTrash = await _trashedLocalAssetRepository.getToTrash(); final localAssetsToTrash = await _trashedLocalAssetRepository.getToTrash();
if (localAssetsToTrash.isNotEmpty) { if (localAssetsToTrash.isNotEmpty) {
final mediaUrls = await Future.wait( final localIds = localAssetsToTrash.values.expand((assets) => assets).map((asset) => asset.id).toList();
localAssetsToTrash.values _log.info("Moving to trash ${localIds.join(", ")} assets");
.expand((e) => e) final movedIds = await _assetMediaRepository.deleteAll(localIds);
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())), if (movedIds.isNotEmpty) {
); final movedAssetsByAlbum = localAssetsToTrash.map(
_log.info("Moving to trash ${mediaUrls.join(", ")} assets"); (albumId, assets) => MapEntry(albumId, assets.where((asset) => movedIds.contains(asset.id)).toList()),
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList()); )..removeWhere((_, assets) => assets.isEmpty);
if (result) {
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash); await _trashedLocalAssetRepository.trashLocalAsset(movedAssetsByAlbum);
} }
} else { } else {
_log.info("syncTrashedAssets, No assets found in backup-enabled albums for move to trash"); _log.info("syncTrashedAssets, No assets found in backup-enabled albums for move to trash");
@@ -9,12 +9,12 @@ import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/permission.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/semver.dart'; import 'package:immich_mobile/utils/semver.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@@ -34,8 +34,8 @@ class SyncStreamService {
final SyncStreamRepository _syncStreamRepository; final SyncStreamRepository _syncStreamRepository;
final DriftLocalAssetRepository _localAssetRepository; final DriftLocalAssetRepository _localAssetRepository;
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
final LocalFilesManagerRepository _localFilesManager; final AssetMediaRepository _assetMediaRepository;
final StorageRepository _storageRepository; final IPermissionRepository _permissionRepository;
final SyncMigrationRepository _syncMigrationRepository; final SyncMigrationRepository _syncMigrationRepository;
final ApiService _api; final ApiService _api;
final bool Function()? _cancelChecker; final bool Function()? _cancelChecker;
@@ -45,8 +45,8 @@ class SyncStreamService {
required SyncStreamRepository syncStreamRepository, required SyncStreamRepository syncStreamRepository,
required DriftLocalAssetRepository localAssetRepository, required DriftLocalAssetRepository localAssetRepository,
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository, required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required LocalFilesManagerRepository localFilesManager, required AssetMediaRepository assetMediaRepository,
required StorageRepository storageRepository, required IPermissionRepository permissionRepository,
required SyncMigrationRepository syncMigrationRepository, required SyncMigrationRepository syncMigrationRepository,
required ApiService api, required ApiService api,
bool Function()? cancelChecker, bool Function()? cancelChecker,
@@ -54,8 +54,8 @@ class SyncStreamService {
_syncStreamRepository = syncStreamRepository, _syncStreamRepository = syncStreamRepository,
_localAssetRepository = localAssetRepository, _localAssetRepository = localAssetRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository, _trashedLocalAssetRepository = trashedLocalAssetRepository,
_localFilesManager = localFilesManager, _assetMediaRepository = assetMediaRepository,
_storageRepository = storageRepository, _permissionRepository = permissionRepository,
_syncMigrationRepository = syncMigrationRepository, _syncMigrationRepository = syncMigrationRepository,
_api = api, _api = api,
_cancelChecker = cancelChecker; _cancelChecker = cancelChecker;
@@ -500,22 +500,22 @@ class SyncStreamService {
} }
Future<void> _trashLocalAssets(Map<String, List<LocalAsset>> localAssetsToTrash) async { Future<void> _trashLocalAssets(Map<String, List<LocalAsset>> localAssetsToTrash) async {
final mediaUrls = await Future.wait( final localIds = localAssetsToTrash.values.expand((assets) => assets).map((asset) => asset.id).toList();
localAssetsToTrash.values _logger.info("Moving to trash ${localIds.join(", ")} assets");
.expand((e) => e) final movedIds = await _assetMediaRepository.deleteAll(localIds);
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())), if (movedIds.isNotEmpty) {
); final movedAssetsByAlbum = localAssetsToTrash.map(
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets"); (albumId, assets) => MapEntry(albumId, assets.where((asset) => movedIds.contains(asset.id)).toList()),
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList()); )..removeWhere((_, assets) => assets.isEmpty);
if (result) {
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash); await _trashedLocalAssetRepository.trashLocalAsset(movedAssetsByAlbum);
} }
} }
Future<void> _applyRemoteRestoreToLocal() async { Future<void> _applyRemoteRestoreToLocal() async {
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore(); final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
if (assetsToRestore.isNotEmpty) { if (assetsToRestore.isNotEmpty) {
final restoredIds = await _localFilesManager.restoreAssetsFromTrash(assetsToRestore); final restoredIds = await _assetMediaRepository.restoreAssetsFromTrash(assetsToRestore);
await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds); await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
} else { } else {
_logger.info("No remote assets found for restoration"); _logger.info("No remote assets found for restoration");
@@ -523,7 +523,7 @@ class SyncStreamService {
} }
Future<void> _syncAssetTrashStatus(List<String> remoteIds) async { Future<void> _syncAssetTrashStatus(List<String> remoteIds) async {
if (!(await _localFilesManager.hasManageMediaPermission())) { if (!(await _permissionRepository.hasManageMediaPermission())) {
_logger.warning("Syncing asset trash status cannot proceed because MANAGE_MEDIA permission is missing"); _logger.warning("Syncing asset trash status cannot proceed because MANAGE_MEDIA permission is missing");
return; return;
} }
@@ -533,7 +533,7 @@ class SyncStreamService {
} }
Future<void> _syncAssetDeletion(List<String> remoteIds) async { Future<void> _syncAssetDeletion(List<String> remoteIds) async {
if (!(await _localFilesManager.hasManageMediaPermission())) { if (!(await _permissionRepository.hasManageMediaPermission())) {
_logger.warning("Syncing asset deletion cannot proceed because MANAGE_MEDIA permission is missing"); _logger.warning("Syncing asset deletion cannot proceed because MANAGE_MEDIA permission is missing");
return; return;
} }
@@ -4,6 +4,8 @@ extension StringExtension on String {
String capitalize() { String capitalize() {
return split(" ").map((str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1)).join(" "); return split(" ").map((str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1)).join(" ");
} }
String? get nullIfEmpty => isEmpty ? null : this;
} }
extension DurationExtension on String { 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/app_config.dart';
import 'package:immich_mobile/domain/models/config/system_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/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/entities/metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.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), look: repo._read(.slideshowLook),
direction: repo._read(.slideshowDirection), 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: 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),
),
);
} }
} }
} }
@@ -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/domain/services/sync_linked_album.service.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/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/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart'; import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
import 'package:immich_mobile/widgets/common/search_field.dart'; import 'package:immich_mobile/widgets/common/search_field.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@@ -43,7 +43,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
_searchController = TextEditingController(); _searchController = TextEditingController();
_searchFocusNode = FocusNode(); _searchFocusNode = FocusNode();
_enableSyncUploadAlbum.value = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); _enableSyncUploadAlbum.value = ref.read(metadataProvider).appConfig.backup.syncAlbums;
ref.read(backupAlbumProvider.notifier).getAll(); ref.read(backupAlbumProvider.notifier).getAll();
_initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount)); _initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
@@ -55,7 +55,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
return; return;
} }
final enableSyncUploadAlbum = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); final enableSyncUploadAlbum = ref.read(metadataProvider).appConfig.backup.syncAlbums;
final selectedAlbums = ref final selectedAlbums = ref
.read(backupAlbumProvider) .read(backupAlbumProvider)
.where((a) => a.backupSelection == BackupSelection.selected) .where((a) => a.backupSelection == BackupSelection.selected)
@@ -103,7 +103,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
return; return;
} }
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled;
await ref.read(driftBackupProvider.notifier).getBackupStatus(user.id); await ref.read(driftBackupProvider.notifier).getBackupStatus(user.id);
final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount)); final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
final totalChanged = currentTotalAssetCount != _initialTotalAssetCount; final totalChanged = currentTotalAssetCount != _initialTotalAssetCount;
@@ -3,14 +3,12 @@ import 'dart:async';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/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/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/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/user.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:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@@ -21,18 +19,20 @@ class DriftBackupOptionsPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
bool hasPopped = false; bool hasPopped = false;
final previousWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false; final previousBackup = ref.read(metadataProvider).appConfig.backup;
final previousWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false; final previousCellularForVideos = previousBackup.useCellularForVideos;
final previousCellularForPhotos = previousBackup.useCellularForPhotos;
return PopScope( return PopScope(
onPopInvokedWithResult: (didPop, result) async { onPopInvokedWithResult: (didPop, result) async {
// There is an issue with Flutter where the pop event // There is an issue with Flutter where the pop event
// can be triggered multiple times, so we guard it with _hasPopped // can be triggered multiple times, so we guard it with _hasPopped
final currentWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false; final currentBackup = ref.read(metadataProvider).appConfig.backup;
final currentWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false; final currentCellularForVideos = currentBackup.useCellularForVideos;
final currentCellularForPhotos = currentBackup.useCellularForPhotos;
if (currentWifiReqForVideos == previousWifiReqForVideos && if (currentCellularForVideos == previousCellularForVideos &&
currentWifiReqForPhotos == previousWifiReqForPhotos) { currentCellularForPhotos == previousCellularForPhotos) {
return; return;
} }
@@ -45,7 +45,7 @@ class DriftBackupOptionsPage extends ConsumerWidget {
} }
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled;
if (!isBackupEnabled) { if (!isBackupEnabled) {
return; return;
} }
@@ -1,14 +1,12 @@
import 'dart:convert';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
class SettingsHeader { class SettingsHeader {
String key = ""; String key = "";
@@ -24,17 +22,14 @@ class HeaderSettingsPage extends HookConsumerWidget {
final headers = useState<List<SettingsHeader>>([]); final headers = useState<List<SettingsHeader>>([]);
final setInitialHeaders = useState(false); final setInitialHeaders = useState(false);
var headersStr = Store.get(StoreKey.customHeaders, ""); final storedHeaders = ref.read(metadataProvider).systemConfig.network.customHeaders;
if (!setInitialHeaders.value) { if (!setInitialHeaders.value) {
if (headersStr.isNotEmpty) { storedHeaders.forEach((k, v) {
var customHeaders = jsonDecode(headersStr) as Map; final header = SettingsHeader();
customHeaders.forEach((k, v) { header.key = k;
final header = SettingsHeader(); header.value = v;
header.key = k; headers.value.add(header);
header.value = v; });
headers.value.add(header);
});
}
// add first one to help the user // add first one to help the user
if (headers.value.isEmpty) { if (headers.value.isEmpty) {
@@ -88,8 +83,8 @@ class HeaderSettingsPage extends HookConsumerWidget {
} }
saveHeaders(WidgetRef ref, List<SettingsHeader> headers) async { saveHeaders(WidgetRef ref, List<SettingsHeader> headers) async {
final headersMap = {}; final headersMap = <String, String>{};
for (var header in headers) { for (final header in headers) {
final key = header.key.trim(); final key = header.key.trim();
final value = header.value.trim(); final value = header.value.trim();
@@ -99,8 +94,7 @@ class HeaderSettingsPage extends HookConsumerWidget {
headersMap[key] = value; headersMap[key] = value;
} }
var encoded = jsonEncode(headersMap); await ref.read(metadataProvider).write(MetadataKey.networkCustomHeaders, headersMap);
await Store.put(StoreKey.customHeaders, encoded);
await ref.read(apiServiceProvider).updateHeaders(); 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/entities/store.entity.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
@@ -340,7 +341,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
await backgroundManager.hashAssets(); await backgroundManager.hashAssets();
} }
if (Store.get(StoreKey.syncAlbums, false)) { if (MetadataRepository.instance.appConfig.backup.syncAlbums) {
await backgroundManager.syncLinkedAlbum(); await backgroundManager.syncLinkedAlbum();
} }
} catch (e) { } catch (e) {
@@ -369,7 +370,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
} }
Future<void> _resumeBackup(DriftBackupNotifier notifier) async { Future<void> _resumeBackup(DriftBackupNotifier notifier) async {
final isEnableBackup = Store.get(StoreKey.enableBackup, false); final isEnableBackup = MetadataRepository.instance.appConfig.backup.enabled;
if (isEnableBackup) { if (isEnableBackup) {
final currentUser = Store.tryGet(StoreKey.currentUser); final currentUser = Store.tryGet(StoreKey.currentUser);
+19
View File
@@ -654,6 +654,25 @@ class NativeSyncApi {
return (pigeonVar_replyValue! as Map<Object?, Object?>).cast<String, List<PlatformAsset>>(); return (pigeonVar_replyValue! as Map<Object?, Object?>).cast<String, List<PlatformAsset>>();
} }
Future<bool> restoreFromTrashById(String mediaId, int type) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[mediaId, type]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return pigeonVar_replyValue! as bool;
}
Future<List<CloudIdResult>> getCloudIdForAssetIds(List<String> assetIds) async { Future<List<CloudIdResult>> getCloudIdForAssetIds(List<String> assetIds) async {
final pigeonVar_channelName = final pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$pigeonVar_messageChannelSuffix'; 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$pigeonVar_messageChannelSuffix';
+19
View File
@@ -309,4 +309,23 @@ class NetworkApi {
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
} }
Future<String> getAppGroupId() async {
final pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return pigeonVar_replyValue! as String;
}
} }
+119
View File
@@ -0,0 +1,119 @@
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: unused_import, unused_shown_name
// ignore_for_file: type=lint
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List;
import 'package:flutter/services.dart';
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName, {required bool isNullValid}) {
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel: "$channelName".',
);
} else if (replyList.length > 1) {
throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]);
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
}
return replyList.firstOrNull;
}
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
default:
return super.readValueOfType(type, buffer);
}
}
}
class PermissionApi {
/// Constructor for [PermissionApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
PermissionApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
Future<bool> hasManageMediaPermission() async {
final pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return pigeonVar_replyValue! as bool;
}
Future<bool> requestManageMediaPermission() async {
final pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return pigeonVar_replyValue! as bool;
}
Future<bool> manageMediaPermission() async {
final pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return pigeonVar_replyValue! as bool;
}
}
@@ -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/album_tile.dart';
import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/album_filter.utils.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/confirm_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -58,19 +58,11 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final appSettings = ref.read(appSettingsServiceProvider); final albumConfig = ref.read(metadataProvider).appConfig.album;
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,
);
setState(() { setState(() {
sort = AlbumSort(mode: albumSortMode, isReverse: savedIsReverse); sort = AlbumSort(mode: albumConfig.sortMode, isReverse: albumConfig.isReverse);
isGrid = savedIsGrid; isGrid = albumConfig.isGrid;
}); });
ref.read(remoteAlbumProvider.notifier).refresh(); ref.read(remoteAlbumProvider.notifier).refresh();
@@ -102,7 +94,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
setState(() { setState(() {
isGrid = !isGrid; isGrid = !isGrid;
}); });
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.albumGridView, isGrid); ref.read(metadataProvider).write(MetadataKey.albumIsGrid, isGrid);
} }
void changeFilter(QuickFilterMode mode) { void changeFilter(QuickFilterMode mode) {
@@ -118,9 +110,9 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
this.sort = sort; this.sort = sort;
}); });
final appSettings = ref.read(appSettingsServiceProvider); final metadata = ref.read(metadataProvider);
await appSettings.setSetting(AppSettingsEnum.selectedAlbumSortOrder, sort.mode.storeIndex); await metadata.write(MetadataKey.albumSortMode, sort.mode);
await appSettings.setSetting(AppSettingsEnum.selectedAlbumSortReverse, sort.isReverse); await metadata.write(MetadataKey.albumIsReverse, sort.isReverse);
await sortAlbums(); await sortAlbums();
} }
@@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
class BackupToggleButton extends ConsumerStatefulWidget { class BackupToggleButton extends ConsumerStatefulWidget {
final VoidCallback onStart; final VoidCallback onStart;
@@ -31,7 +31,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
end: 1, end: 1,
).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut)); ).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_isEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); _isEnabled = ref.read(metadataProvider).appConfig.backup.enabled;
} }
@override @override
@@ -41,7 +41,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
} }
Future<void> _onToggle(bool value) async { Future<void> _onToggle(bool value) async {
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.enableBackup, value); await ref.read(metadataProvider).write(MetadataKey.backupEnabled, value);
setState(() { setState(() {
_isEnabled = value; _isEnabled = value;
@@ -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/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/notification_permission.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
enum AppLifeCycleEnum { active, inactive, paused, resumed, detached, hidden } enum AppLifeCycleEnum { active, inactive, paused, resumed, detached, hidden }
@@ -108,7 +107,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
final backgroundManager = _ref.read(backgroundSyncProvider); final backgroundManager = _ref.read(backgroundSyncProvider);
final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); final isAlbumLinkedSyncEnable = _ref.read(metadataProvider).appConfig.backup.syncAlbums;
try { try {
bool syncSuccess = false; bool syncSuccess = false;
@@ -138,7 +137,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
} }
Future<void> _resumeBackup() async { Future<void> _resumeBackup() async {
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); final isEnableBackup = _ref.read(metadataProvider).appConfig.backup.enabled;
if (isEnableBackup) { if (isEnableBackup) {
final currentUser = Store.tryGet(StoreKey.currentUser); 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:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart';
@@ -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/auth_state.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/services/auth.service.dart';
@@ -126,7 +130,8 @@ class AuthNotifier extends StateNotifier<AuthState> {
await _apiService.updateHeaders(); await _apiService.updateHeaders();
final serverEndpoint = Store.get(StoreKey.serverEndpoint); 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); await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders);
// Get the deviceid from the store if it exists, otherwise generate a new one // 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 { 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 { Future<void> saveLocalEndpoint(String url) async {
await Store.put(StoreKey.localEndpoint, url); await _ref.read(metadataProvider).write(MetadataKey.networkLocalEndpoint, url);
} }
String? getSavedWifiName() { String? getSavedWifiName() {
return Store.tryGet(StoreKey.preferredWifiName); return _ref.read(metadataProvider).systemConfig.network.preferredWifiName;
} }
String? getSavedLocalEndpoint() { 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 /// Returns the current server endpoint (with /api) URL from the store
@@ -3,9 +3,10 @@ import 'package:immich_mobile/domain/services/background_worker.service.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart'; import 'package:immich_mobile/platform/background_worker_api.g.dart';
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
import 'package:immich_mobile/platform/connectivity_api.g.dart'; import 'package:immich_mobile/platform/connectivity_api.g.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/platform/local_image_api.g.dart'; import 'package:immich_mobile/platform/local_image_api.g.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/platform/network_api.g.dart'; import 'package:immich_mobile/platform/network_api.g.dart';
import 'package:immich_mobile/platform/permission_api.g.dart';
import 'package:immich_mobile/platform/remote_image_api.g.dart'; import 'package:immich_mobile/platform/remote_image_api.g.dart';
final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi())); final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
@@ -16,6 +17,8 @@ final backgroundWorkerLockServiceProvider = Provider<BackgroundWorkerLockService
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi()); final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
final permissionApiProvider = Provider<PermissionApi>((_) => PermissionApi());
final connectivityApiProvider = Provider<ConnectivityApi>((_) => ConnectivityApi()); final connectivityApiProvider = Provider<ConnectivityApi>((_) => ConnectivityApi());
final localImageApi = LocalImageApi(); final localImageApi = LocalImageApi();
@@ -11,8 +11,8 @@ import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/repositories/permission.repository.dart';
final syncMigrationRepositoryProvider = Provider((ref) => SyncMigrationRepository(ref.watch(driftProvider))); final syncMigrationRepositoryProvider = Provider((ref) => SyncMigrationRepository(ref.watch(driftProvider)));
@@ -22,8 +22,8 @@ final syncStreamServiceProvider = Provider(
syncStreamRepository: ref.watch(syncStreamRepositoryProvider), syncStreamRepository: ref.watch(syncStreamRepositoryProvider),
localAssetRepository: ref.watch(localAssetRepository), localAssetRepository: ref.watch(localAssetRepository),
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository), trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
localFilesManager: ref.watch(localFilesManagerRepositoryProvider), assetMediaRepository: ref.watch(assetMediaRepositoryProvider),
storageRepository: ref.watch(storageRepositoryProvider), permissionRepository: ref.watch(permissionRepositoryProvider),
syncMigrationRepository: ref.watch(syncMigrationRepositoryProvider), syncMigrationRepository: ref.watch(syncMigrationRepositoryProvider),
api: ref.watch(apiServiceProvider), api: ref.watch(apiServiceProvider),
cancelChecker: ref.watch(cancellationProvider), cancelChecker: ref.watch(cancellationProvider),
@@ -39,8 +39,8 @@ final localSyncServiceProvider = Provider(
localAlbumRepository: ref.watch(localAlbumRepository), localAlbumRepository: ref.watch(localAlbumRepository),
localAssetRepository: ref.watch(localAssetRepository), localAssetRepository: ref.watch(localAssetRepository),
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository), trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
localFilesManager: ref.watch(localFilesManagerRepositoryProvider), assetMediaRepository: ref.watch(assetMediaRepositoryProvider),
storageRepository: ref.watch(storageRepositoryProvider), permissionRepository: ref.watch(permissionRepositoryProvider),
nativeSyncApi: ref.watch(nativeSyncApiProvider), nativeSyncApi: ref.watch(nativeSyncApiProvider),
), ),
); );
+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/models/server_info/server_version.model.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debounce.dart';
import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/debug_print.dart';
@@ -192,7 +193,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
return; return;
} }
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false); final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums;
try { try {
unawaited( unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) { _ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) {
@@ -213,7 +214,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
return; return;
} }
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false); final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums;
try { try {
unawaited( unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) { _ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) {
@@ -8,19 +8,24 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/response_extensions.dart'; import 'package:immich_mobile/extensions/response_extensions.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider))); final assetMediaRepositoryProvider = Provider(
(ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider), ref.watch(nativeSyncApiProvider)),
);
class AssetMediaRepository { class AssetMediaRepository {
final AssetApiRepository _assetApiRepository; final AssetApiRepository _assetApiRepository;
final NativeSyncApi _nativeSyncApi;
static final Logger _log = Logger("AssetMediaRepository"); static final Logger _log = Logger("AssetMediaRepository");
const AssetMediaRepository(this._assetApiRepository); const AssetMediaRepository(this._assetApiRepository, this._nativeSyncApi);
Future<bool> _androidSupportsTrash() async { Future<bool> _androidSupportsTrash() async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
@@ -45,6 +50,27 @@ class AssetMediaRepository {
return PhotoManager.editor.deleteWithIds(ids); return PhotoManager.editor.deleteWithIds(ids);
} }
Future<bool> _restoreFromTrashById(String mediaId, int type) async {
try {
return await _nativeSyncApi.restoreFromTrashById(mediaId, type);
} catch (e, s) {
_log.warning('Error restore file from trash by Id', e, s);
return false;
}
}
Future<List<String>> restoreAssetsFromTrash(Iterable<LocalAsset> assets) async {
final restoredIds = <String>[];
for (final asset in assets) {
_log.info("Restoring from trash, localId: ${asset.id}, checksum: ${asset.checksum}");
final result = await _restoreFromTrashById(asset.id, asset.type.index);
if (result) {
restoredIds.add(asset.id);
}
}
return restoredIds;
}
Future<AssetEntity?> get(String id) async { Future<AssetEntity?> get(String id) async {
final entity = await AssetEntity.fromId(id); final entity = await AssetEntity.fromId(id);
return entity; return entity;
+13 -19
View File
@@ -1,46 +1,40 @@
import 'dart:convert';
import 'package:hooks_riverpod/hooks_riverpod.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/infrastructure/repositories/db.repository.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/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
final authRepositoryProvider = Provider<AuthRepository>((ref) => AuthRepository(ref.watch(driftProvider))); final authRepositoryProvider = Provider<AuthRepository>(
(ref) => AuthRepository(ref.watch(driftProvider), ref.watch(metadataProvider)),
);
class AuthRepository { class AuthRepository {
final Drift _drift; final Drift _drift;
final MetadataRepository _metadata;
const AuthRepository(this._drift); const AuthRepository(this._drift, this._metadata);
Future<void> clearLocalData() async { Future<void> clearLocalData() async {
await SyncStreamRepository(_drift).reset(); await SyncStreamRepository(_drift).reset();
} }
bool getEndpointSwitchingFeature() { bool getEndpointSwitchingFeature() {
return Store.tryGet(StoreKey.autoEndpointSwitching) ?? false; return _metadata.systemConfig.network.autoEndpointSwitching;
} }
String? getPreferredWifiName() { String? getPreferredWifiName() {
return Store.tryGet(StoreKey.preferredWifiName); return _metadata.systemConfig.network.preferredWifiName;
} }
String? getLocalEndpoint() { String? getLocalEndpoint() {
return Store.tryGet(StoreKey.localEndpoint); return _metadata.systemConfig.network.localEndpoint;
} }
List<AuxilaryEndpoint> getExternalEndpointList() { List<AuxilaryEndpoint> getExternalEndpointList() {
final jsonString = Store.tryGet(StoreKey.externalEndpointList); return _metadata.systemConfig.network.externalEndpointList
.map((url) => AuxilaryEndpoint(url: url, status: .valid))
if (jsonString == null) { .toList();
return [];
}
final List<dynamic> jsonList = jsonDecode(jsonString);
final endpointList = jsonList.map((e) => AuxilaryEndpoint.fromJson(e)).toList();
return endpointList;
} }
} }
@@ -1,51 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/services/local_files_manager.service.dart';
import 'package:logging/logging.dart';
final localFilesManagerRepositoryProvider = Provider(
(ref) => LocalFilesManagerRepository(ref.watch(localFileManagerServiceProvider)),
);
class LocalFilesManagerRepository {
LocalFilesManagerRepository(this._service);
final Logger _logger = Logger('LocalFilesManagerRepo');
final LocalFilesManagerService _service;
Future<bool> moveToTrash(List<String> mediaUrls) async {
return await _service.moveToTrash(mediaUrls);
}
Future<bool> restoreFromTrash(String fileName, int type) async {
return await _service.restoreFromTrash(fileName, type);
}
Future<bool> requestManageMediaPermission() async {
return await _service.requestManageMediaPermission();
}
Future<bool> hasManageMediaPermission() async {
return await _service.hasManageMediaPermission();
}
Future<bool> manageMediaPermission() async {
return await _service.manageMediaPermission();
}
Future<List<String>> restoreAssetsFromTrash(Iterable<LocalAsset> assets) async {
final restoredIds = <String>[];
for (final asset in assets) {
_logger.info("Restoring from trash, localId: ${asset.id}, remoteId: ${asset.checksum}");
try {
final result = await _service.restoreFromTrashById(asset.id, asset.type.index);
if (result) {
restoredIds.add(asset.id);
}
} catch (e) {
_logger.warning("Restoring failure: $e");
}
}
return restoredIds;
}
}
@@ -1,12 +1,16 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/platform/permission_api.g.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
final permissionRepositoryProvider = Provider((_) { final permissionRepositoryProvider = Provider((ref) {
return const PermissionRepository(); return PermissionRepository(ref.watch(permissionApiProvider));
}); });
class PermissionRepository implements IPermissionRepository { class PermissionRepository implements IPermissionRepository {
const PermissionRepository(); final PermissionApi _permissionApi;
const PermissionRepository(this._permissionApi);
@override @override
Future<bool> hasLocationWhenInUsePermission() { Future<bool> hasLocationWhenInUsePermission() {
@@ -34,6 +38,21 @@ class PermissionRepository implements IPermissionRepository {
Future<bool> openSettings() { Future<bool> openSettings() {
return openAppSettings(); return openAppSettings();
} }
@override
Future<bool> hasManageMediaPermission() {
return _permissionApi.hasManageMediaPermission();
}
@override
Future<bool> requestManageMediaPermission() {
return _permissionApi.requestManageMediaPermission();
}
@override
Future<bool> manageMediaPermission() {
return _permissionApi.manageMediaPermission();
}
} }
abstract interface class IPermissionRepository { abstract interface class IPermissionRepository {
@@ -42,4 +61,7 @@ abstract interface class IPermissionRepository {
Future<bool> hasLocationAlwaysPermission(); Future<bool> hasLocationAlwaysPermission();
Future<bool> requestLocationAlwaysPermission(); Future<bool> requestLocationAlwaysPermission();
Future<bool> openSettings(); Future<bool> openSettings();
Future<bool> hasManageMediaPermission();
Future<bool> requestManageMediaPermission();
Future<bool> manageMediaPermission();
} }
@@ -1,5 +1,6 @@
import 'package:home_widget/home_widget.dart'; import 'package:home_widget/home_widget.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
final widgetRepositoryProvider = Provider((_) => const WidgetRepository()); final widgetRepositoryProvider = Provider((_) => const WidgetRepository());
@@ -14,7 +15,7 @@ class WidgetRepository {
await HomeWidget.updateWidget(iOSName: iosName, qualifiedAndroidName: androidName); await HomeWidget.updateWidget(iOSName: iosName, qualifiedAndroidName: androidName);
} }
Future<void> setAppGroupId(String appGroupId) async { Future<void> setAppGroupId() async {
await HomeWidget.setAppGroupId(appGroupId); await HomeWidget.setAppGroupId(await networkApi.getAppGroupId());
} }
} }
+8 -17
View File
@@ -5,8 +5,8 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/utils/url_helper.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@@ -177,30 +177,21 @@ class ApiService {
if (serverEndpoint != null && serverEndpoint.isNotEmpty) { if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
urls.add(serverEndpoint); urls.add(serverEndpoint);
} }
final localEndpoint = Store.tryGet(StoreKey.localEndpoint); final network = MetadataRepository.instance.systemConfig.network;
if (localEndpoint != null && localEndpoint.isNotEmpty) { final localEndpoint = network.localEndpoint;
if (localEndpoint != null) {
urls.add(localEndpoint); urls.add(localEndpoint);
} }
final externalJson = Store.tryGet(StoreKey.externalEndpointList); for (final url in network.externalEndpointList) {
if (externalJson != null) { if (url.isNotEmpty) {
final List<dynamic> list = jsonDecode(externalJson); urls.add(url);
for (final entry in list) {
final url = AuxilaryEndpoint.fromJson(entry).url;
if (url.isNotEmpty) {
urls.add(url);
}
} }
} }
return urls; return urls;
} }
static Map<String, String> getRequestHeaders() { static Map<String, String> getRequestHeaders() {
var customHeadersStr = Store.get(StoreKey.customHeaders, ""); return MetadataRepository.instance.systemConfig.network.customHeaders;
if (customHeadersStr.isEmpty) {
return const {};
}
return (jsonDecode(customHeadersStr) as Map).cast<String, String>();
} }
ApiClient get apiClient => _apiClient; 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'; import 'package:immich_mobile/entities/store.entity.dart';
enum AppSettingsEnum<T> { enum AppSettingsEnum<T> {
selectedAlbumSortOrder<int>(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 2),
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false), advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false), manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
selectedAlbumSortReverse<bool>(StoreKey.selectedAlbumSortReverse, null, true),
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true), enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
syncAlbums<bool>(StoreKey.syncAlbums, null, false), readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", 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);
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
+3 -10
View File
@@ -1,19 +1,19 @@
import 'dart:async'; import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.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/providers/background_sync.provider.dart';
import 'package:immich_mobile/repositories/auth.repository.dart'; import 'package:immich_mobile/repositories/auth.repository.dart';
import 'package:immich_mobile/repositories/auth_api.repository.dart'; import 'package:immich_mobile/repositories/auth_api.repository.dart';
import 'package:immich_mobile/services/api.service.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:immich_mobile/services/network.service.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@@ -25,7 +25,6 @@ final authServiceProvider = Provider(
ref.watch(apiServiceProvider), ref.watch(apiServiceProvider),
ref.watch(networkServiceProvider), ref.watch(networkServiceProvider),
ref.watch(backgroundSyncProvider), ref.watch(backgroundSyncProvider),
ref.watch(appSettingsServiceProvider),
), ),
); );
@@ -35,7 +34,6 @@ class AuthService {
final ApiService _apiService; final ApiService _apiService;
final NetworkService _networkService; final NetworkService _networkService;
final BackgroundSyncManager _backgroundSyncManager; final BackgroundSyncManager _backgroundSyncManager;
final AppSettingsService _appSettingsService;
final _log = Logger("AuthService"); final _log = Logger("AuthService");
AuthService( AuthService(
@@ -44,7 +42,6 @@ class AuthService {
this._apiService, this._apiService,
this._networkService, this._networkService,
this._backgroundSyncManager, this._backgroundSyncManager,
this._appSettingsService,
); );
/// Validates the provided server URL by resolving and setting the endpoint. /// 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); _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(), _authRepository.clearLocalData(),
Store.delete(StoreKey.currentUser), Store.delete(StoreKey.currentUser),
Store.delete(StoreKey.accessToken), 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/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.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/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.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/asset_media.repository.dart';
import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart';
import 'package:immich_mobile/services/api.service.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:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
@@ -31,7 +30,6 @@ final backgroundUploadServiceProvider = Provider((ref) {
ref.watch(storageRepositoryProvider), ref.watch(storageRepositoryProvider),
ref.watch(localAssetRepository), ref.watch(localAssetRepository),
ref.watch(backupRepositoryProvider), ref.watch(backupRepositoryProvider),
ref.watch(appSettingsServiceProvider),
ref.watch(assetMediaRepositoryProvider), ref.watch(assetMediaRepositoryProvider),
); );
@@ -105,7 +103,6 @@ class BackgroundUploadService {
this._storageRepository, this._storageRepository,
this._localAssetRepository, this._localAssetRepository,
this._backupRepository, this._backupRepository,
this._appSettingsService,
this._assetMediaRepository, this._assetMediaRepository,
) { ) {
_uploadRepository.onUploadStatus = _onUploadCallback; _uploadRepository.onUploadStatus = _onUploadCallback;
@@ -116,7 +113,6 @@ class BackgroundUploadService {
final StorageRepository _storageRepository; final StorageRepository _storageRepository;
final DriftLocalAssetRepository _localAssetRepository; final DriftLocalAssetRepository _localAssetRepository;
final DriftBackupRepository _backupRepository; final DriftBackupRepository _backupRepository;
final AppSettingsService _appSettingsService;
final AssetMediaRepository _assetMediaRepository; final AssetMediaRepository _assetMediaRepository;
final Logger _logger = Logger('BackgroundUploadService'); final Logger _logger = Logger('BackgroundUploadService');
@@ -363,15 +359,14 @@ class BackgroundUploadService {
} }
bool _shouldRequireWiFi(LocalAsset asset) { bool _shouldRequireWiFi(LocalAsset asset) {
bool requiresWiFi = true; final backup = MetadataRepository.instance.appConfig.backup;
if (asset.isVideo && backup.useCellularForVideos) {
if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) { return false;
requiresWiFi = false;
} else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) {
requiresWiFi = false;
} }
if (!asset.isVideo && backup.useCellularForPhotos) {
return requiresWiFi; return false;
}
return true;
} }
Future<UploadTask> buildUploadTask( 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/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/network_capability_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/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/platform/connectivity_api.g.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/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.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/asset_media.repository.dart';
import 'package:immich_mobile/repositories/upload.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:logging/logging.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
@@ -39,7 +38,6 @@ final foregroundUploadServiceProvider = Provider((ref) {
ref.watch(storageRepositoryProvider), ref.watch(storageRepositoryProvider),
ref.watch(backupRepositoryProvider), ref.watch(backupRepositoryProvider),
ref.watch(connectivityApiProvider), ref.watch(connectivityApiProvider),
ref.watch(appSettingsServiceProvider),
ref.watch(assetMediaRepositoryProvider), ref.watch(assetMediaRepositoryProvider),
); );
}); });
@@ -55,7 +53,6 @@ class ForegroundUploadService {
this._storageRepository, this._storageRepository,
this._backupRepository, this._backupRepository,
this._connectivityApi, this._connectivityApi,
this._appSettingsService,
this._assetMediaRepository, this._assetMediaRepository,
); );
@@ -63,7 +60,6 @@ class ForegroundUploadService {
final StorageRepository _storageRepository; final StorageRepository _storageRepository;
final DriftBackupRepository _backupRepository; final DriftBackupRepository _backupRepository;
final ConnectivityApi _connectivityApi; final ConnectivityApi _connectivityApi;
final AppSettingsService _appSettingsService;
final AssetMediaRepository _assetMediaRepository; final AssetMediaRepository _assetMediaRepository;
final Logger _logger = Logger('ForegroundUploadService'); final Logger _logger = Logger('ForegroundUploadService');
@@ -455,14 +451,13 @@ class ForegroundUploadService {
} }
bool _shouldRequireWiFi(LocalAsset asset) { bool _shouldRequireWiFi(LocalAsset asset) {
bool requiresWiFi = true; final backup = MetadataRepository.instance.appConfig.backup;
if (asset.isVideo && backup.useCellularForVideos) {
if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) { return false;
requiresWiFi = false;
} else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) {
requiresWiFi = false;
} }
if (!asset.isVideo && backup.useCellularForPhotos) {
return requiresWiFi; return false;
}
return true;
} }
} }
@@ -1,66 +0,0 @@
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
final localFileManagerServiceProvider = Provider<LocalFilesManagerService>((ref) => const LocalFilesManagerService());
class LocalFilesManagerService {
const LocalFilesManagerService();
static final Logger _logger = Logger('LocalFilesManager');
static const MethodChannel _channel = MethodChannel('file_trash');
Future<bool> moveToTrash(List<String> mediaUrls) async {
try {
return await _channel.invokeMethod('moveToTrash', {'mediaUrls': mediaUrls});
} catch (e, s) {
_logger.warning('Error moving file to trash', e, s);
return false;
}
}
Future<bool> restoreFromTrash(String fileName, int type) async {
try {
return await _channel.invokeMethod('restoreFromTrash', {'fileName': fileName, 'type': type});
} catch (e, s) {
_logger.warning('Error restore file from trash', e, s);
return false;
}
}
Future<bool> restoreFromTrashById(String mediaId, int type) async {
try {
return await _channel.invokeMethod('restoreFromTrash', {'mediaId': mediaId, 'type': type});
} catch (e, s) {
_logger.warning('Error restore file from trash by Id', e, s);
return false;
}
}
Future<bool> requestManageMediaPermission() async {
try {
return await _channel.invokeMethod('requestManageMediaPermission');
} catch (e, s) {
_logger.warning('Error requesting manage media permission', e, s);
return false;
}
}
Future<bool> hasManageMediaPermission() async {
try {
return await _channel.invokeMethod('hasManageMediaPermission');
} catch (e, s) {
_logger.warning('Error requesting manage media permission state', e, s);
return false;
}
}
Future<bool> manageMediaPermission() async {
try {
return await _channel.invokeMethod('manageMediaPermission');
} catch (e, s) {
_logger.warning('Error requesting manage media permission settings', e, s);
return false;
}
}
}
+2 -2
View File
@@ -12,7 +12,7 @@ class WidgetService {
const WidgetService(this._repository); const WidgetService(this._repository);
Future<void> writeCredentials(String serverURL, String sessionKey, String? customHeaders) async { Future<void> writeCredentials(String serverURL, String sessionKey, String? customHeaders) async {
await _repository.setAppGroupId(appShareGroupId); await _repository.setAppGroupId();
await _repository.saveData(kWidgetServerEndpoint, serverURL); await _repository.saveData(kWidgetServerEndpoint, serverURL);
await _repository.saveData(kWidgetAuthToken, sessionKey); await _repository.saveData(kWidgetAuthToken, sessionKey);
@@ -25,7 +25,7 @@ class WidgetService {
} }
Future<void> clearCredentials() async { Future<void> clearCredentials() async {
await _repository.setAppGroupId(appShareGroupId); await _repository.setAppGroupId();
await _repository.saveData(kWidgetServerEndpoint, ""); await _repository.saveData(kWidgetServerEndpoint, "");
await _repository.saveData(kWidgetAuthToken, ""); await _repository.saveData(kWidgetAuthToken, "");
await _repository.saveData(kWidgetCustomHeaders, ""); await _repository.saveData(kWidgetCustomHeaders, "");
+123 -12
View File
@@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/material.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/entities/metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.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; const int targetVersion = 26;
@@ -37,12 +39,35 @@ Future<void> _migrateTo25() async {
return; return;
} }
final serverUrls = ApiService.getServerUrls(); final urls = <String>[];
if (serverUrls.isEmpty) { 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; 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 { Future<void> _migrateTo26(Drift drift) async {
@@ -57,14 +82,7 @@ Future<void> _migrateTo26(Drift drift) async {
final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(StoreKey.legacyCleanupKeepAlbumIds.id); final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(StoreKey.legacyCleanupKeepAlbumIds.id);
if (cleanupKeepAlbumIds != null) { if (cleanupKeepAlbumIds != null) {
final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList(); final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList();
await drift.metadataEntity.insertOnConflictUpdate( migrator.stage(StoreKey.legacyCleanupKeepAlbumIds, MetadataKey.cleanupKeepAlbumIds, ids);
MetadataEntityCompanion.insert(
key: MetadataKey.cleanupKeepAlbumIds.key,
value: MetadataKey.cleanupKeepAlbumIds.encode(ids),
updatedAt: Value(DateTime.now()),
),
);
await migrator.deleteLegacyStoreRows([StoreKey.legacyCleanupKeepAlbumIds.id]);
} }
await migrator.migrateBool(StoreKey.legacyCleanupKeepFavorites, MetadataKey.cleanupKeepFavorites); await migrator.migrateBool(StoreKey.legacyCleanupKeepFavorites, MetadataKey.cleanupKeepFavorites);
await migrator.migrateEnumIndex( await migrator.migrateEnumIndex(
@@ -96,9 +114,87 @@ Future<void> _migrateTo26(Drift drift) async {
await migrator.migrateBool(StoreKey.legacyLoadOriginalVideo, MetadataKey.viewerLoadOriginalVideo); await migrator.migrateBool(StoreKey.legacyLoadOriginalVideo, MetadataKey.viewerLoadOriginalVideo);
await migrator.migrateBool(StoreKey.legacyAutoPlayVideo, MetadataKey.viewerAutoPlayVideo); await migrator.migrateBool(StoreKey.legacyAutoPlayVideo, MetadataKey.viewerAutoPlayVideo);
await migrator.migrateBool(StoreKey.legacyTapToNavigate, MetadataKey.viewerTapToNavigate); 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(); 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 { class _StoreMigrator {
final Drift _db; final Drift _db;
final Map<MetadataKey<Object>, Object> _cache = {}; final Map<MetadataKey<Object>, Object> _cache = {};
@@ -153,6 +249,21 @@ class _StoreMigrator {
_migratedStoreIds.add(legacyKey.id); _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 { Future<void> complete() async {
await _db.batch((batch) { await _db.batch((batch) {
for (final entry in _cache.entries) { for (final entry in _cache.entries) {
@@ -6,13 +6,12 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/server_info/server_info.model.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/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/cast.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/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/server_info.provider.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.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) { 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 hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none));
final isDarkTheme = context.isDarkTheme; final isDarkTheme = context.isDarkTheme;
final iconColor = isDarkTheme ? Colors.white : Colors.black; final iconColor = isDarkTheme ? Colors.white : Colors.black;
final isUploading = ref.watch(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty)); final isUploading = ref.watch(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty));
return StreamBuilder( if (!backupEnabled) {
stream: backupStateStream, return _BadgeLabel(
initialData: false, Icon(Icons.cloud_off_rounded, size: 9, color: iconColor, semanticLabel: 'backup_controller_page_backup'.tr()),
builder: (ctx, snapshot) { );
final backupEnabled = snapshot.data ?? false; }
if (!backupEnabled) { if (hasError) {
return _BadgeLabel( return _BadgeLabel(
Icon( Icon(
Icons.cloud_off_rounded, Icons.warning_rounded,
size: 9, size: 12,
color: iconColor, color: context.colorScheme.error,
semanticLabel: 'backup_controller_page_backup'.tr(), 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),
), ),
); child: CircularProgressIndicator(
} strokeWidth: 2,
strokeCap: StrokeCap.round,
if (hasError) { valueColor: AlwaysStoppedAnimation<Color>(iconColor),
return _BadgeLabel( semanticsLabel: 'backup_controller_page_backup'.tr(),
Icon(
Icons.warning_rounded,
size: 12,
color: context.colorScheme.error,
semanticLabel: 'backup_controller_page_backup'.tr(),
), ),
backgroundColor: context.colorScheme.errorContainer, ),
); ),
} );
}
if (isUploading) { return _BadgeLabel(
return _BadgeLabel( Icon(Icons.check_outlined, size: 9, color: iconColor, semanticLabel: 'backup_controller_page_backup'.tr()),
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()),
);
},
); );
} }
} }
+14 -13
View File
@@ -15,13 +15,14 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/oauth.provider.dart'; import 'package:immich_mobile/providers/oauth.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/repositories/permission.repository.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/provider_utils.dart'; import 'package:immich_mobile/utils/provider_utils.dart';
import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/utils/url_helper.dart';
@@ -186,13 +187,13 @@ class LoginForm extends HookConsumerWidget {
await backgroundManager.syncRemote(); await backgroundManager.syncRemote();
await backgroundManager.hashAssets(); await backgroundManager.hashAssets();
if (Store.get(StoreKey.syncAlbums, false)) { if (MetadataRepository.instance.appConfig.backup.syncAlbums) {
await backgroundManager.syncLinkedAlbum(); await backgroundManager.syncLinkedAlbum();
} }
} }
getManageMediaPermission() async { getManageMediaPermission() async {
final hasPermission = await ref.read(localFilesManagerRepositoryProvider).hasManageMediaPermission(); final hasPermission = await ref.read(permissionRepositoryProvider).hasManageMediaPermission();
if (!hasPermission) { if (!hasPermission) {
await showDialog( await showDialog(
context: context, context: context,
@@ -223,7 +224,7 @@ class LoginForm extends HookConsumerWidget {
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
ref.read(localFilesManagerRepositoryProvider).requestManageMediaPermission(); unawaited(ref.read(permissionRepositoryProvider).requestManageMediaPermission());
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text( child: Text(
@@ -397,16 +398,16 @@ class LoginForm extends HookConsumerWidget {
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
children: [ children: [
ImmichForm( ImmichForm(
onSubmit: getServerAuthSettings,
submitText: 'next'.t(context: context), submitText: 'next'.t(context: context),
submitIcon: Icons.arrow_forward_rounded, submitIcon: Icons.arrow_forward_rounded,
onSubmit: getServerAuthSettings, builder: (_, form) => ImmichURLInput(
child: ImmichURLInput(
controller: serverEndpointController, controller: serverEndpointController,
label: 'login_form_endpoint_url'.t(context: context), label: 'login_form_endpoint_url'.t(context: context),
hintText: 'login_form_endpoint_hint'.t(context: context), hintText: 'login_form_endpoint_hint'.t(context: context),
validator: _validateUrl, validator: _validateUrl,
keyboardAction: .next, keyboardAction: .next,
onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(), onSubmit: (_) => form.submit(),
), ),
), ),
ImmichTextButton( ImmichTextButton(
@@ -434,10 +435,10 @@ class LoginForm extends HookConsumerWidget {
), ),
if (isPasswordLoginEnable.value) if (isPasswordLoginEnable.value)
ImmichForm( ImmichForm(
onSubmit: login,
submitText: 'login'.t(context: context), submitText: 'login'.t(context: context),
submitIcon: Icons.login_rounded, submitIcon: Icons.login_rounded,
onSubmit: login, builder: (context, form) => Column(
child: Column(
spacing: ImmichSpacing.md, spacing: ImmichSpacing.md,
children: [ children: [
ImmichTextInput( ImmichTextInput(
@@ -448,7 +449,7 @@ class LoginForm extends HookConsumerWidget {
keyboardAction: TextInputAction.next, keyboardAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email], autofillHints: const [AutofillHints.email],
onSubmit: (_, _) => passwordFocusNode.requestFocus(), onSubmit: (_) => passwordFocusNode.requestFocus(),
), ),
ImmichPasswordInput( ImmichPasswordInput(
controller: passwordController, controller: passwordController,
@@ -456,17 +457,17 @@ class LoginForm extends HookConsumerWidget {
label: 'password'.t(context: context), label: 'password'.t(context: context),
hintText: 'login_form_password_hint'.t(context: context), hintText: 'login_form_password_hint'.t(context: context),
keyboardAction: TextInputAction.go, keyboardAction: TextInputAction.go,
onSubmit: (ctx, _) => ImmichForm.of(ctx).submit(), onSubmit: (_) => form.submit(),
), ),
], ],
), ),
), ),
if (isOauthEnable.value) if (isOauthEnable.value)
ImmichForm( ImmichForm(
onSubmit: oAuthLogin,
submitText: oAuthButtonLabel.value, submitText: oAuthButtonLabel.value,
submitIcon: Icons.pin_outlined, submitIcon: Icons.pin_outlined,
onSubmit: oAuthLogin, builder: (context, _) => isPasswordLoginEnable.value
child: isPasswordLoginEnable.value
? Padding( ? Padding(
padding: const EdgeInsets.only(left: 18.0, right: 18.0, top: 12.0), padding: const EdgeInsets.only(left: 18.0, right: 18.0, top: 12.0),
child: Divider(color: context.isDarkTheme ? Colors.white : Colors.black, height: 5), child: Divider(color: context.isDarkTheme ? Colors.white : Colors.black, height: 5),
@@ -10,7 +10,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart'; import 'package:immich_mobile/repositories/permission.repository.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
@@ -57,9 +57,7 @@ class AdvancedSettings extends HookConsumerWidget {
() async { () async {
isManageMediaSupported.value = await checkAndroidVersion(); isManageMediaSupported.value = await checkAndroidVersion();
if (isManageMediaSupported.value) { if (isManageMediaSupported.value) {
manageMediaAndroidPermission.value = await ref manageMediaAndroidPermission.value = await ref.read(permissionRepositoryProvider).hasManageMediaPermission();
.read(localFilesManagerRepositoryProvider)
.hasManageMediaPermission();
} }
}(); }();
return null; return null;
@@ -82,7 +80,7 @@ class AdvancedSettings extends HookConsumerWidget {
subtitle: "advanced_settings_sync_remote_deletions_subtitle".tr(), subtitle: "advanced_settings_sync_remote_deletions_subtitle".tr(),
onChanged: (value) async { onChanged: (value) async {
if (value) { if (value) {
final result = await ref.read(localFilesManagerRepositoryProvider).requestManageMediaPermission(); final result = await ref.read(permissionRepositoryProvider).requestManageMediaPermission();
manageLocalMediaAndroid.value = result; manageLocalMediaAndroid.value = result;
manageMediaAndroidPermission.value = result; manageMediaAndroidPermission.value = result;
} }
@@ -96,7 +94,7 @@ class AdvancedSettings extends HookConsumerWidget {
? const Color.fromARGB(255, 243, 188, 106) ? const Color.fromARGB(255, 243, 188, 106)
: null, : null,
onActionTap: () async { onActionTap: () async {
final result = await ref.read(localFilesManagerRepositoryProvider).manageMediaPermission(); final result = await ref.read(permissionRepositoryProvider).manageMediaPermission();
manageMediaAndroidPermission.value = result; manageMediaAndroidPermission.value = result;
}, },
), ),
@@ -4,18 +4,17 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.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/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/build_context_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/translate_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/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/user.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_group_title.dart';
import 'package:immich_mobile/widgets/settings/setting_list_tile.dart'; import 'package:immich_mobile/widgets/settings/setting_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.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), title: "network_requirements".t(context: context),
icon: Icons.cell_tower, icon: Icons.cell_tower,
), ),
const _UseWifiForUploadVideosButton(), const _UseCellularForVideosButton(),
const _UseWifiForUploadPhotosButton(), const _UseCellularForPhotosButton(),
if (CurrentPlatform.isAndroid) ...[ if (CurrentPlatform.isAndroid) ...[
const Divider(), const Divider(),
SettingGroupTitle( SettingGroupTitle(
@@ -99,64 +98,58 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final albumSyncEnable = ref.watch(appConfigProvider.select((c) => c.backup.syncAlbums));
return Padding( return Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0),
child: ListView( child: ListView(
shrinkWrap: true, shrinkWrap: true,
children: [ children: [
StreamBuilder( Column(
stream: Store.watch(StoreKey.syncAlbums), children: [
initialData: Store.tryGet(StoreKey.syncAlbums) ?? false, SettingListTile(
builder: (context, snapshot) { title: "sync_albums".t(context: context),
final albumSyncEnable = snapshot.data ?? false; subtitle: "sync_upload_album_setting_subtitle".t(context: context),
return Column( trailing: Switch(
children: [ value: albumSyncEnable,
SettingListTile( onChanged: (bool newValue) async {
title: "sync_albums".t(context: context), await ref.read(metadataProvider).write(MetadataKey.backupSyncAlbums, newValue);
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);
if (newValue == true) { if (newValue == true) {
await _manageLinkedAlbums(); await _manageLinkedAlbums();
} }
}, },
), ),
), ),
AnimatedSize( AnimatedSize(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
child: AnimatedOpacity( child: AnimatedOpacity(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
opacity: albumSyncEnable ? 1.0 : 0.0, opacity: albumSyncEnable ? 1.0 : 0.0,
child: albumSyncEnable child: albumSyncEnable
? SettingListTile( ? SettingListTile(
onTap: _manualSyncAlbums, onTap: _manualSyncAlbums,
contentPadding: const EdgeInsets.only(left: 32, right: 16), contentPadding: const EdgeInsets.only(left: 32, right: 16),
title: "organize_into_albums".t(context: context), title: "organize_into_albums".t(context: context),
subtitle: "organize_into_albums_description".t(context: context), subtitle: "organize_into_albums_description".t(context: context),
trailing: isAlbumSyncInProgress trailing: isAlbumSyncInProgress
? const SizedBox( ? const SizedBox(
width: 32, width: 32,
height: 32, height: 32,
child: CircularProgressIndicator.adaptive(strokeWidth: 2), child: CircularProgressIndicator.adaptive(strokeWidth: 2),
) )
: IconButton( : IconButton(
onPressed: _manualSyncAlbums, onPressed: _manualSyncAlbums,
icon: const Icon(Icons.sync_rounded), icon: const Icon(Icons.sync_rounded),
color: context.colorScheme.onSurface.withValues(alpha: 0.7), color: context.colorScheme.onSurface.withValues(alpha: 0.7),
iconSize: 20, iconSize: 20,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32), constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
), ),
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
), ),
), ),
], ],
);
},
), ),
], ],
), ),
@@ -164,60 +157,34 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton>
} }
} }
class _SettingsSwitchTile extends ConsumerStatefulWidget { class _BackupSwitchTile extends ConsumerWidget {
final AppSettingsEnum<bool> appSettingsEnum; final MetadataKey<bool> metadataKey;
final bool Function(AppConfig) selector;
final String titleKey; final String titleKey;
final String subtitleKey; final String subtitleKey;
final void Function(bool?)? onChanged; final void Function(bool)? onChanged;
const _SettingsSwitchTile({ const _BackupSwitchTile({
required this.appSettingsEnum, required this.metadataKey,
required this.selector,
required this.titleKey, required this.titleKey,
required this.subtitleKey, required this.subtitleKey,
this.onChanged, this.onChanged,
}); });
@override @override
ConsumerState createState() => _SettingsSwitchTileState(); Widget build(BuildContext context, WidgetRef ref) {
} final value = ref.watch(appConfigProvider.select(selector));
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) {
return Padding( return Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0),
child: SettingListTile( child: SettingListTile(
title: widget.titleKey.t(context: context), title: titleKey.t(context: context),
subtitle: widget.subtitleKey.t(context: context), subtitle: subtitleKey.t(context: context),
trailing: StreamBuilder( trailing: Switch(
stream: valueStream, value: value,
initialData: Store.tryGet(widget.appSettingsEnum.storeKey) ?? widget.appSettingsEnum.defaultValue, onChanged: (bool newValue) async {
builder: (context, snapshot) { await ref.read(metadataProvider).write(metadataKey, newValue);
final value = snapshot.data ?? false; onChanged?.call(newValue);
return Switch(
value: value,
onChanged: (bool newValue) async {
await ref.read(appSettingsServiceProvider).setSetting(widget.appSettingsEnum, newValue);
},
);
}, },
), ),
), ),
@@ -225,26 +192,28 @@ class _SettingsSwitchTileState extends ConsumerState<_SettingsSwitchTile> {
} }
} }
class _UseWifiForUploadVideosButton extends ConsumerWidget { class _UseCellularForVideosButton extends StatelessWidget {
const _UseWifiForUploadVideosButton(); const _UseCellularForVideosButton();
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context) {
return const _SettingsSwitchTile( return _BackupSwitchTile(
appSettingsEnum: AppSettingsEnum.useCellularForUploadVideos, metadataKey: MetadataKey.backupUseCellularForVideos,
selector: (c) => c.backup.useCellularForVideos,
titleKey: "videos", titleKey: "videos",
subtitleKey: "network_requirement_videos_upload", subtitleKey: "network_requirement_videos_upload",
); );
} }
} }
class _UseWifiForUploadPhotosButton extends ConsumerWidget { class _UseCellularForPhotosButton extends StatelessWidget {
const _UseWifiForUploadPhotosButton(); const _UseCellularForPhotosButton();
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context) {
return const _SettingsSwitchTile( return _BackupSwitchTile(
appSettingsEnum: AppSettingsEnum.useCellularForUploadPhotos, metadataKey: MetadataKey.backupUseCellularForPhotos,
selector: (c) => c.backup.useCellularForPhotos,
titleKey: "photos", titleKey: "photos",
subtitleKey: "network_requirement_photos_upload", subtitleKey: "network_requirement_photos_upload",
); );
@@ -256,29 +225,22 @@ class _BackupOnlyWhenChargingButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return _SettingsSwitchTile( final fgService = ref.read(backgroundWorkerFgServiceProvider);
appSettingsEnum: AppSettingsEnum.backupRequireCharging, return _BackupSwitchTile(
metadataKey: MetadataKey.backupRequireCharging,
selector: (c) => c.backup.requireCharging,
titleKey: "charging", titleKey: "charging",
subtitleKey: "charging_requirement_mobile_backup", subtitleKey: "charging_requirement_mobile_backup",
onChanged: (value) { onChanged: (value) {
ref.read(backgroundWorkerFgServiceProvider).configure(requireCharging: value ?? false); fgService.configure(requireCharging: value);
}, },
); );
} }
} }
class _BackupDelaySlider extends ConsumerStatefulWidget { class _BackupDelaySlider extends ConsumerWidget {
const _BackupDelaySlider(); 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) { static int backupDelayToSliderValue(int ms) => switch (ms) {
5 => 0, 5 => 0,
30 => 1, 30 => 1,
@@ -301,30 +263,9 @@ class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> {
}; };
@override @override
void initState() { Widget build(BuildContext context, WidgetRef ref) {
super.initState(); final triggerDelay = ref.watch(appConfigProvider.select((c) => c.backup.triggerDelay));
final initialValue = final currentValue = backupDelayToSliderValue(triggerDelay);
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) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -339,14 +280,13 @@ class _BackupDelaySliderState extends ConsumerState<_BackupDelaySlider> {
), ),
Slider( Slider(
value: currentValue.toDouble(), value: currentValue.toDouble(),
onChanged: (double v) { onChanged: (double v) async {
setState(() { final seconds = backupDelayToSeconds(v.toInt());
currentValue = v.toInt(); await ref.read(metadataProvider).write(MetadataKey.backupTriggerDelay, seconds);
});
}, },
onChangeEnd: (double v) async { onChangeEnd: (double v) async {
final milliseconds = backupDelayToSeconds(v.toInt()); final seconds = backupDelayToSeconds(v.toInt());
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.backupTriggerDelay, milliseconds); await ref.read(metadataProvider).write(MetadataKey.backupTriggerDelay, seconds);
}, },
max: 3.0, max: 3.0,
min: 0.0, min: 0.0,
@@ -1,13 +1,11 @@
import 'dart:convert';
import 'package:flutter/material.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:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.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'; import 'package:immich_mobile/widgets/settings/networking_settings/endpoint_input.dart';
class ExternalNetworkPreference extends HookConsumerWidget { class ExternalNetworkPreference extends HookConsumerWidget {
@@ -23,11 +21,12 @@ class ExternalNetworkPreference extends HookConsumerWidget {
saveEndpointList() { saveEndpointList() {
canSave.value = entries.value.every((e) => e.status == AuxCheckStatus.valid); 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); ref.read(metadataProvider).write(MetadataKey.networkExternalEndpointList, urls);
Store.put(StoreKey.externalEndpointList, jsonString);
} }
updateValidationStatus(String url, int index, AuxCheckStatus status) { updateValidationStatus(String url, int index, AuxCheckStatus status) {
@@ -69,14 +68,13 @@ class ExternalNetworkPreference extends HookConsumerWidget {
} }
useEffect(() { useEffect(() {
final jsonString = Store.tryGet(StoreKey.externalEndpointList); final urls = ref.read(metadataProvider).systemConfig.network.externalEndpointList;
if (jsonString == null) { if (urls.isEmpty) {
return null; return null;
} }
final List<dynamic> jsonList = jsonDecode(jsonString); entries.value = urls.map((url) => AuxilaryEndpoint(url: url, status: .valid)).toList();
entries.value = jsonList.map((e) => AuxilaryEndpoint.fromJson(e)).toList();
return null; return null;
}, const []); }, const []);
@@ -1,13 +1,12 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.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/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/utils/url_helper.dart';
import 'package:immich_mobile/widgets/settings/networking_settings/external_network_preference.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'; import 'package:immich_mobile/widgets/settings/networking_settings/local_network_preference.dart';
@@ -20,7 +19,10 @@ class NetworkingSettings extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final currentEndpoint = getServerUrl(); 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 { Future<void> checkWifiReadPermission() async {
final [hasLocationInUse, hasLocationAlways] = await Future.wait([ 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 .PHONY: build watch create_app_icon create_splash build_release_android pigeon test analyze format migration translation
build: 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: 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: 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: 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: 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: 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: 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: 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
+69 -69
View File
@@ -4,95 +4,95 @@ import 'package:flutter/material.dart';
import 'package:immich_ui/immich_ui.dart'; import 'package:immich_ui/immich_ui.dart';
import 'package:immich_ui/src/internal.dart'; import 'package:immich_ui/src/internal.dart';
class ImmichForm extends StatefulWidget { class ImmichFormController extends ChangeNotifier {
final String? submitText; ImmichFormController({this.onSubmit});
final IconData? submitIcon;
final FutureOr<void> Function()? onSubmit;
final Widget child;
const ImmichForm({ FutureOr<void> Function()? onSubmit;
super.key, final formKey = GlobalKey<FormState>();
this.submitText,
this.submitIcon,
required this.onSubmit,
required this.child,
});
@override
State<ImmichForm> createState() => ImmichFormState();
static ImmichFormState of(BuildContext context) {
final scope = context.dependOnInheritedWidgetOfExactType<_ImmichFormScope>();
if (scope == null) {
throw FlutterError(
'ImmichForm.of() called with a context that does not contain an ImmichForm.\n'
'No ImmichForm ancestor could be found starting from the context that was passed to '
'ImmichForm.of(). This usually happens when the context provided is '
'from a widget above the ImmichForm.\n'
'The context used was:\n'
'$context',
);
}
return scope._formState;
}
}
class ImmichFormState extends State<ImmichForm> {
final _formKey = GlobalKey<FormState>();
bool _isLoading = false; bool _isLoading = false;
bool get isLoading => _isLoading;
FutureOr<void> submit() async { Future<void> submit() async {
final isValid = _formKey.currentState?.validate() ?? false; if (_isLoading) {
if (!isValid) { return;
}
if (!(formKey.currentState?.validate() ?? false)) {
return; return;
} }
setState(() { _isLoading = true;
_isLoading = true; notifyListeners();
});
try { try {
await widget.onSubmit?.call(); await onSubmit?.call();
} finally { } finally {
if (mounted) { _isLoading = false;
setState(() { notifyListeners();
_isLoading = false;
});
}
} }
} }
}
class ImmichForm extends StatefulWidget {
final FutureOr<void> Function()? onSubmit;
final Widget Function(BuildContext context, ImmichFormController form) builder;
final String? submitText;
final IconData? submitIcon;
const ImmichForm({
super.key,
this.onSubmit,
this.submitText,
this.submitIcon,
required this.builder,
});
@override
State<ImmichForm> createState() => _ImmichFormState();
}
class _ImmichFormState extends State<ImmichForm> {
late final ImmichFormController _controller;
@override
void initState() {
super.initState();
_controller = ImmichFormController(onSubmit: widget.onSubmit);
}
@override
void didUpdateWidget(ImmichForm oldWidget) {
super.didUpdateWidget(oldWidget);
_controller.onSubmit = widget.onSubmit;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final submitText = widget.submitText ?? context.translations.submit; final submitText = widget.submitText ?? context.translations.submit;
return _ImmichFormScope( return Form(
formState: this, key: _controller.formKey,
child: Form( child: Column(
key: _formKey, spacing: ImmichSpacing.md,
child: Column( children: [
spacing: ImmichSpacing.md, widget.builder(context, _controller),
children: [ ListenableBuilder(
widget.child, listenable: _controller,
ImmichTextButton( builder: (context, _) => ImmichTextButton(
labelText: submitText, labelText: submitText,
icon: widget.submitIcon, icon: widget.submitIcon,
variant: ImmichVariant.filled, variant: ImmichVariant.filled,
loading: _isLoading, loading: _controller.isLoading,
onPressed: submit, onPressed: _controller.submit,
disabled: widget.onSubmit == null, disabled: _controller.onSubmit == null,
), ),
], ),
), ],
), ),
); );
} }
} }
class _ImmichFormScope extends InheritedWidget {
const _ImmichFormScope({required super.child, required ImmichFormState formState}) : _formState = formState;
final ImmichFormState _formState;
@override
bool updateShouldNotify(_ImmichFormScope oldWidget) => oldWidget._formState != _formState;
}

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