Compare commits

...

15 Commits

Author SHA1 Message Date
Daniel Dietzler cc66ad8a74 feat: sharing permissions 2026-05-22 17:11:21 +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
164 changed files with 7908 additions and 4967 deletions
+7 -7
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
+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",
+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"
+391
View File
@@ -0,0 +1,391 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools.flutter]]
version = "3.41.9-stable"
backend = "asdf:flutter"
[[tools."github:CQLabs/homebrew-dcm"]]
version = "1.37.0"
backend = "github:CQLabs/homebrew-dcm"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64"]
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64-musl"]
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64"]
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64-musl"]
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-arm64"]
checksum = "sha256:30bede64367d09067093cc57af6ec9496d7717898138ded5cb98a16ac8dd9d93"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543757"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-x64"]
checksum = "sha256:e56cb99872be7445a4de1d37e5438ca70e3bcd83be7a2b9b385e3538881f8068"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543727"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.windows-x64"]
checksum = "sha256:f133470daa3fb0427f039b424392af7e917d7e7db6b556aa2a968ab0e31587da"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-windows-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543660"
github_attestations = "unavailable"
[[tools."github:extism/cli"]]
version = "1.6.3"
backend = "github:extism/cli"
[tools."github:extism/cli"."platforms.linux-arm64"]
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.linux-arm64-musl"]
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.linux-x64"]
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.linux-x64-musl"]
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.macos-arm64"]
checksum = "sha256:b4ddbc575b5ac000115247f781723f9b9f284ed87b29c600539d72161b5b29fc"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-arm64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694029"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.macos-x64"]
checksum = "sha256:9a2f71b6e6009685a622cc3084e52d2a1a8e23c98d29ffa72e666e9dc699855f"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-amd64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694026"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.windows-x64"]
checksum = "sha256:47e4ed2782445b2b08a4d1ac127211588f8b4d1fc25fd6481d4cb65151b5213c"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-windows-amd64.zip"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694035"
github_attestations = "unavailable"
[[tools."github:extism/js-pdk"]]
version = "1.6.0"
backend = "github:extism/js-pdk"
[tools."github:extism/js-pdk"."platforms.linux-arm64"]
checksum = "sha256:15a186250e68d6bff4ec839fff275d45a90e383a69209dcc1239eb9e3aee6e1b"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-linux-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223214"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.linux-arm64-musl"]
checksum = "sha256:15a186250e68d6bff4ec839fff275d45a90e383a69209dcc1239eb9e3aee6e1b"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-linux-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223214"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.linux-x64"]
checksum = "sha256:4ded271ccf465031ccd0dc35e7a140e134d7f30721671cc4a8e1ff805d4aad68"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-linux-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223119"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.linux-x64-musl"]
checksum = "sha256:4ded271ccf465031ccd0dc35e7a140e134d7f30721671cc4a8e1ff805d4aad68"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-linux-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223119"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.macos-arm64"]
checksum = "sha256:548e25bda3971a07c32d78a249135cf8cb7b3eede101e878e06e53e01ac2e0ce"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-macos-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223215"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.macos-x64"]
checksum = "sha256:d85a875c2a071f0c29fe572764c52c3a499f157ab7f9efac8939a4364390e29b"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-macos-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223239"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.windows-x64"]
checksum = "sha256:97b7b746141e4777e1ca2b76febdeb16dc9d314ff6a4257df05a476b67228acc"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-windows-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133"
github_attestations = "unavailable"
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"]
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"]
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-x64"]
checksum = "sha256:066ede9774aaae97a18098aaeea8b7e0d286653eb8618f640476e99c59a536c2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_mac64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995889"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"]
checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094"
github_attestations = "unavailable"
[[tools."github:webassembly/binaryen"]]
version = "version_124"
backend = "github:webassembly/binaryen"
[tools."github:webassembly/binaryen"."platforms.linux-arm64"]
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.linux-arm64-musl"]
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.linux-x64"]
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.linux-x64-musl"]
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.macos-arm64"]
checksum = "sha256:86a2c960ff62c6d2ea6009d1f89745c22c70100d394a095eab45eb941bdaa24c"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-arm64-macos.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926134"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.macos-x64"]
checksum = "sha256:b389bb0731758d86c3cb266d01d28a12725c23bd3cabc3df34faa162af0887e9"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-macos.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926135"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.windows-x64"]
checksum = "sha256:b5e1d2a1ad3c03229ddc89823848f4a1c11f9c6402a51fa26f0aaa5f1d7a2203"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-windows.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288925833"
github_attestations = "unavailable"
[[tools.java]]
version = "21.0.2"
backend = "core:java"
[tools.java."platforms.linux-arm64"]
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-aarch64_bin.tar.gz"
[tools.java."platforms.linux-x64"]
checksum = "sha256:a2def047a73941e01a73739f92755f86b895811afb1f91243db214cff5bdac3f"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz"
[tools.java."platforms.macos-arm64"]
checksum = "sha256:b3d588e16ec1e0ef9805d8a696591bd518a5cea62567da8f53b5ce32d11d22e4"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-aarch64_bin.tar.gz"
[tools.java."platforms.macos-x64"]
checksum = "sha256:8fd09e15dc406387a0aba70bf5d99692874e999bf9cd9208b452b5d76ac922d3"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-x64_bin.tar.gz"
[tools.java."platforms.windows-x64"]
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
[[tools.node]]
version = "24.15.0"
backend = "core:node"
[tools.node."platforms.linux-arm64"]
checksum = "sha256:73afc234d558c24919875f51c2d1ea002a2ada4ea6f83601a383869fefa64eed"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-linux-arm64.tar.gz"
[tools.node."platforms.linux-arm64-musl"]
checksum = "sha256:31e98aa960a067da91edffd5d93bc46657b5d2a8029612c359f5f2ac0060152a"
url = "https://unofficial-builds.nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-arm64-musl.tar.gz"
[tools.node."platforms.linux-x64"]
checksum = "sha256:44836872d9aec49f1e6b52a9a922872db9a2b02d235a616a5681b6a85fec8d89"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-linux-x64.tar.gz"
[tools.node."platforms.linux-x64-musl"]
checksum = "sha256:f55af5bd489c5347b113ca6594cae00a54b30ba57ac5875324311bfc6f4762e3"
url = "https://unofficial-builds.nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-x64-musl.tar.gz"
[tools.node."platforms.macos-arm64"]
checksum = "sha256:372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-darwin-arm64.tar.gz"
[tools.node."platforms.macos-x64"]
checksum = "sha256:ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-darwin-x64.tar.gz"
[tools.node."platforms.windows-x64"]
checksum = "sha256:cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62"
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-win-x64.zip"
[[tools."npm:oazapfts"]]
version = "7.5.0"
backend = "npm:oazapfts"
[[tools.opentofu]]
version = "1.11.6"
backend = "aqua:opentofu/opentofu"
[tools.opentofu."platforms.linux-arm64"]
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
[tools.opentofu."platforms.linux-arm64-musl"]
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
[tools.opentofu."platforms.linux-x64"]
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
[tools.opentofu."platforms.linux-x64-musl"]
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
[tools.opentofu."platforms.macos-arm64"]
checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz"
[tools.opentofu."platforms.macos-x64"]
checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz"
[tools.opentofu."platforms.windows-x64"]
checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e"
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
[[tools.pnpm]]
version = "10.33.1"
backend = "aqua:pnpm/pnpm"
[tools.pnpm."platforms.linux-arm64"]
checksum = "sha256:ed8aa7901cf325f4cf5019405bdd6bf988426e4b23d08fe9b12ea4df7046f23e"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-linux-arm64"
[tools.pnpm."platforms.linux-arm64-musl"]
checksum = "sha256:ed8aa7901cf325f4cf5019405bdd6bf988426e4b23d08fe9b12ea4df7046f23e"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-linux-arm64"
[tools.pnpm."platforms.linux-x64"]
checksum = "sha256:fba950842532edd365e949b74643b64e6311089a45532dbe1e8f909a247fe3e9"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-linux-x64"
[tools.pnpm."platforms.linux-x64-musl"]
checksum = "sha256:fba950842532edd365e949b74643b64e6311089a45532dbe1e8f909a247fe3e9"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-linux-x64"
[tools.pnpm."platforms.macos-arm64"]
checksum = "sha256:909ced0038b00881d4d620ba2018c5d9691de373deea8e3c84b722b44324e47c"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-macos-arm64"
[tools.pnpm."platforms.macos-x64"]
checksum = "sha256:afdad60b83f4f482f4c95cc79325f29aef776d0922a324f023a312f40e0cc7d3"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-macos-x64"
[tools.pnpm."platforms.windows-x64"]
checksum = "sha256:67b23fd8c6800566b1cc04c446b170ff6e7977250084e4d8df9bfdbd8e6f4d02"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-win-x64.exe"
[[tools.terragrunt]]
version = "1.0.3"
backend = "aqua:gruntwork-io/terragrunt"
[tools.terragrunt."platforms.linux-arm64"]
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
[tools.terragrunt."platforms.linux-arm64-musl"]
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
[tools.terragrunt."platforms.linux-x64"]
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
[tools.terragrunt."platforms.linux-x64-musl"]
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
[tools.terragrunt."platforms.macos-arm64"]
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
[tools.terragrunt."platforms.macos-x64"]
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
[tools.terragrunt."platforms.windows-x64"]
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
+6 -5
View File
@@ -16,8 +16,8 @@ config_roots = [
[tools] [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
@@ -1,4 +1,5 @@
import 'package:immich_mobile/domain/models/config/album_config.dart'; 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';
@@ -16,6 +17,7 @@ class AppConfig {
final ViewerConfig viewer; final ViewerConfig viewer;
final SlideshowConfig slideshow; final SlideshowConfig slideshow;
final AlbumConfig album; final AlbumConfig album;
final BackupConfig backup;
const AppConfig({ const AppConfig({
this.theme = const .new(), this.theme = const .new(),
@@ -26,6 +28,7 @@ class AppConfig {
this.viewer = const .new(), this.viewer = const .new(),
this.slideshow = const .new(), this.slideshow = const .new(),
this.album = const .new(), this.album = const .new(),
this.backup = const .new(),
}); });
AppConfig copyWith({ AppConfig copyWith({
@@ -37,6 +40,7 @@ class AppConfig {
ViewerConfig? viewer, ViewerConfig? viewer,
SlideshowConfig? slideshow, SlideshowConfig? slideshow,
AlbumConfig? album, AlbumConfig? album,
BackupConfig? backup,
}) => .new( }) => .new(
theme: theme ?? this.theme, theme: theme ?? this.theme,
cleanup: cleanup ?? this.cleanup, cleanup: cleanup ?? this.cleanup,
@@ -46,6 +50,7 @@ class AppConfig {
viewer: viewer ?? this.viewer, viewer: viewer ?? this.viewer,
slideshow: slideshow ?? this.slideshow, slideshow: slideshow ?? this.slideshow,
album: album ?? this.album, album: album ?? this.album,
backup: backup ?? this.backup,
); );
@override @override
@@ -59,12 +64,13 @@ class AppConfig {
other.image == image && other.image == image &&
other.viewer == viewer && other.viewer == viewer &&
other.slideshow == slideshow && other.slideshow == slideshow &&
other.album == album); other.album == album &&
other.backup == backup);
@override @override
int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, slideshow, album); 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, album: $album)'; '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)';
}
@@ -62,6 +62,14 @@ enum MetadataKey<T extends Object> {
albumIsReverse<bool>(.appConfig, 'album.isReverse', true), albumIsReverse<bool>(.appConfig, 'album.isReverse', true),
albumIsGrid<bool>(.appConfig, 'album.isGrid', false), 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>(
+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);
+6 -7
View File
@@ -6,26 +6,25 @@ 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),
advancedTroubleshooting<bool>._(114), advancedTroubleshooting<bool>._(114),
enableHapticFeedback<bool>._(126), enableHapticFeedback<bool>._(126),
syncAlbums<bool>._(131),
manageLocalMediaAndroid<bool>._(137), manageLocalMediaAndroid<bool>._(137),
// Read-only Mode settings // Read-only Mode settings
readonlyModeEnabled<bool>._(138), readonlyModeEnabled<bool>._(138),
// 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), legacySelectedAlbumSortOrder<int>._(113),
legacySelectedAlbumSortReverse<bool>._(123), legacySelectedAlbumSortReverse<bool>._(123),
legacyAlbumGridView<bool>._(140), legacyAlbumGridView<bool>._(140),
@@ -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 {
@@ -152,6 +152,14 @@ extension<T extends Object> on MetadataDomain<T> {
isReverse: repo._read(.albumIsReverse), isReverse: repo._read(.albumIsReverse),
isGrid: repo._read(.albumIsGrid), 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( repo._systemConfig = .new(
@@ -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;
} }
@@ -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);
@@ -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);
+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((_) {
@@ -5,13 +5,7 @@ enum AppSettingsEnum<T> {
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false), advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false), manageLocalMediaAndroid<bool>(StoreKey.manageLocalMediaAndroid, null, false),
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true), enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
syncAlbums<bool>(StoreKey.syncAlbums, null, false), readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", 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),
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 -6
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);
} }
} }
@@ -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;
} }
} }
+7
View File
@@ -124,6 +124,13 @@ Future<void> _migrateTo26(Drift drift) async {
await _migrateAlbumSortMode(migrator); await _migrateAlbumSortMode(migrator);
await migrator.migrateBool(StoreKey.legacySelectedAlbumSortReverse, MetadataKey.albumIsReverse); await migrator.migrateBool(StoreKey.legacySelectedAlbumSortReverse, MetadataKey.albumIsReverse);
await migrator.migrateBool(StoreKey.legacyAlbumGridView, MetadataKey.albumIsGrid); 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();
} }
@@ -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()),
);
},
); );
} }
} }
@@ -15,6 +15,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/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';
@@ -186,7 +187,7 @@ 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();
} }
} }
@@ -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,
+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
+6 -1
View File
@@ -92,10 +92,12 @@ Class | Method | HTTP request | Description
*AlbumsApi* | [**getAlbumMapMarkers**](doc//AlbumsApi.md#getalbummapmarkers) | **GET** /albums/{id}/map-markers | Retrieve album map markers *AlbumsApi* | [**getAlbumMapMarkers**](doc//AlbumsApi.md#getalbummapmarkers) | **GET** /albums/{id}/map-markers | Retrieve album map markers
*AlbumsApi* | [**getAlbumStatistics**](doc//AlbumsApi.md#getalbumstatistics) | **GET** /albums/statistics | Retrieve album statistics *AlbumsApi* | [**getAlbumStatistics**](doc//AlbumsApi.md#getalbumstatistics) | **GET** /albums/statistics | Retrieve album statistics
*AlbumsApi* | [**getAllAlbums**](doc//AlbumsApi.md#getallalbums) | **GET** /albums | List all albums *AlbumsApi* | [**getAllAlbums**](doc//AlbumsApi.md#getallalbums) | **GET** /albums | List all albums
*AlbumsApi* | [**getOwnAlbumUser**](doc//AlbumsApi.md#getownalbumuser) | **GET** /albums/{id}/user/self | Get own sharing permissions
*AlbumsApi* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets | Remove assets from an album *AlbumsApi* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets | Remove assets from an album
*AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} | Remove user from album *AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} | Remove user from album
*AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} | Update an album *AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} | Update an album
*AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} | Update user role *AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} | Update user role
*AlbumsApi* | [**updateOwnAlbumUser**](doc//AlbumsApi.md#updateownalbumuser) | **PUT** /albums/{id}/user/self | Update own sharing permissions
*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | Check bulk upload *AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | Check bulk upload
*AssetsApi* | [**copyAsset**](doc//AssetsApi.md#copyasset) | **PUT** /assets/copy | Copy asset *AssetsApi* | [**copyAsset**](doc//AssetsApi.md#copyasset) | **PUT** /assets/copy | Copy asset
*AssetsApi* | [**deleteAssetMetadata**](doc//AssetsApi.md#deleteassetmetadata) | **DELETE** /assets/{id}/metadata/{key} | Delete asset metadata by key *AssetsApi* | [**deleteAssetMetadata**](doc//AssetsApi.md#deleteassetmetadata) | **DELETE** /assets/{id}/metadata/{key} | Delete asset metadata by key
@@ -452,7 +454,7 @@ Class | Method | HTTP request | Description
- [MemoryStatisticsResponseDto](doc//MemoryStatisticsResponseDto.md) - [MemoryStatisticsResponseDto](doc//MemoryStatisticsResponseDto.md)
- [MemoryType](doc//MemoryType.md) - [MemoryType](doc//MemoryType.md)
- [MemoryUpdateDto](doc//MemoryUpdateDto.md) - [MemoryUpdateDto](doc//MemoryUpdateDto.md)
- [MergePersonDto](doc//MergePersonDto.md) - [MergeFaceClusterDto](doc//MergeFaceClusterDto.md)
- [MetadataSearchDto](doc//MetadataSearchDto.md) - [MetadataSearchDto](doc//MetadataSearchDto.md)
- [MirrorAxis](doc//MirrorAxis.md) - [MirrorAxis](doc//MirrorAxis.md)
- [MirrorParameters](doc//MirrorParameters.md) - [MirrorParameters](doc//MirrorParameters.md)
@@ -544,6 +546,8 @@ Class | Method | HTTP request | Description
- [SharedLinkType](doc//SharedLinkType.md) - [SharedLinkType](doc//SharedLinkType.md)
- [SharedLinksResponse](doc//SharedLinksResponse.md) - [SharedLinksResponse](doc//SharedLinksResponse.md)
- [SharedLinksUpdate](doc//SharedLinksUpdate.md) - [SharedLinksUpdate](doc//SharedLinksUpdate.md)
- [SharingOptionsResponseDto](doc//SharingOptionsResponseDto.md)
- [SharingPermission](doc//SharingPermission.md)
- [SignUpDto](doc//SignUpDto.md) - [SignUpDto](doc//SignUpDto.md)
- [SmartSearchDto](doc//SmartSearchDto.md) - [SmartSearchDto](doc//SmartSearchDto.md)
- [SourceType](doc//SourceType.md) - [SourceType](doc//SourceType.md)
@@ -643,6 +647,7 @@ Class | Method | HTTP request | Description
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md) - [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
- [UpdateAssetDto](doc//UpdateAssetDto.md) - [UpdateAssetDto](doc//UpdateAssetDto.md)
- [UpdateLibraryDto](doc//UpdateLibraryDto.md) - [UpdateLibraryDto](doc//UpdateLibraryDto.md)
- [UpdateSharingOptionsDto](doc//UpdateSharingOptionsDto.md)
- [UsageByUserDto](doc//UsageByUserDto.md) - [UsageByUserDto](doc//UsageByUserDto.md)
- [UserAdminCreateDto](doc//UserAdminCreateDto.md) - [UserAdminCreateDto](doc//UserAdminCreateDto.md)
- [UserAdminDeleteDto](doc//UserAdminDeleteDto.md) - [UserAdminDeleteDto](doc//UserAdminDeleteDto.md)
+4 -1
View File
@@ -198,7 +198,7 @@ part 'model/memory_search_order.dart';
part 'model/memory_statistics_response_dto.dart'; part 'model/memory_statistics_response_dto.dart';
part 'model/memory_type.dart'; part 'model/memory_type.dart';
part 'model/memory_update_dto.dart'; part 'model/memory_update_dto.dart';
part 'model/merge_person_dto.dart'; part 'model/merge_face_cluster_dto.dart';
part 'model/metadata_search_dto.dart'; part 'model/metadata_search_dto.dart';
part 'model/mirror_axis.dart'; part 'model/mirror_axis.dart';
part 'model/mirror_parameters.dart'; part 'model/mirror_parameters.dart';
@@ -290,6 +290,8 @@ part 'model/shared_link_response_dto.dart';
part 'model/shared_link_type.dart'; part 'model/shared_link_type.dart';
part 'model/shared_links_response.dart'; part 'model/shared_links_response.dart';
part 'model/shared_links_update.dart'; part 'model/shared_links_update.dart';
part 'model/sharing_options_response_dto.dart';
part 'model/sharing_permission.dart';
part 'model/sign_up_dto.dart'; part 'model/sign_up_dto.dart';
part 'model/smart_search_dto.dart'; part 'model/smart_search_dto.dart';
part 'model/source_type.dart'; part 'model/source_type.dart';
@@ -389,6 +391,7 @@ part 'model/update_album_dto.dart';
part 'model/update_album_user_dto.dart'; part 'model/update_album_user_dto.dart';
part 'model/update_asset_dto.dart'; part 'model/update_asset_dto.dart';
part 'model/update_library_dto.dart'; part 'model/update_library_dto.dart';
part 'model/update_sharing_options_dto.dart';
part 'model/usage_by_user_dto.dart'; part 'model/usage_by_user_dto.dart';
part 'model/user_admin_create_dto.dart'; part 'model/user_admin_create_dto.dart';
part 'model/user_admin_delete_dto.dart'; part 'model/user_admin_delete_dto.dart';
+110
View File
@@ -580,6 +580,63 @@ class AlbumsApi {
return null; return null;
} }
/// Get own sharing permissions
///
/// Get the own sharing permissions in a specific album.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
Future<Response> getOwnAlbumUserWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/albums/{id}/user/self'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Get own sharing permissions
///
/// Get the own sharing permissions in a specific album.
///
/// Parameters:
///
/// * [String] id (required):
Future<SharingOptionsResponseDto?> getOwnAlbumUser(String id,) async {
final response = await getOwnAlbumUserWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharingOptionsResponseDto',) as SharingOptionsResponseDto;
}
return null;
}
/// Remove assets from an album /// Remove assets from an album
/// ///
/// Remove multiple assets from a specific album by its ID. /// Remove multiple assets from a specific album by its ID.
@@ -816,4 +873,57 @@ class AlbumsApi {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
} }
/// Update own sharing permissions
///
/// Change the own sharing permissions in a specific album.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [UpdateSharingOptionsDto] updateSharingOptionsDto (required):
Future<Response> updateOwnAlbumUserWithHttpInfo(String id, UpdateSharingOptionsDto updateSharingOptionsDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/albums/{id}/user/self'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = updateSharingOptionsDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Update own sharing permissions
///
/// Change the own sharing permissions in a specific album.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [UpdateSharingOptionsDto] updateSharingOptionsDto (required):
Future<void> updateOwnAlbumUser(String id, UpdateSharingOptionsDto updateSharingOptionsDto,) async {
final response = await updateOwnAlbumUserWithHttpInfo(id, updateSharingOptionsDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
} }
+6 -6
View File
@@ -448,14 +448,14 @@ class PeopleApi {
/// ///
/// * [String] id (required): /// * [String] id (required):
/// ///
/// * [MergePersonDto] mergePersonDto (required): /// * [MergeFaceClusterDto] mergeFaceClusterDto (required):
Future<Response> mergePersonWithHttpInfo(String id, MergePersonDto mergePersonDto,) async { Future<Response> mergePersonWithHttpInfo(String id, MergeFaceClusterDto mergeFaceClusterDto,) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/people/{id}/merge' final apiPath = r'/people/{id}/merge'
.replaceAll('{id}', id); .replaceAll('{id}', id);
// ignore: prefer_final_locals // ignore: prefer_final_locals
Object? postBody = mergePersonDto; Object? postBody = mergeFaceClusterDto;
final queryParams = <QueryParam>[]; final queryParams = <QueryParam>[];
final headerParams = <String, String>{}; final headerParams = <String, String>{};
@@ -483,9 +483,9 @@ class PeopleApi {
/// ///
/// * [String] id (required): /// * [String] id (required):
/// ///
/// * [MergePersonDto] mergePersonDto (required): /// * [MergeFaceClusterDto] mergeFaceClusterDto (required):
Future<List<BulkIdResponseDto>?> mergePerson(String id, MergePersonDto mergePersonDto,) async { Future<List<BulkIdResponseDto>?> mergePerson(String id, MergeFaceClusterDto mergeFaceClusterDto,) async {
final response = await mergePersonWithHttpInfo(id, mergePersonDto,); final response = await mergePersonWithHttpInfo(id, mergeFaceClusterDto,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
+8 -2
View File
@@ -442,8 +442,8 @@ class ApiClient {
return MemoryTypeTypeTransformer().decode(value); return MemoryTypeTypeTransformer().decode(value);
case 'MemoryUpdateDto': case 'MemoryUpdateDto':
return MemoryUpdateDto.fromJson(value); return MemoryUpdateDto.fromJson(value);
case 'MergePersonDto': case 'MergeFaceClusterDto':
return MergePersonDto.fromJson(value); return MergeFaceClusterDto.fromJson(value);
case 'MetadataSearchDto': case 'MetadataSearchDto':
return MetadataSearchDto.fromJson(value); return MetadataSearchDto.fromJson(value);
case 'MirrorAxis': case 'MirrorAxis':
@@ -626,6 +626,10 @@ class ApiClient {
return SharedLinksResponse.fromJson(value); return SharedLinksResponse.fromJson(value);
case 'SharedLinksUpdate': case 'SharedLinksUpdate':
return SharedLinksUpdate.fromJson(value); return SharedLinksUpdate.fromJson(value);
case 'SharingOptionsResponseDto':
return SharingOptionsResponseDto.fromJson(value);
case 'SharingPermission':
return SharingPermissionTypeTransformer().decode(value);
case 'SignUpDto': case 'SignUpDto':
return SignUpDto.fromJson(value); return SignUpDto.fromJson(value);
case 'SmartSearchDto': case 'SmartSearchDto':
@@ -824,6 +828,8 @@ class ApiClient {
return UpdateAssetDto.fromJson(value); return UpdateAssetDto.fromJson(value);
case 'UpdateLibraryDto': case 'UpdateLibraryDto':
return UpdateLibraryDto.fromJson(value); return UpdateLibraryDto.fromJson(value);
case 'UpdateSharingOptionsDto':
return UpdateSharingOptionsDto.fromJson(value);
case 'UsageByUserDto': case 'UsageByUserDto':
return UsageByUserDto.fromJson(value); return UsageByUserDto.fromJson(value);
case 'UserAdminCreateDto': case 'UserAdminCreateDto':
+3
View File
@@ -163,6 +163,9 @@ String parameterToString(dynamic value) {
if (value is SharedLinkType) { if (value is SharedLinkType) {
return SharedLinkTypeTypeTransformer().encode(value).toString(); return SharedLinkTypeTypeTransformer().encode(value).toString();
} }
if (value is SharingPermission) {
return SharingPermissionTypeTransformer().encode(value).toString();
}
if (value is SourceType) { if (value is SourceType) {
return SourceTypeTypeTransformer().encode(value).toString(); return SourceTypeTypeTransformer().encode(value).toString();
} }
+9 -1
View File
@@ -37,6 +37,7 @@ class AssetResponseDto {
this.owner, this.owner,
required this.ownerId, required this.ownerId,
this.people = const [], this.people = const [],
this.permissions = const [],
this.resized, this.resized,
this.stack, this.stack,
this.tags = const [], this.tags = const [],
@@ -140,6 +141,8 @@ class AssetResponseDto {
List<PersonResponseDto> people; List<PersonResponseDto> people;
List<SharingPermission> permissions;
/// Is resized /// Is resized
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
@@ -195,6 +198,7 @@ class AssetResponseDto {
other.owner == owner && other.owner == owner &&
other.ownerId == ownerId && other.ownerId == ownerId &&
_deepEquality.equals(other.people, people) && _deepEquality.equals(other.people, people) &&
_deepEquality.equals(other.permissions, permissions) &&
other.resized == resized && other.resized == resized &&
other.stack == stack && other.stack == stack &&
_deepEquality.equals(other.tags, tags) && _deepEquality.equals(other.tags, tags) &&
@@ -231,6 +235,7 @@ class AssetResponseDto {
(owner == null ? 0 : owner!.hashCode) + (owner == null ? 0 : owner!.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
(people.hashCode) + (people.hashCode) +
(permissions.hashCode) +
(resized == null ? 0 : resized!.hashCode) + (resized == null ? 0 : resized!.hashCode) +
(stack == null ? 0 : stack!.hashCode) + (stack == null ? 0 : stack!.hashCode) +
(tags.hashCode) + (tags.hashCode) +
@@ -241,7 +246,7 @@ class AssetResponseDto {
(width == null ? 0 : width!.hashCode); (width == null ? 0 : width!.hashCode);
@override @override
String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt, visibility=$visibility, width=$width]'; String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, permissions=$permissions, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt, visibility=$visibility, width=$width]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -301,6 +306,7 @@ class AssetResponseDto {
} }
json[r'ownerId'] = this.ownerId; json[r'ownerId'] = this.ownerId;
json[r'people'] = this.people; json[r'people'] = this.people;
json[r'permissions'] = this.permissions;
if (this.resized != null) { if (this.resized != null) {
json[r'resized'] = this.resized; json[r'resized'] = this.resized;
} else { } else {
@@ -361,6 +367,7 @@ class AssetResponseDto {
owner: UserResponseDto.fromJson(json[r'owner']), owner: UserResponseDto.fromJson(json[r'owner']),
ownerId: mapValueOfType<String>(json, r'ownerId')!, ownerId: mapValueOfType<String>(json, r'ownerId')!,
people: PersonResponseDto.listFromJson(json[r'people']), people: PersonResponseDto.listFromJson(json[r'people']),
permissions: SharingPermission.listFromJson(json[r'permissions']),
resized: mapValueOfType<bool>(json, r'resized'), resized: mapValueOfType<bool>(json, r'resized'),
stack: AssetStackResponseDto.fromJson(json[r'stack']), stack: AssetStackResponseDto.fromJson(json[r'stack']),
tags: TagResponseDto.listFromJson(json[r'tags']), tags: TagResponseDto.listFromJson(json[r'tags']),
@@ -433,6 +440,7 @@ class AssetResponseDto {
'originalFileName', 'originalFileName',
'originalPath', 'originalPath',
'ownerId', 'ownerId',
'permissions',
'thumbhash', 'thumbhash',
'type', 'type',
'updatedAt', 'updatedAt',
+3
View File
@@ -42,6 +42,7 @@ class JobName {
static const databaseBackup = JobName._(r'DatabaseBackup'); static const databaseBackup = JobName._(r'DatabaseBackup');
static const facialRecognitionQueueAll = JobName._(r'FacialRecognitionQueueAll'); static const facialRecognitionQueueAll = JobName._(r'FacialRecognitionQueueAll');
static const facialRecognition = JobName._(r'FacialRecognition'); static const facialRecognition = JobName._(r'FacialRecognition');
static const facialRecognitionMerge = JobName._(r'FacialRecognitionMerge');
static const fileDelete = JobName._(r'FileDelete'); static const fileDelete = JobName._(r'FileDelete');
static const fileMigrationQueueAll = JobName._(r'FileMigrationQueueAll'); static const fileMigrationQueueAll = JobName._(r'FileMigrationQueueAll');
static const libraryDeleteCheck = JobName._(r'LibraryDeleteCheck'); static const libraryDeleteCheck = JobName._(r'LibraryDeleteCheck');
@@ -100,6 +101,7 @@ class JobName {
databaseBackup, databaseBackup,
facialRecognitionQueueAll, facialRecognitionQueueAll,
facialRecognition, facialRecognition,
facialRecognitionMerge,
fileDelete, fileDelete,
fileMigrationQueueAll, fileMigrationQueueAll,
libraryDeleteCheck, libraryDeleteCheck,
@@ -193,6 +195,7 @@ class JobNameTypeTransformer {
case r'DatabaseBackup': return JobName.databaseBackup; case r'DatabaseBackup': return JobName.databaseBackup;
case r'FacialRecognitionQueueAll': return JobName.facialRecognitionQueueAll; case r'FacialRecognitionQueueAll': return JobName.facialRecognitionQueueAll;
case r'FacialRecognition': return JobName.facialRecognition; case r'FacialRecognition': return JobName.facialRecognition;
case r'FacialRecognitionMerge': return JobName.facialRecognitionMerge;
case r'FileDelete': return JobName.fileDelete; case r'FileDelete': return JobName.fileDelete;
case r'FileMigrationQueueAll': return JobName.fileMigrationQueueAll; case r'FileMigrationQueueAll': return JobName.fileMigrationQueueAll;
case r'LibraryDeleteCheck': return JobName.libraryDeleteCheck; case r'LibraryDeleteCheck': return JobName.libraryDeleteCheck;
+3
View File
@@ -29,6 +29,7 @@ class ManualJobName {
static const memoryCleanup = ManualJobName._(r'memory-cleanup'); static const memoryCleanup = ManualJobName._(r'memory-cleanup');
static const memoryCreate = ManualJobName._(r'memory-create'); static const memoryCreate = ManualJobName._(r'memory-create');
static const backupDatabase = ManualJobName._(r'backup-database'); static const backupDatabase = ManualJobName._(r'backup-database');
static const personGroupMerge = ManualJobName._(r'person-group-merge');
/// List of all possible values in this [enum][ManualJobName]. /// List of all possible values in this [enum][ManualJobName].
static const values = <ManualJobName>[ static const values = <ManualJobName>[
@@ -38,6 +39,7 @@ class ManualJobName {
memoryCleanup, memoryCleanup,
memoryCreate, memoryCreate,
backupDatabase, backupDatabase,
personGroupMerge,
]; ];
static ManualJobName? fromJson(dynamic value) => ManualJobNameTypeTransformer().decode(value); static ManualJobName? fromJson(dynamic value) => ManualJobNameTypeTransformer().decode(value);
@@ -82,6 +84,7 @@ class ManualJobNameTypeTransformer {
case r'memory-cleanup': return ManualJobName.memoryCleanup; case r'memory-cleanup': return ManualJobName.memoryCleanup;
case r'memory-create': return ManualJobName.memoryCreate; case r'memory-create': return ManualJobName.memoryCreate;
case r'backup-database': return ManualJobName.backupDatabase; case r'backup-database': return ManualJobName.backupDatabase;
case r'person-group-merge': return ManualJobName.personGroupMerge;
default: default:
if (!allowNull) { if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data'); throw ArgumentError('Unknown enum value to decode: $data');
@@ -10,17 +10,17 @@
part of openapi.api; part of openapi.api;
class MergePersonDto { class MergeFaceClusterDto {
/// Returns a new [MergePersonDto] instance. /// Returns a new [MergeFaceClusterDto] instance.
MergePersonDto({ MergeFaceClusterDto({
this.ids = const [], this.ids = const [],
}); });
/// Person IDs to merge /// Face cluster IDs to merge
List<String> ids; List<String> ids;
@override @override
bool operator ==(Object other) => identical(this, other) || other is MergePersonDto && bool operator ==(Object other) => identical(this, other) || other is MergeFaceClusterDto &&
_deepEquality.equals(other.ids, ids); _deepEquality.equals(other.ids, ids);
@override @override
@@ -29,7 +29,7 @@ class MergePersonDto {
(ids.hashCode); (ids.hashCode);
@override @override
String toString() => 'MergePersonDto[ids=$ids]'; String toString() => 'MergeFaceClusterDto[ids=$ids]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -37,15 +37,15 @@ class MergePersonDto {
return json; return json;
} }
/// Returns a new [MergePersonDto] instance and imports its values from /// Returns a new [MergeFaceClusterDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise. /// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods // ignore: prefer_constructors_over_static_methods
static MergePersonDto? fromJson(dynamic value) { static MergeFaceClusterDto? fromJson(dynamic value) {
upgradeDto(value, "MergePersonDto"); upgradeDto(value, "MergeFaceClusterDto");
if (value is Map) { if (value is Map) {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return MergePersonDto( return MergeFaceClusterDto(
ids: json[r'ids'] is Iterable ids: json[r'ids'] is Iterable
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false) ? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],
@@ -54,11 +54,11 @@ class MergePersonDto {
return null; return null;
} }
static List<MergePersonDto> listFromJson(dynamic json, {bool growable = false,}) { static List<MergeFaceClusterDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <MergePersonDto>[]; final result = <MergeFaceClusterDto>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
final value = MergePersonDto.fromJson(row); final value = MergeFaceClusterDto.fromJson(row);
if (value != null) { if (value != null) {
result.add(value); result.add(value);
} }
@@ -67,12 +67,12 @@ class MergePersonDto {
return result.toList(growable: growable); return result.toList(growable: growable);
} }
static Map<String, MergePersonDto> mapFromJson(dynamic json) { static Map<String, MergeFaceClusterDto> mapFromJson(dynamic json) {
final map = <String, MergePersonDto>{}; final map = <String, MergeFaceClusterDto>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) { for (final entry in json.entries) {
final value = MergePersonDto.fromJson(entry.value); final value = MergeFaceClusterDto.fromJson(entry.value);
if (value != null) { if (value != null) {
map[entry.key] = value; map[entry.key] = value;
} }
@@ -81,14 +81,14 @@ class MergePersonDto {
return map; return map;
} }
// maps a json object with a list of MergePersonDto-objects as value to a dart map // maps a json object with a list of MergeFaceClusterDto-objects as value to a dart map
static Map<String, List<MergePersonDto>> mapListFromJson(dynamic json, {bool growable = false,}) { static Map<String, List<MergeFaceClusterDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<MergePersonDto>>{}; final map = <String, List<MergeFaceClusterDto>>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments // ignore: parameter_assignments
json = json.cast<String, dynamic>(); json = json.cast<String, dynamic>();
for (final entry in json.entries) { for (final entry in json.entries) {
map[entry.key] = MergePersonDto.listFromJson(entry.value, growable: growable,); map[entry.key] = MergeFaceClusterDto.listFromJson(entry.value, growable: growable,);
} }
} }
return map; return map;
+14 -1
View File
@@ -15,6 +15,7 @@ class PersonResponseDto {
PersonResponseDto({ PersonResponseDto({
required this.birthDate, required this.birthDate,
this.color, this.color,
required this.faceClusterId,
required this.id, required this.id,
this.isFavorite, this.isFavorite,
required this.isHidden, required this.isHidden,
@@ -35,6 +36,9 @@ class PersonResponseDto {
/// ///
String? color; String? color;
/// Face cluster ID
String? faceClusterId;
/// Person ID /// Person ID
String id; String id;
@@ -69,6 +73,7 @@ class PersonResponseDto {
bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto && bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto &&
other.birthDate == birthDate && other.birthDate == birthDate &&
other.color == color && other.color == color &&
other.faceClusterId == faceClusterId &&
other.id == id && other.id == id &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
other.isHidden == isHidden && other.isHidden == isHidden &&
@@ -81,6 +86,7 @@ class PersonResponseDto {
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(birthDate == null ? 0 : birthDate!.hashCode) + (birthDate == null ? 0 : birthDate!.hashCode) +
(color == null ? 0 : color!.hashCode) + (color == null ? 0 : color!.hashCode) +
(faceClusterId == null ? 0 : faceClusterId!.hashCode) +
(id.hashCode) + (id.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) +
(isHidden.hashCode) + (isHidden.hashCode) +
@@ -89,7 +95,7 @@ class PersonResponseDto {
(updatedAt == null ? 0 : updatedAt!.hashCode); (updatedAt == null ? 0 : updatedAt!.hashCode);
@override @override
String toString() => 'PersonResponseDto[birthDate=$birthDate, color=$color, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]'; String toString() => 'PersonResponseDto[birthDate=$birthDate, color=$color, faceClusterId=$faceClusterId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -102,6 +108,11 @@ class PersonResponseDto {
json[r'color'] = this.color; json[r'color'] = this.color;
} else { } else {
// json[r'color'] = null; // json[r'color'] = null;
}
if (this.faceClusterId != null) {
json[r'faceClusterId'] = this.faceClusterId;
} else {
// json[r'faceClusterId'] = null;
} }
json[r'id'] = this.id; json[r'id'] = this.id;
if (this.isFavorite != null) { if (this.isFavorite != null) {
@@ -131,6 +142,7 @@ class PersonResponseDto {
return PersonResponseDto( return PersonResponseDto(
birthDate: mapDateTime(json, r'birthDate', r''), birthDate: mapDateTime(json, r'birthDate', r''),
color: mapValueOfType<String>(json, r'color'), color: mapValueOfType<String>(json, r'color'),
faceClusterId: mapValueOfType<String>(json, r'faceClusterId'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite'), isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isHidden: mapValueOfType<bool>(json, r'isHidden')!, isHidden: mapValueOfType<bool>(json, r'isHidden')!,
@@ -185,6 +197,7 @@ class PersonResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'birthDate', 'birthDate',
'faceClusterId',
'id', 'id',
'isHidden', 'isHidden',
'name', 'name',
+107
View File
@@ -0,0 +1,107 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SharingOptionsResponseDto {
/// Returns a new [SharingOptionsResponseDto] instance.
SharingOptionsResponseDto({
required this.inTimeline,
this.permissions = const [],
});
bool inTimeline;
List<SharingPermission> permissions;
@override
bool operator ==(Object other) => identical(this, other) || other is SharingOptionsResponseDto &&
other.inTimeline == inTimeline &&
_deepEquality.equals(other.permissions, permissions);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(inTimeline.hashCode) +
(permissions.hashCode);
@override
String toString() => 'SharingOptionsResponseDto[inTimeline=$inTimeline, permissions=$permissions]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'inTimeline'] = this.inTimeline;
json[r'permissions'] = this.permissions;
return json;
}
/// Returns a new [SharingOptionsResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SharingOptionsResponseDto? fromJson(dynamic value) {
upgradeDto(value, "SharingOptionsResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SharingOptionsResponseDto(
inTimeline: mapValueOfType<bool>(json, r'inTimeline')!,
permissions: SharingPermission.listFromJson(json[r'permissions']),
);
}
return null;
}
static List<SharingOptionsResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SharingOptionsResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SharingOptionsResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SharingOptionsResponseDto> mapFromJson(dynamic json) {
final map = <String, SharingOptionsResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SharingOptionsResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SharingOptionsResponseDto-objects as value to a dart map
static Map<String, List<SharingOptionsResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SharingOptionsResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SharingOptionsResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'inTimeline',
'permissions',
};
}
+112
View File
@@ -0,0 +1,112 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
/// Sharing permission schema
class SharingPermission {
/// Instantiate a new enum with the provided [value].
const SharingPermission._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const all = SharingPermission._(r'all');
static const assetPeriodRead = SharingPermission._(r'asset.read');
static const assetPeriodUpdate = SharingPermission._(r'asset.update');
static const assetPeriodEdit = SharingPermission._(r'asset.edit');
static const assetPeriodDelete = SharingPermission._(r'asset.delete');
static const assetPeriodShare = SharingPermission._(r'asset.share');
static const exifPeriodRead = SharingPermission._(r'exif.read');
static const personPeriodRead = SharingPermission._(r'person.read');
static const personPeriodUpdate = SharingPermission._(r'person.update');
static const personPeriodMerge = SharingPermission._(r'person.merge');
static const personPeriodDelete = SharingPermission._(r'person.delete');
/// List of all possible values in this [enum][SharingPermission].
static const values = <SharingPermission>[
all,
assetPeriodRead,
assetPeriodUpdate,
assetPeriodEdit,
assetPeriodDelete,
assetPeriodShare,
exifPeriodRead,
personPeriodRead,
personPeriodUpdate,
personPeriodMerge,
personPeriodDelete,
];
static SharingPermission? fromJson(dynamic value) => SharingPermissionTypeTransformer().decode(value);
static List<SharingPermission> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SharingPermission>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SharingPermission.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [SharingPermission] to String,
/// and [decode] dynamic data back to [SharingPermission].
class SharingPermissionTypeTransformer {
factory SharingPermissionTypeTransformer() => _instance ??= const SharingPermissionTypeTransformer._();
const SharingPermissionTypeTransformer._();
String encode(SharingPermission data) => data.value;
/// Decodes a [dynamic value][data] to a SharingPermission.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
SharingPermission? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'all': return SharingPermission.all;
case r'asset.read': return SharingPermission.assetPeriodRead;
case r'asset.update': return SharingPermission.assetPeriodUpdate;
case r'asset.edit': return SharingPermission.assetPeriodEdit;
case r'asset.delete': return SharingPermission.assetPeriodDelete;
case r'asset.share': return SharingPermission.assetPeriodShare;
case r'exif.read': return SharingPermission.exifPeriodRead;
case r'person.read': return SharingPermission.personPeriodRead;
case r'person.update': return SharingPermission.personPeriodUpdate;
case r'person.merge': return SharingPermission.personPeriodMerge;
case r'person.delete': return SharingPermission.personPeriodDelete;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [SharingPermissionTypeTransformer] instance.
static SharingPermissionTypeTransformer? _instance;
}
+14 -14
View File
@@ -19,11 +19,11 @@ class SyncAssetFaceV2 {
required this.boundingBoxY1, required this.boundingBoxY1,
required this.boundingBoxY2, required this.boundingBoxY2,
required this.deletedAt, required this.deletedAt,
required this.faceClusterId,
required this.id, required this.id,
required this.imageHeight, required this.imageHeight,
required this.imageWidth, required this.imageWidth,
required this.isVisible, required this.isVisible,
required this.personId,
required this.sourceType, required this.sourceType,
}); });
@@ -57,6 +57,9 @@ class SyncAssetFaceV2 {
/// Face deleted at /// Face deleted at
DateTime? deletedAt; DateTime? deletedAt;
/// Person ID
String? faceClusterId;
/// Asset face ID /// Asset face ID
String id; String id;
@@ -75,9 +78,6 @@ class SyncAssetFaceV2 {
/// Is the face visible in the asset /// Is the face visible in the asset
bool isVisible; bool isVisible;
/// Person ID
String? personId;
/// Source type /// Source type
String sourceType; String sourceType;
@@ -89,11 +89,11 @@ class SyncAssetFaceV2 {
other.boundingBoxY1 == boundingBoxY1 && other.boundingBoxY1 == boundingBoxY1 &&
other.boundingBoxY2 == boundingBoxY2 && other.boundingBoxY2 == boundingBoxY2 &&
other.deletedAt == deletedAt && other.deletedAt == deletedAt &&
other.faceClusterId == faceClusterId &&
other.id == id && other.id == id &&
other.imageHeight == imageHeight && other.imageHeight == imageHeight &&
other.imageWidth == imageWidth && other.imageWidth == imageWidth &&
other.isVisible == isVisible && other.isVisible == isVisible &&
other.personId == personId &&
other.sourceType == sourceType; other.sourceType == sourceType;
@override @override
@@ -105,15 +105,15 @@ class SyncAssetFaceV2 {
(boundingBoxY1.hashCode) + (boundingBoxY1.hashCode) +
(boundingBoxY2.hashCode) + (boundingBoxY2.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode) + (deletedAt == null ? 0 : deletedAt!.hashCode) +
(faceClusterId == null ? 0 : faceClusterId!.hashCode) +
(id.hashCode) + (id.hashCode) +
(imageHeight.hashCode) + (imageHeight.hashCode) +
(imageWidth.hashCode) + (imageWidth.hashCode) +
(isVisible.hashCode) + (isVisible.hashCode) +
(personId == null ? 0 : personId!.hashCode) +
(sourceType.hashCode); (sourceType.hashCode);
@override @override
String toString() => 'SyncAssetFaceV2[assetId=$assetId, boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, deletedAt=$deletedAt, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, isVisible=$isVisible, personId=$personId, sourceType=$sourceType]'; String toString() => 'SyncAssetFaceV2[assetId=$assetId, boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, deletedAt=$deletedAt, faceClusterId=$faceClusterId, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, isVisible=$isVisible, sourceType=$sourceType]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -128,16 +128,16 @@ class SyncAssetFaceV2 {
: this.deletedAt!.toUtc().toIso8601String(); : this.deletedAt!.toUtc().toIso8601String();
} else { } else {
// json[r'deletedAt'] = null; // json[r'deletedAt'] = null;
}
if (this.faceClusterId != null) {
json[r'faceClusterId'] = this.faceClusterId;
} else {
// json[r'faceClusterId'] = null;
} }
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'imageHeight'] = this.imageHeight; json[r'imageHeight'] = this.imageHeight;
json[r'imageWidth'] = this.imageWidth; json[r'imageWidth'] = this.imageWidth;
json[r'isVisible'] = this.isVisible; json[r'isVisible'] = this.isVisible;
if (this.personId != null) {
json[r'personId'] = this.personId;
} else {
// json[r'personId'] = null;
}
json[r'sourceType'] = this.sourceType; json[r'sourceType'] = this.sourceType;
return json; return json;
} }
@@ -157,11 +157,11 @@ class SyncAssetFaceV2 {
boundingBoxY1: mapValueOfType<int>(json, r'boundingBoxY1')!, boundingBoxY1: mapValueOfType<int>(json, r'boundingBoxY1')!,
boundingBoxY2: mapValueOfType<int>(json, r'boundingBoxY2')!, boundingBoxY2: mapValueOfType<int>(json, r'boundingBoxY2')!,
deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
faceClusterId: mapValueOfType<String>(json, r'faceClusterId'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
imageHeight: mapValueOfType<int>(json, r'imageHeight')!, imageHeight: mapValueOfType<int>(json, r'imageHeight')!,
imageWidth: mapValueOfType<int>(json, r'imageWidth')!, imageWidth: mapValueOfType<int>(json, r'imageWidth')!,
isVisible: mapValueOfType<bool>(json, r'isVisible')!, isVisible: mapValueOfType<bool>(json, r'isVisible')!,
personId: mapValueOfType<String>(json, r'personId'),
sourceType: mapValueOfType<String>(json, r'sourceType')!, sourceType: mapValueOfType<String>(json, r'sourceType')!,
); );
} }
@@ -216,11 +216,11 @@ class SyncAssetFaceV2 {
'boundingBoxY1', 'boundingBoxY1',
'boundingBoxY2', 'boundingBoxY2',
'deletedAt', 'deletedAt',
'faceClusterId',
'id', 'id',
'imageHeight', 'imageHeight',
'imageWidth', 'imageWidth',
'isVisible', 'isVisible',
'personId',
'sourceType', 'sourceType',
}; };
} }
+107
View File
@@ -0,0 +1,107 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class UpdateSharingOptionsDto {
/// Returns a new [UpdateSharingOptionsDto] instance.
UpdateSharingOptionsDto({
required this.inTimeline,
this.permissions = const [],
});
bool inTimeline;
List<SharingPermission> permissions;
@override
bool operator ==(Object other) => identical(this, other) || other is UpdateSharingOptionsDto &&
other.inTimeline == inTimeline &&
_deepEquality.equals(other.permissions, permissions);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(inTimeline.hashCode) +
(permissions.hashCode);
@override
String toString() => 'UpdateSharingOptionsDto[inTimeline=$inTimeline, permissions=$permissions]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'inTimeline'] = this.inTimeline;
json[r'permissions'] = this.permissions;
return json;
}
/// Returns a new [UpdateSharingOptionsDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static UpdateSharingOptionsDto? fromJson(dynamic value) {
upgradeDto(value, "UpdateSharingOptionsDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return UpdateSharingOptionsDto(
inTimeline: mapValueOfType<bool>(json, r'inTimeline')!,
permissions: SharingPermission.listFromJson(json[r'permissions']),
);
}
return null;
}
static List<UpdateSharingOptionsDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <UpdateSharingOptionsDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = UpdateSharingOptionsDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, UpdateSharingOptionsDto> mapFromJson(dynamic json) {
final map = <String, UpdateSharingOptionsDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = UpdateSharingOptionsDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of UpdateSharingOptionsDto-objects as value to a dart map
static Map<String, List<UpdateSharingOptionsDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<UpdateSharingOptionsDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = UpdateSharingOptionsDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'inTimeline',
'permissions',
};
}
+10 -10
View File
@@ -5,10 +5,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -103,10 +103,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.18.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@@ -124,10 +124,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.1" version: "1.10.2"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -164,10 +164,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.10" version: "0.7.11"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -180,10 +180,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.2" version: "15.2.0"
sdks: sdks:
dart: ">=3.11.0 <4.0.0" dart: ">=3.11.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54" flutter: ">=3.18.0-18.0.pre.54"
+16 -16
View File
@@ -5,10 +5,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: async name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.0" version: "2.13.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -77,10 +77,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.5" version: "2.2.0"
file: file:
dependency: transitive dependency: transitive
description: description:
@@ -124,10 +124,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: "5540e4a3f416dd4a93458257b908eb88353cbd0fb5b0a3d1bd7d849ba1e88735" sha256: "92d8cee7c57dff0a6c409c05597b460002434eccf7424a712283225b3962d03f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "17.2.1" version: "17.2.3"
immich_ui: immich_ui:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -211,10 +211,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.18.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@@ -248,10 +248,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.1" version: "1.10.2"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -312,10 +312,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.10" version: "0.7.11"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -328,10 +328,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: uuid name: uuid
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.2" version: "4.5.3"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -344,10 +344,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.2" version: "15.2.0"
web: web:
dependency: transitive dependency: transitive
description: description:
+44 -36
View File
@@ -133,10 +133,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: build name: build
sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.5" version: "4.0.6"
build_config: build_config:
dependency: transitive dependency: transitive
description: description:
@@ -157,10 +157,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: build_runner name: build_runner
sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e" sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.1" version: "2.15.0"
built_collection: built_collection:
dependency: transitive dependency: transitive
description: description:
@@ -173,10 +173,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: built_value name: built_value
sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af" sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.12.5" version: "8.12.6"
cast: cast:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -358,18 +358,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: drift name: drift
sha256: "055c249d1f91be5a47fe447f88afc24c4ca6f4cd6c5ed66767b4797d48acc2e5" sha256: "8033500116b24398fba0cca0369cc31678cd627c01e41753a61186911cea743e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.32.1" version: "2.33.0"
drift_dev: drift_dev:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: drift_dev name: drift_dev
sha256: "88a9de3af8571518148a6d8a513b57779fd1e60a026d3ab8a481a878fba01d91" sha256: b3dd5b75e30522a91da8abda9f5bb17230cb038097f6d15fa75d42bb563428aa
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.32.1" version: "2.33.0"
drift_flutter: drift_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -613,10 +613,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_svg name: flutter_svg
sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.4" version: "2.3.0"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -772,10 +772,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: hooks name: hooks
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.3"
hooks_riverpod: hooks_riverpod:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -844,18 +844,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: image_picker name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.2"
image_picker_android: image_picker_android:
dependency: transitive dependency: transitive
description: description:
name: image_picker_android name: image_picker_android
sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622" sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.13+16" version: "0.8.13+17"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
@@ -952,10 +952,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: json_annotation name: json_annotation
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.11.0" version: "4.12.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -984,10 +984,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lean_builder name: lean_builder
sha256: ee4117b03e93a4eb83e1a78c8e7a1dc22188d43bb142309982be48673a1b3a53 sha256: c16e95ddf7b2d49dd551357b7212fe2ce9f13ec7ad1b1e660c157184031e96c0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.7" version: "0.1.10"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@@ -1429,6 +1429,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
riverpod: riverpod:
dependency: transitive dependency: transitive
description: description:
@@ -1607,10 +1615,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_gen name: source_gen
sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd" sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.2" version: "4.2.3"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@@ -1647,10 +1655,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqlparser name: sqlparser
sha256: ab2b467425f1d4f3acfa5fd11a08226f7d6c26ff102c06be1807e1dff34e050b sha256: ecdc06d4a7d79dcbc928d99afd2f7f5b0f98a637c46f89be83d911617f759978
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.44.3" version: "0.44.4"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -1807,10 +1815,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.4.3"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
@@ -1831,10 +1839,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_graphics name: vector_graphics
sha256: "81da85e9ca8885ade47f9685b953cb098970d11be4821ac765580a6607ea4373" sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.21" version: "1.2.2"
vector_graphics_codec: vector_graphics_codec:
dependency: transitive dependency: transitive
description: description:
@@ -1847,10 +1855,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_graphics_compiler name: vector_graphics_compiler
sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" sha256: b9b3f391857781aa96acacef96066f2f49b4cd03cf9fce3ca4d8da2ef5ea129e
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.2.3"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -1863,10 +1871,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499" sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.1.0" version: "15.2.0"
wakelock_plus: wakelock_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1879,10 +1887,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: wakelock_plus_platform_interface name: wakelock_plus_platform_interface
sha256: "14b2e5b9e35c2631e656913c47adecdd71633ae92896a27a64c8f1fcfabc21cc" sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" version: "1.5.1"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:
@@ -9,7 +9,7 @@ import 'package:mocktail/mocktail.dart';
import '../../infrastructure/repository.mock.dart'; import '../../infrastructure/repository.mock.dart';
const _kAccessToken = '#ThisIsAToken'; const _kAccessToken = '#ThisIsAToken';
const _kEnableBackup = false; const _kAdvancedTroubleshooting = false;
const _kVersion = 2; const _kVersion = 2;
void main() { void main() {
@@ -22,13 +22,13 @@ void main() {
mockDriftStoreRepo = MockDriftStoreRepository(); mockDriftStoreRepo = MockDriftStoreRepository();
// For generics, we need to provide fallback to each concrete type to avoid runtime errors // For generics, we need to provide fallback to each concrete type to avoid runtime errors
registerFallbackValue(StoreKey.accessToken); registerFallbackValue(StoreKey.accessToken);
registerFallbackValue(StoreKey.backupTriggerDelay); registerFallbackValue(StoreKey.version);
registerFallbackValue(StoreKey.enableBackup); registerFallbackValue(StoreKey.advancedTroubleshooting);
when(() => mockDriftStoreRepo.getAll()).thenAnswer( when(() => mockDriftStoreRepo.getAll()).thenAnswer(
(_) async => [ (_) async => [
const StoreDto(StoreKey.accessToken, _kAccessToken), const StoreDto(StoreKey.accessToken, _kAccessToken),
const StoreDto(StoreKey.enableBackup, _kEnableBackup), const StoreDto(StoreKey.advancedTroubleshooting, _kAdvancedTroubleshooting),
const StoreDto(StoreKey.version, _kVersion), const StoreDto(StoreKey.version, _kVersion),
], ],
); );
@@ -46,7 +46,7 @@ void main() {
test('Populates the internal cache on init', () { test('Populates the internal cache on init', () {
verify(() => mockDriftStoreRepo.getAll()).called(1); verify(() => mockDriftStoreRepo.getAll()).called(1);
expect(sut.tryGet(StoreKey.accessToken), _kAccessToken); expect(sut.tryGet(StoreKey.accessToken), _kAccessToken);
expect(sut.tryGet(StoreKey.enableBackup), _kEnableBackup); expect(sut.tryGet(StoreKey.advancedTroubleshooting), _kAdvancedTroubleshooting);
expect(sut.tryGet(StoreKey.version), _kVersion); expect(sut.tryGet(StoreKey.version), _kVersion);
// Other keys should be null // Other keys should be null
expect(sut.tryGet(StoreKey.currentUser), isNull); expect(sut.tryGet(StoreKey.currentUser), isNull);
@@ -147,7 +147,7 @@ void main() {
await sut.clear(); await sut.clear();
verify(() => mockDriftStoreRepo.deleteAll()).called(1); verify(() => mockDriftStoreRepo.deleteAll()).called(1);
expect(sut.tryGet(StoreKey.accessToken), isNull); expect(sut.tryGet(StoreKey.accessToken), isNull);
expect(sut.tryGet(StoreKey.enableBackup), isNull); expect(sut.tryGet(StoreKey.advancedTroubleshooting), isNull);
expect(sut.tryGet(StoreKey.version), isNull); expect(sut.tryGet(StoreKey.version), isNull);
}); });
}); });
@@ -13,7 +13,7 @@ import '../../fixtures/user.stub.dart';
const _kTestAccessToken = "#TestToken"; const _kTestAccessToken = "#TestToken";
const _kTestVersion = 10; const _kTestVersion = 10;
const _kTestBackupRequireCharging = false; const _kTestAdvancedTroubleshooting = false;
final _kTestUser = UserStub.admin; final _kTestUser = UserStub.admin;
Future<void> _populateStore(Drift db) async { Future<void> _populateStore(Drift db) async {
@@ -21,8 +21,8 @@ Future<void> _populateStore(Drift db) async {
batch.insert( batch.insert(
db.storeEntity, db.storeEntity,
StoreEntityCompanion( StoreEntityCompanion(
id: Value(StoreKey.backupRequireCharging.id), id: Value(StoreKey.advancedTroubleshooting.id),
intValue: const Value(_kTestBackupRequireCharging ? 1 : 0), intValue: const Value(_kTestAdvancedTroubleshooting ? 1 : 0),
stringValue: const Value(null), stringValue: const Value(null),
), ),
); );
@@ -76,11 +76,11 @@ void main() {
}); });
test('converts bool', () async { test('converts bool', () async {
bool? backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging); bool? advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting);
expect(backupRequireCharging, isNull); expect(advancedTroubleshooting, isNull);
await sut.upsert(StoreKey.backupRequireCharging, _kTestBackupRequireCharging); await sut.upsert(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting);
backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging); advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting);
expect(backupRequireCharging, _kTestBackupRequireCharging); expect(advancedTroubleshooting, _kTestAdvancedTroubleshooting);
}); });
test('converts user', () async { test('converts user', () async {
@@ -98,11 +98,11 @@ void main() {
}); });
test('delete()', () async { test('delete()', () async {
bool? backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging); bool? advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting);
expect(backupRequireCharging, isFalse); expect(advancedTroubleshooting, isFalse);
await sut.delete(StoreKey.backupRequireCharging); await sut.delete(StoreKey.advancedTroubleshooting);
backupRequireCharging = await sut.tryGet(StoreKey.backupRequireCharging); advancedTroubleshooting = await sut.tryGet(StoreKey.advancedTroubleshooting);
expect(backupRequireCharging, isNull); expect(advancedTroubleshooting, isNull);
}); });
test('deleteAll()', () async { test('deleteAll()', () async {
@@ -147,13 +147,13 @@ void main() {
emitsInOrder([ emitsInOrder([
[ [
const StoreDto<Object>(StoreKey.version, _kTestVersion), const StoreDto<Object>(StoreKey.version, _kTestVersion),
const StoreDto<Object>(StoreKey.backupRequireCharging, _kTestBackupRequireCharging),
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken), const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
const StoreDto<Object>(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting),
], ],
[ [
const StoreDto<Object>(StoreKey.version, _kTestVersion + 10), const StoreDto<Object>(StoreKey.version, _kTestVersion + 10),
const StoreDto<Object>(StoreKey.backupRequireCharging, _kTestBackupRequireCharging),
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken), const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
const StoreDto<Object>(StoreKey.advancedTroubleshooting, _kTestAdvancedTroubleshooting),
], ],
]), ]),
), ),
@@ -21,7 +21,6 @@ void main() {
late MockApiService apiService; late MockApiService apiService;
late MockNetworkService networkService; late MockNetworkService networkService;
late MockBackgroundSyncManager backgroundSyncManager; late MockBackgroundSyncManager backgroundSyncManager;
late MockAppSettingService appSettingsService;
late Drift db; late Drift db;
setUp(() async { setUp(() async {
@@ -30,15 +29,12 @@ void main() {
apiService = MockApiService(); apiService = MockApiService();
networkService = MockNetworkService(); networkService = MockNetworkService();
backgroundSyncManager = MockBackgroundSyncManager(); backgroundSyncManager = MockBackgroundSyncManager();
appSettingsService = MockAppSettingService();
sut = AuthService( sut = AuthService(
authApiRepository, authApiRepository,
authRepository, authRepository,
apiService, apiService,
networkService, networkService,
backgroundSyncManager, backgroundSyncManager,
appSettingsService,
); );
registerFallbackValue(Uri()); registerFallbackValue(Uri());
@@ -13,11 +13,9 @@ 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/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:immich_mobile/services/background_upload.service.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import '../domain/service.mock.dart';
import '../fixtures/asset.stub.dart'; import '../fixtures/asset.stub.dart';
import '../infrastructure/repository.mock.dart'; import '../infrastructure/repository.mock.dart';
import '../mocks/asset_entity.mock.dart'; import '../mocks/asset_entity.mock.dart';
@@ -29,13 +27,10 @@ void main() {
late MockStorageRepository mockStorageRepository; late MockStorageRepository mockStorageRepository;
late MockDriftLocalAssetRepository mockLocalAssetRepository; late MockDriftLocalAssetRepository mockLocalAssetRepository;
late MockDriftBackupRepository mockBackupRepository; late MockDriftBackupRepository mockBackupRepository;
late MockAppSettingsService mockAppSettingsService;
late MockAssetMediaRepository mockAssetMediaRepository; late MockAssetMediaRepository mockAssetMediaRepository;
late Drift db; late Drift db;
setUpAll(() async { setUpAll(() async {
registerFallbackValue(AppSettingsEnum.useCellularForUploadPhotos);
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
const MethodChannel('plugins.flutter.io/path_provider'), const MethodChannel('plugins.flutter.io/path_provider'),
@@ -54,18 +49,13 @@ void main() {
mockStorageRepository = MockStorageRepository(); mockStorageRepository = MockStorageRepository();
mockLocalAssetRepository = MockDriftLocalAssetRepository(); mockLocalAssetRepository = MockDriftLocalAssetRepository();
mockBackupRepository = MockDriftBackupRepository(); mockBackupRepository = MockDriftBackupRepository();
mockAppSettingsService = MockAppSettingsService();
mockAssetMediaRepository = MockAssetMediaRepository(); mockAssetMediaRepository = MockAssetMediaRepository();
when(() => mockAppSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)).thenReturn(false);
when(() => mockAppSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)).thenReturn(false);
sut = BackgroundUploadService( sut = BackgroundUploadService(
mockUploadRepository, mockUploadRepository,
mockStorageRepository, mockStorageRepository,
mockLocalAssetRepository, mockLocalAssetRepository,
mockBackupRepository, mockBackupRepository,
mockAppSettingsService,
mockAssetMediaRepository, mockAssetMediaRepository,
); );
@@ -181,7 +171,6 @@ void main() {
mockStorageRepository, mockStorageRepository,
mockLocalAssetRepository, mockLocalAssetRepository,
mockBackupRepository, mockBackupRepository,
mockAppSettingsService,
mockAssetMediaRepository, mockAssetMediaRepository,
); );
addTearDown(() => sutWithV24.dispose()); addTearDown(() => sutWithV24.dispose());
@@ -232,7 +221,6 @@ void main() {
mockStorageRepository, mockStorageRepository,
mockLocalAssetRepository, mockLocalAssetRepository,
mockBackupRepository, mockBackupRepository,
mockAppSettingsService,
mockAssetMediaRepository, mockAssetMediaRepository,
); );
addTearDown(() => sutAndroid.dispose()); addTearDown(() => sutAndroid.dispose());
@@ -273,7 +261,6 @@ void main() {
mockStorageRepository, mockStorageRepository,
mockLocalAssetRepository, mockLocalAssetRepository,
mockBackupRepository, mockBackupRepository,
mockAppSettingsService,
mockAssetMediaRepository, mockAssetMediaRepository,
); );
addTearDown(() => sutWithV24.dispose()); addTearDown(() => sutWithV24.dispose());
@@ -314,7 +301,6 @@ void main() {
mockStorageRepository, mockStorageRepository,
mockLocalAssetRepository, mockLocalAssetRepository,
mockBackupRepository, mockBackupRepository,
mockAppSettingsService,
mockAssetMediaRepository, mockAssetMediaRepository,
); );
addTearDown(() => sutWithV24.dispose()); addTearDown(() => sutWithV24.dispose());
+193 -31
View File
@@ -11,9 +11,6 @@
"required": true, "required": true,
"in": "query", "in": "query",
"description": "Album ID", "description": "Album ID",
"x-nestjs_zod-parent-metadata": {
"description": "Activity search"
},
"schema": { "schema": {
"format": "uuid", "format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
@@ -25,9 +22,6 @@
"required": false, "required": false,
"in": "query", "in": "query",
"description": "Asset ID (if activity is for an asset)", "description": "Asset ID (if activity is for an asset)",
"x-nestjs_zod-parent-metadata": {
"description": "Activity search"
},
"schema": { "schema": {
"format": "uuid", "format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
@@ -38,9 +32,6 @@
"name": "level", "name": "level",
"required": false, "required": false,
"in": "query", "in": "query",
"x-nestjs_zod-parent-metadata": {
"description": "Activity search"
},
"schema": { "schema": {
"$ref": "#/components/schemas/ReactionLevel" "$ref": "#/components/schemas/ReactionLevel"
} }
@@ -49,9 +40,6 @@
"name": "type", "name": "type",
"required": false, "required": false,
"in": "query", "in": "query",
"x-nestjs_zod-parent-metadata": {
"description": "Activity search"
},
"schema": { "schema": {
"$ref": "#/components/schemas/ReactionType" "$ref": "#/components/schemas/ReactionType"
} }
@@ -61,9 +49,6 @@
"required": false, "required": false,
"in": "query", "in": "query",
"description": "Filter by user ID", "description": "Filter by user ID",
"x-nestjs_zod-parent-metadata": {
"description": "Activity search"
},
"schema": { "schema": {
"format": "uuid", "format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
@@ -187,9 +172,6 @@
"required": true, "required": true,
"in": "query", "in": "query",
"description": "Album ID", "description": "Album ID",
"x-nestjs_zod-parent-metadata": {
"description": "Activity"
},
"schema": { "schema": {
"format": "uuid", "format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
@@ -201,9 +183,6 @@
"required": false, "required": false,
"in": "query", "in": "query",
"description": "Asset ID (if activity is for an asset)", "description": "Asset ID (if activity is for an asset)",
"x-nestjs_zod-parent-metadata": {
"description": "Activity"
},
"schema": { "schema": {
"format": "uuid", "format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
@@ -2298,6 +2277,121 @@
"x-immich-permission": "album.read" "x-immich-permission": "album.read"
} }
}, },
"/albums/{id}/user/self": {
"get": {
"description": "Get the own sharing permissions in a specific album.",
"operationId": "getOwnAlbumUser",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SharingOptionsResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Get own sharing permissions",
"tags": [
"Albums"
],
"x-immich-history": [
{
"version": "v3",
"state": "Added"
},
{
"version": "v3",
"state": "Stable"
}
],
"x-immich-permission": "albumAsset.create",
"x-immich-state": "Stable"
},
"put": {
"description": "Change the own sharing permissions in a specific album.",
"operationId": "updateOwnAlbumUser",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateSharingOptionsDto"
}
}
},
"required": true
},
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Update own sharing permissions",
"tags": [
"Albums"
],
"x-immich-history": [
{
"version": "v3",
"state": "Added"
},
{
"version": "v3",
"state": "Stable"
}
],
"x-immich-permission": "albumAsset.create",
"x-immich-state": "Stable"
}
},
"/albums/{id}/user/{userId}": { "/albums/{id}/user/{userId}": {
"delete": { "delete": {
"description": "Remove a user from an album. Use an ID of \"me\" to leave a shared album.", "description": "Remove a user from an album. Use an ID of \"me\" to leave a shared album.",
@@ -8366,7 +8460,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/MergePersonDto" "$ref": "#/components/schemas/MergeFaceClusterDto"
} }
} }
}, },
@@ -16963,6 +17057,12 @@
}, },
"type": "array" "type": "array"
}, },
"permissions": {
"items": {
"$ref": "#/components/schemas/SharingPermission"
},
"type": "array"
},
"resized": { "resized": {
"description": "Is resized", "description": "Is resized",
"type": "boolean", "type": "boolean",
@@ -17034,6 +17134,7 @@
"originalFileName", "originalFileName",
"originalPath", "originalPath",
"ownerId", "ownerId",
"permissions",
"thumbhash", "thumbhash",
"type", "type",
"updatedAt", "updatedAt",
@@ -18093,6 +18194,7 @@
"DatabaseBackup", "DatabaseBackup",
"FacialRecognitionQueueAll", "FacialRecognitionQueueAll",
"FacialRecognition", "FacialRecognition",
"FacialRecognitionMerge",
"FileDelete", "FileDelete",
"FileMigrationQueueAll", "FileMigrationQueueAll",
"LibraryDeleteCheck", "LibraryDeleteCheck",
@@ -18502,7 +18604,8 @@
"user-cleanup", "user-cleanup",
"memory-cleanup", "memory-cleanup",
"memory-create", "memory-create",
"backup-database" "backup-database",
"person-group-merge"
], ],
"type": "string" "type": "string"
}, },
@@ -18828,10 +18931,10 @@
}, },
"type": "object" "type": "object"
}, },
"MergePersonDto": { "MergeFaceClusterDto": {
"properties": { "properties": {
"ids": { "ids": {
"description": "Person IDs to merge", "description": "Face cluster IDs to merge",
"items": { "items": {
"format": "uuid", "format": "uuid",
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$",
@@ -19856,6 +19959,11 @@
], ],
"x-immich-state": "Stable" "x-immich-state": "Stable"
}, },
"faceClusterId": {
"description": "Face cluster ID",
"nullable": true,
"type": "string"
},
"id": { "id": {
"description": "Person ID", "description": "Person ID",
"type": "string" "type": "string"
@@ -19906,6 +20014,7 @@
}, },
"required": [ "required": [
"birthDate", "birthDate",
"faceClusterId",
"id", "id",
"isHidden", "isHidden",
"name", "name",
@@ -21818,6 +21927,41 @@
}, },
"type": "object" "type": "object"
}, },
"SharingOptionsResponseDto": {
"properties": {
"inTimeline": {
"type": "boolean"
},
"permissions": {
"items": {
"$ref": "#/components/schemas/SharingPermission"
},
"type": "array"
}
},
"required": [
"inTimeline",
"permissions"
],
"type": "object"
},
"SharingPermission": {
"description": "Sharing permission schema",
"enum": [
"all",
"asset.read",
"asset.update",
"asset.edit",
"asset.delete",
"asset.share",
"exif.read",
"person.read",
"person.update",
"person.merge",
"person.delete"
],
"type": "string"
},
"SignUpDto": { "SignUpDto": {
"properties": { "properties": {
"email": { "email": {
@@ -22914,6 +23058,11 @@
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string" "type": "string"
}, },
"faceClusterId": {
"description": "Person ID",
"nullable": true,
"type": "string"
},
"id": { "id": {
"description": "Asset face ID", "description": "Asset face ID",
"type": "string" "type": "string"
@@ -22934,11 +23083,6 @@
"description": "Is the face visible in the asset", "description": "Is the face visible in the asset",
"type": "boolean" "type": "boolean"
}, },
"personId": {
"description": "Person ID",
"nullable": true,
"type": "string"
},
"sourceType": { "sourceType": {
"description": "Source type", "description": "Source type",
"type": "string" "type": "string"
@@ -22951,11 +23095,11 @@
"boundingBoxY1", "boundingBoxY1",
"boundingBoxY2", "boundingBoxY2",
"deletedAt", "deletedAt",
"faceClusterId",
"id", "id",
"imageHeight", "imageHeight",
"imageWidth", "imageWidth",
"isVisible", "isVisible",
"personId",
"sourceType" "sourceType"
], ],
"type": "object" "type": "object"
@@ -25447,6 +25591,24 @@
}, },
"type": "object" "type": "object"
}, },
"UpdateSharingOptionsDto": {
"properties": {
"inTimeline": {
"type": "boolean"
},
"permissions": {
"items": {
"$ref": "#/components/schemas/SharingPermission"
},
"type": "array"
}
},
"required": [
"inTimeline",
"permissions"
],
"type": "object"
},
"UsageByUserDto": { "UsageByUserDto": {
"properties": { "properties": {
"photos": { "photos": {
+1 -1
View File
@@ -7,7 +7,7 @@
"format": "prettier --cache --check i18n/", "format": "prettier --cache --check i18n/",
"format:fix": "prettier --cache --write --list-different i18n" "format:fix": "prettier --cache --write --list-different i18n"
}, },
"packageManager": "pnpm@10.33.1+sha512.05ba3c1d5d1c18f68df06470d74055e62d41fc110a0c660db1b2dfb2785327f04cf0f68345d4609bc52089e7fa0343c31593b2f9594e2c5d5da426230acc9820", "packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800",
"engines": { "engines": {
"pnpm": ">=10.0.0" "pnpm": ">=10.0.0"
}, },
+1 -1
View File
@@ -25,7 +25,7 @@
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9", "@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^24.12.2", "@types/node": "^24.12.4",
"@vitest/coverage-v8": "^4.0.0", "@vitest/coverage-v8": "^4.0.0",
"byte-size": "^9.0.0", "byte-size": "^9.0.0",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
+1 -1
View File
@@ -13,5 +13,5 @@
"oidc-provider": "^9.0.0", "oidc-provider": "^9.0.0",
"tsx": "^4.20.6" "tsx": "^4.20.6"
}, },
"packageManager": "pnpm@10.33.1" "packageManager": "pnpm@10.33.4"
} }
+3 -3
View File
@@ -24,11 +24,11 @@
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"packageManager": "pnpm@10.30.3", "packageManager": "pnpm@10.33.4",
"devDependencies": { "devDependencies": {
"@extism/js-pdk": "^1.1.1", "@extism/js-pdk": "^1.1.1",
"@types/node": "^24.11.0", "@types/node": "^24.12.4",
"esbuild": "^0.27.3", "esbuild": "^0.28.0",
"tsc-alias": "^1.8.16", "tsc-alias": "^1.8.16",
"typescript": "^5.9.3" "typescript": "^5.9.3"
}, },
+1 -1
View File
@@ -24,7 +24,7 @@
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.12.2", "@types/node": "^24.12.4",
"typescript": "^6.0.0" "typescript": "^6.0.0"
} }
} }
+60 -8
View File
@@ -555,6 +555,14 @@ export type MapMarkerResponseDto = {
/** State/Province name */ /** State/Province name */
state: string | null; state: string | null;
}; };
export type SharingOptionsResponseDto = {
inTimeline: boolean;
permissions: SharingPermission[];
};
export type UpdateSharingOptionsDto = {
inTimeline: boolean;
permissions: SharingPermission[];
};
export type UpdateAlbumUserDto = { export type UpdateAlbumUserDto = {
role: AlbumUserRole; role: AlbumUserRole;
}; };
@@ -792,6 +800,8 @@ export type PersonResponseDto = {
birthDate: string | null; birthDate: string | null;
/** Person color (hex) */ /** Person color (hex) */
color?: string; color?: string;
/** Face cluster ID */
faceClusterId: string | null;
/** Person ID */ /** Person ID */
id: string; id: string;
/** Is favorite */ /** Is favorite */
@@ -875,6 +885,7 @@ export type AssetResponseDto = {
/** Owner user ID */ /** Owner user ID */
ownerId: string; ownerId: string;
people?: PersonResponseDto[]; people?: PersonResponseDto[];
permissions: SharingPermission[];
/** Is resized */ /** Is resized */
resized?: boolean; resized?: boolean;
stack?: (AssetStackResponseDto) | null; stack?: (AssetStackResponseDto) | null;
@@ -1460,8 +1471,8 @@ export type PersonUpdateDto = {
/** Person name */ /** Person name */
name?: string; name?: string;
}; };
export type MergePersonDto = { export type MergeFaceClusterDto = {
/** Person IDs to merge */ /** Face cluster IDs to merge */
ids: string[]; ids: string[];
}; };
export type AssetFaceUpdateItem = { export type AssetFaceUpdateItem = {
@@ -2922,6 +2933,8 @@ export type SyncAssetFaceV2 = {
boundingBoxY2: number; boundingBoxY2: number;
/** Face deleted at */ /** Face deleted at */
deletedAt: string | null; deletedAt: string | null;
/** Person ID */
faceClusterId: string | null;
/** Asset face ID */ /** Asset face ID */
id: string; id: string;
/** Image height */ /** Image height */
@@ -2930,8 +2943,6 @@ export type SyncAssetFaceV2 = {
imageWidth: number; imageWidth: number;
/** Is the face visible in the asset */ /** Is the face visible in the asset */
isVisible: boolean; isVisible: boolean;
/** Person ID */
personId: string | null;
/** Source type */ /** Source type */
sourceType: string; sourceType: string;
}; };
@@ -3727,6 +3738,32 @@ export function getAlbumMapMarkers({ id, key, slug }: {
...opts ...opts
})); }));
} }
/**
* Get own sharing permissions
*/
export function getOwnAlbumUser({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: SharingOptionsResponseDto;
}>(`/albums/${encodeURIComponent(id)}/user/self`, {
...opts
}));
}
/**
* Update own sharing permissions
*/
export function updateOwnAlbumUser({ id, updateSharingOptionsDto }: {
id: string;
updateSharingOptionsDto: UpdateSharingOptionsDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/albums/${encodeURIComponent(id)}/user/self`, oazapfts.json({
...opts,
method: "PUT",
body: updateSharingOptionsDto
})));
}
/** /**
* Remove user from album * Remove user from album
*/ */
@@ -5131,9 +5168,9 @@ export function updatePerson({ id, personUpdateDto }: {
/** /**
* Merge people * Merge people
*/ */
export function mergePerson({ id, mergePersonDto }: { export function mergePerson({ id, mergeFaceClusterDto }: {
id: string; id: string;
mergePersonDto: MergePersonDto; mergeFaceClusterDto: MergeFaceClusterDto;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
@@ -5141,7 +5178,7 @@ export function mergePerson({ id, mergePersonDto }: {
}>(`/people/${encodeURIComponent(id)}/merge`, oazapfts.json({ }>(`/people/${encodeURIComponent(id)}/merge`, oazapfts.json({
...opts, ...opts,
method: "POST", method: "POST",
body: mergePersonDto body: mergeFaceClusterDto
}))); })));
} }
/** /**
@@ -6788,6 +6825,19 @@ export enum BulkIdErrorReason {
Unknown = "unknown", Unknown = "unknown",
Validation = "validation" Validation = "validation"
} }
export enum SharingPermission {
All = "all",
AssetRead = "asset.read",
AssetUpdate = "asset.update",
AssetEdit = "asset.edit",
AssetDelete = "asset.delete",
AssetShare = "asset.share",
ExifRead = "exif.read",
PersonRead = "person.read",
PersonUpdate = "person.update",
PersonMerge = "person.merge",
PersonDelete = "person.delete"
}
export enum Permission { export enum Permission {
All = "all", All = "all",
ActivityCreate = "activity.create", ActivityCreate = "activity.create",
@@ -6995,7 +7045,8 @@ export enum ManualJobName {
UserCleanup = "user-cleanup", UserCleanup = "user-cleanup",
MemoryCleanup = "memory-cleanup", MemoryCleanup = "memory-cleanup",
MemoryCreate = "memory-create", MemoryCreate = "memory-create",
BackupDatabase = "backup-database" BackupDatabase = "backup-database",
PersonGroupMerge = "person-group-merge"
} }
export enum QueueName { export enum QueueName {
ThumbnailGeneration = "thumbnailGeneration", ThumbnailGeneration = "thumbnailGeneration",
@@ -7072,6 +7123,7 @@ export enum JobName {
DatabaseBackup = "DatabaseBackup", DatabaseBackup = "DatabaseBackup",
FacialRecognitionQueueAll = "FacialRecognitionQueueAll", FacialRecognitionQueueAll = "FacialRecognitionQueueAll",
FacialRecognition = "FacialRecognition", FacialRecognition = "FacialRecognition",
FacialRecognitionMerge = "FacialRecognitionMerge",
FileDelete = "FileDelete", FileDelete = "FileDelete",
FileMigrationQueueAll = "FileMigrationQueueAll", FileMigrationQueueAll = "FileMigrationQueueAll",
LibraryDeleteCheck = "LibraryDeleteCheck", LibraryDeleteCheck = "LibraryDeleteCheck",
+4449 -4093
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -55,7 +55,7 @@ FROM builder AS plugins
ARG TARGETPLATFORM ARG TARGETPLATFORM
COPY --from=ghcr.io/jdx/mise:2026.3.12@sha256:0210678cbf58413806531a27adb2c7daf1c37238e56e8f7ea381d73521571775 /usr/local/bin/mise /usr/local/bin/mise COPY --from=ghcr.io/jdx/mise:2026.5.11@sha256:2ba959e4827f845fe0c4cfb4814089e790dc513040ef74f9e14925f446412a51 /usr/local/bin/mise /usr/local/bin/mise
WORKDIR /app WORKDIR /app
COPY ./mise.toml ./mise.toml COPY ./mise.toml ./mise.toml
+8 -8
View File
@@ -49,14 +49,14 @@
"@nestjs/websockets": "^11.0.4", "@nestjs/websockets": "^11.0.4",
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
"@opentelemetry/context-async-hooks": "^2.0.0", "@opentelemetry/context-async-hooks": "^2.0.0",
"@opentelemetry/exporter-prometheus": "^0.217.0", "@opentelemetry/exporter-prometheus": "^0.218.0",
"@opentelemetry/instrumentation-http": "^0.215.0", "@opentelemetry/instrumentation-http": "^0.218.0",
"@opentelemetry/instrumentation-ioredis": "^0.63.0", "@opentelemetry/instrumentation-ioredis": "^0.66.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.61.0", "@opentelemetry/instrumentation-nestjs-core": "^0.64.0",
"@opentelemetry/instrumentation-pg": "^0.67.0", "@opentelemetry/instrumentation-pg": "^0.70.0",
"@opentelemetry/resources": "^2.0.1", "@opentelemetry/resources": "^2.0.1",
"@opentelemetry/sdk-metrics": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1",
"@opentelemetry/sdk-node": "^0.217.0", "@opentelemetry/sdk-node": "^0.218.0",
"@opentelemetry/semantic-conventions": "^1.34.0", "@opentelemetry/semantic-conventions": "^1.34.0",
"@react-email/components": "^1.0.0", "@react-email/components": "^1.0.0",
"@react-email/render": "^2.0.0", "@react-email/render": "^2.0.0",
@@ -116,7 +116,7 @@
"ua-parser-js": "^2.0.0", "ua-parser-js": "^2.0.0",
"uuid": "^14.0.0", "uuid": "^14.0.0",
"validator": "^13.12.0", "validator": "^13.12.0",
"zod": "^4.3.6" "zod": "4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.0", "@eslint/js": "^10.0.0",
@@ -138,7 +138,7 @@
"@types/luxon": "^3.6.2", "@types/luxon": "^3.6.2",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/multer": "^2.0.0", "@types/multer": "^2.0.0",
"@types/node": "^24.12.2", "@types/node": "^24.12.4",
"@types/nodemailer": "^8.0.0", "@types/nodemailer": "^8.0.0",
"@types/picomatch": "^4.0.0", "@types/picomatch": "^4.0.0",
"@types/pngjs": "^6.0.5", "@types/pngjs": "^6.0.5",
@@ -11,6 +11,7 @@ import {
GetAlbumsDto, GetAlbumsDto,
UpdateAlbumDto, UpdateAlbumDto,
UpdateAlbumUserDto, UpdateAlbumUserDto,
UpdateSharingPermissionsDto as UpdateSharingOptionsDto,
} from 'src/dtos/album.dto'; } from 'src/dtos/album.dto';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
@@ -165,6 +166,33 @@ export class AlbumController {
return this.service.addUsers(auth, id, dto); return this.service.addUsers(auth, id, dto);
} }
@Get(':id/user/self')
@Authenticated({ permission: Permission.AlbumAssetCreate })
@Endpoint({
summary: 'Get own sharing permissions',
description: 'Get the own sharing permissions in a specific album.',
history: new HistoryBuilder().added('v3').stable('v3'),
})
getOwnAlbumUser(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.getSelf(auth, id);
}
@Put(':id/user/self')
@Authenticated({ permission: Permission.AlbumAssetCreate })
@HttpCode(HttpStatus.NO_CONTENT)
@Endpoint({
summary: 'Update own sharing permissions',
description: 'Change the own sharing permissions in a specific album.',
history: new HistoryBuilder().added('v3').stable('v3'),
})
updateOwnAlbumUser(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: UpdateSharingOptionsDto,
): Promise<void> {
return this.service.updateSelf(auth, id, dto);
}
@Put(':id/user/:userId') @Put(':id/user/:userId')
@Authenticated({ permission: Permission.AlbumUserUpdate }) @Authenticated({ permission: Permission.AlbumUserUpdate })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
+2 -2
View File
@@ -19,7 +19,7 @@ import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { import {
AssetFaceUpdateDto, AssetFaceUpdateDto,
MergePersonDto, MergeFaceClusterDto,
PeopleResponseDto, PeopleResponseDto,
PeopleUpdateDto, PeopleUpdateDto,
PersonCreateDto, PersonCreateDto,
@@ -182,7 +182,7 @@ export class PersonController {
mergePerson( mergePerson(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Body() dto: MergePersonDto, @Body() dto: MergeFaceClusterDto,
): Promise<BulkIdResponseDto[]> { ): Promise<BulkIdResponseDto[]> {
return this.service.mergePerson(auth, id, dto); return this.service.mergePerson(auth, id, dto);
} }
+4 -1
View File
@@ -9,6 +9,7 @@ import {
MemoryType, MemoryType,
Permission, Permission,
SharedLinkType, SharedLinkType,
SharingPermission,
SourceType, SourceType,
UserAvatarColor, UserAvatarColor,
UserStatus, UserStatus,
@@ -209,6 +210,7 @@ export type Partner = {
updatedAt: Date; updatedAt: Date;
updateId: string; updateId: string;
inTimeline: boolean; inTimeline: boolean;
permissions: SharingPermission[];
}; };
export type Place = { export type Place = {
@@ -252,6 +254,7 @@ export type Person = {
faceAssetId: string | null; faceAssetId: string | null;
isHidden: boolean; isHidden: boolean;
thumbnailPath: string; thumbnailPath: string;
faceClusterId: string | null;
}; };
export type AssetFace = { export type AssetFace = {
@@ -264,7 +267,7 @@ export type AssetFace = {
boundingBoxY2: number; boundingBoxY2: number;
imageHeight: number; imageHeight: number;
imageWidth: number; imageWidth: number;
personId: string | null; faceClusterId: string | null;
sourceType: SourceType; sourceType: SourceType;
person?: ShallowDehydrateObject<Person> | null; person?: ShallowDehydrateObject<Person> | null;
updatedAt: Date; updatedAt: Date;
+5 -7
View File
@@ -36,18 +36,16 @@ const ActivityStatisticsResponseSchema = z
}) })
.meta({ id: 'ActivityStatisticsResponseDto' }); .meta({ id: 'ActivityStatisticsResponseDto' });
const ActivitySchema = z const ActivitySchema = z.object({
.object({ albumId: z.uuidv4().describe('Album ID'),
albumId: z.uuidv4().describe('Album ID'), assetId: z.uuidv4().optional().describe('Asset ID (if activity is for an asset)'),
assetId: z.uuidv4().optional().describe('Asset ID (if activity is for an asset)'), });
})
.describe('Activity');
const ActivitySearchSchema = ActivitySchema.extend({ const ActivitySearchSchema = ActivitySchema.extend({
type: ReactionTypeSchema.optional(), type: ReactionTypeSchema.optional(),
level: ReactionLevelSchema.optional(), level: ReactionLevelSchema.optional(),
userId: z.uuidv4().optional().describe('Filter by user ID'), userId: z.uuidv4().optional().describe('Filter by user ID'),
}).describe('Activity search'); });
const ActivityCreateSchema = ActivitySchema.extend({ const ActivityCreateSchema = ActivitySchema.extend({
type: ReactionTypeSchema, type: ReactionTypeSchema,
+12 -2
View File
@@ -3,8 +3,8 @@ import { createZodDto } from 'nestjs-zod';
import { AlbumUser, AuthSharedLink } from 'src/database'; import { AlbumUser, AuthSharedLink } from 'src/database';
import { BulkIdErrorReasonSchema } from 'src/dtos/asset-ids.response.dto'; import { BulkIdErrorReasonSchema } from 'src/dtos/asset-ids.response.dto';
import { MapAsset } from 'src/dtos/asset-response.dto'; import { MapAsset } from 'src/dtos/asset-response.dto';
import { UserResponseSchema, mapUser } from 'src/dtos/user.dto'; import { mapUser, UserResponseSchema } from 'src/dtos/user.dto';
import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum'; import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema, SharingPermissionSchema } from 'src/enum';
import { MaybeDehydrated } from 'src/types'; import { MaybeDehydrated } from 'src/types';
import { asDateString } from 'src/utils/date'; import { asDateString } from 'src/utils/date';
import { stringToBool } from 'src/validation'; import { stringToBool } from 'src/validation';
@@ -63,6 +63,14 @@ const UpdateAlbumSchema = z
}) })
.meta({ id: 'UpdateAlbumDto' }); .meta({ id: 'UpdateAlbumDto' });
const UpdateSharingOptionsSchema = z
.object({ inTimeline: z.boolean(), permissions: z.array(SharingPermissionSchema) })
.meta({ id: 'UpdateSharingOptionsDto' });
const SharingOptionsResponseSchema = z
.object({ inTimeline: z.boolean(), permissions: z.array(SharingPermissionSchema) })
.meta({ id: 'SharingOptionsResponseDto' });
const GetAlbumsSchema = z const GetAlbumsSchema = z
.object({ .object({
isOwned: stringToBool isOwned: stringToBool
@@ -147,6 +155,8 @@ export class UpdateAlbumDto extends createZodDto(UpdateAlbumSchema) {}
export class GetAlbumsDto extends createZodDto(GetAlbumsSchema) {} export class GetAlbumsDto extends createZodDto(GetAlbumsSchema) {}
export class AlbumStatisticsResponseDto extends createZodDto(AlbumStatisticsResponseSchema) {} export class AlbumStatisticsResponseDto extends createZodDto(AlbumStatisticsResponseSchema) {}
export class UpdateAlbumUserDto extends createZodDto(UpdateAlbumUserSchema) {} export class UpdateAlbumUserDto extends createZodDto(UpdateAlbumUserSchema) {}
export class UpdateSharingPermissionsDto extends createZodDto(UpdateSharingOptionsSchema) {}
export class SharingPermissionsResponseDto extends createZodDto(SharingOptionsResponseSchema) {}
export class AlbumResponseDto extends createZodDto(AlbumResponseSchema) {} export class AlbumResponseDto extends createZodDto(AlbumResponseSchema) {}
class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {} class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {}
+16 -1
View File
@@ -15,6 +15,8 @@ import {
AssetVisibility, AssetVisibility,
AssetVisibilitySchema, AssetVisibilitySchema,
ChecksumAlgorithm, ChecksumAlgorithm,
SharingPermission,
SharingPermissionSchema,
} from 'src/enum'; } from 'src/enum';
import { MaybeDehydrated } from 'src/types'; import { MaybeDehydrated } from 'src/types';
import { hexOrBufferToBase64 } from 'src/utils/bytes'; import { hexOrBufferToBase64 } from 'src/utils/bytes';
@@ -45,6 +47,7 @@ const SanitizedAssetResponseSchema = z
hasMetadata: z.boolean().describe('Whether asset has metadata'), hasMetadata: z.boolean().describe('Whether asset has metadata'),
width: z.int().min(0).nullable().describe('Asset width'), width: z.int().min(0).nullable().describe('Asset width'),
height: z.int().min(0).nullable().describe('Asset height'), height: z.int().min(0).nullable().describe('Asset height'),
permissions: z.array(SharingPermissionSchema),
}) })
.meta({ id: 'SanitizedAssetResponseDto' }); .meta({ id: 'SanitizedAssetResponseDto' });
@@ -113,6 +116,7 @@ export const AssetResponseSchema = SanitizedAssetResponseSchema.extend(
.boolean() .boolean()
.describe('Is edited') .describe('Is edited')
.meta(new HistoryBuilder().added('v2.5.0').beta('v2.5.0').getExtensions()), .meta(new HistoryBuilder().added('v2.5.0').beta('v2.5.0').getExtensions()),
permissions: z.array(SharingPermissionSchema),
}).shape, }).shape,
).meta({ id: 'AssetResponseDto' }); ).meta({ id: 'AssetResponseDto' });
@@ -154,6 +158,7 @@ export type MapAsset = {
width: number | null; width: number | null;
height: number | null; height: number | null;
isEdited: boolean; isEdited: boolean;
permissions?: { permission: SharingPermission }[];
}; };
export type AssetMapOptions = { export type AssetMapOptions = {
@@ -192,8 +197,16 @@ const mapStack = (entity: { stack?: Stack | null }) => {
export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOptions = {}): AssetResponseDto { export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOptions = {}): AssetResponseDto {
const { stripMetadata = false, withStack = false } = options; const { stripMetadata = false, withStack = false } = options;
const permissions =
options.auth?.user.id === entity.ownerId
? [SharingPermission.All]
: (entity.permissions?.map(({ permission }) => permission) ?? []);
if (stripMetadata) { if (
stripMetadata ||
(entity.permissions &&
!(permissions.includes(SharingPermission.All) || permissions.includes(SharingPermission.ExifRead)))
) {
const sanitizedAssetResponse: SanitizedAssetResponseDto = { const sanitizedAssetResponse: SanitizedAssetResponseDto = {
id: entity.id, id: entity.id,
type: entity.type, type: entity.type,
@@ -205,6 +218,7 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
hasMetadata: false, hasMetadata: false,
width: entity.width, width: entity.width,
height: entity.height, height: entity.height,
permissions,
}; };
return sanitizedAssetResponse as AssetResponseDto; return sanitizedAssetResponse as AssetResponseDto;
} }
@@ -242,5 +256,6 @@ export function mapAsset(entity: MaybeDehydrated<MapAsset>, options: AssetMapOpt
width: entity.width, width: entity.width,
height: entity.height, height: entity.height,
isEdited: entity.isEdited, isEdited: entity.isEdited,
permissions,
}; };
} }
+7 -7
View File
@@ -2,7 +2,6 @@ import { Selectable } from 'kysely';
import { createZodDto } from 'nestjs-zod'; import { createZodDto } from 'nestjs-zod';
import { AssetFace, Person } from 'src/database'; import { AssetFace, Person } from 'src/database';
import { HistoryBuilder } from 'src/decorators'; import { HistoryBuilder } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import { AssetEditActionItem } from 'src/dtos/editing.dto'; import { AssetEditActionItem } from 'src/dtos/editing.dto';
import { SourceTypeSchema } from 'src/enum'; import { SourceTypeSchema } from 'src/enum';
import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
@@ -40,11 +39,11 @@ const PeopleUpdateSchema = z
}) })
.meta({ id: 'PeopleUpdateDto' }); .meta({ id: 'PeopleUpdateDto' });
const MergePersonSchema = z const MergeFaceClusterSchema = z
.object({ .object({
ids: z.array(z.uuidv4()).describe('Person IDs to merge'), ids: z.array(z.uuidv4()).describe('Face cluster IDs to merge'),
}) })
.meta({ id: 'MergePersonDto' }); .meta({ id: 'MergeFaceClusterDto' });
const PersonSearchSchema = z const PersonSearchSchema = z
.object({ .object({
@@ -81,13 +80,14 @@ export const PersonResponseSchema = z
.optional() .optional()
.describe('Person color (hex)') .describe('Person color (hex)')
.meta(new HistoryBuilder().added('v1.126.0').stable('v2').getExtensions()), .meta(new HistoryBuilder().added('v1.126.0').stable('v2').getExtensions()),
faceClusterId: z.string().nullable().describe('Face cluster ID'),
}) })
.meta({ id: 'PersonResponseDto' }); .meta({ id: 'PersonResponseDto' });
export class PersonCreateDto extends createZodDto(PersonCreateSchema) {} export class PersonCreateDto extends createZodDto(PersonCreateSchema) {}
export class PersonUpdateDto extends createZodDto(PersonUpdateSchema) {} export class PersonUpdateDto extends createZodDto(PersonUpdateSchema) {}
export class PeopleUpdateDto extends createZodDto(PeopleUpdateSchema) {} export class PeopleUpdateDto extends createZodDto(PeopleUpdateSchema) {}
export class MergePersonDto extends createZodDto(MergePersonSchema) {} export class MergeFaceClusterDto extends createZodDto(MergeFaceClusterSchema) {}
export class PersonSearchDto extends createZodDto(PersonSearchSchema) {} export class PersonSearchDto extends createZodDto(PersonSearchSchema) {}
export class PersonResponseDto extends createZodDto(PersonResponseSchema) {} export class PersonResponseDto extends createZodDto(PersonResponseSchema) {}
@@ -179,6 +179,7 @@ export function mapPerson(person: MaybeDehydrated<Person>): PersonResponseDto {
isFavorite: person.isFavorite, isFavorite: person.isFavorite,
color: person.color ?? undefined, color: person.color ?? undefined,
updatedAt: asDateString(person.updatedAt), updatedAt: asDateString(person.updatedAt),
faceClusterId: person.faceClusterId,
}; };
} }
@@ -207,12 +208,11 @@ function mapFacesWithoutPerson(
export function mapFaces( export function mapFaces(
face: AssetFace, face: AssetFace,
auth: AuthDto,
edits?: AssetEditActionItem[], edits?: AssetEditActionItem[],
assetDimensions?: ImageDimensions, assetDimensions?: ImageDimensions,
): AssetFaceResponseDto { ): AssetFaceResponseDto {
return { return {
...mapFacesWithoutPerson(face, edits, assetDimensions), ...mapFacesWithoutPerson(face, edits, assetDimensions),
person: face.person?.ownerId === auth.user.id ? mapPerson(face.person) : null, person: face.person ? mapPerson(face.person) : null,
}; };
} }
+7 -4
View File
@@ -374,10 +374,13 @@ const SyncAssetFaceV1Schema = z
}) })
.meta({ id: 'SyncAssetFaceV1' }); .meta({ id: 'SyncAssetFaceV1' });
const SyncAssetFaceV2Schema = SyncAssetFaceV1Schema.extend({ const SyncAssetFaceV2Schema = SyncAssetFaceV1Schema.omit({ personId: true })
deletedAt: isoDatetimeToDate.nullable().describe('Face deleted at'), .extend({
isVisible: z.boolean().describe('Is the face visible in the asset'), deletedAt: isoDatetimeToDate.nullable().describe('Face deleted at'),
}).meta({ id: 'SyncAssetFaceV2' }); isVisible: z.boolean().describe('Is the face visible in the asset'),
faceClusterId: z.string().nullable().describe('Person ID'),
})
.meta({ id: 'SyncAssetFaceV2' });
const SyncAssetFaceDeleteV1Schema = z const SyncAssetFaceDeleteV1Schema = z
.object({ assetFaceId: z.string().describe('Asset face ID') }) .object({ assetFaceId: z.string().describe('Asset face ID') })
+24
View File
@@ -306,6 +306,28 @@ export enum Permission {
AdminAuthUnlinkAll = 'adminAuth.unlinkAll', AdminAuthUnlinkAll = 'adminAuth.unlinkAll',
} }
export enum SharingPermission {
All = 'all',
AssetRead = 'asset.read',
AssetUpdate = 'asset.update',
AssetEdit = 'asset.edit',
AssetDelete = 'asset.delete',
AssetShare = 'asset.share',
ExifRead = 'exif.read',
PersonRead = 'person.read',
PersonUpdate = 'person.update',
PersonMerge = 'person.merge',
PersonDelete = 'person.delete',
}
export const SharingPermissionSchema = z
.enum(SharingPermission)
.describe('Sharing permission schema')
.meta({ id: 'SharingPermission' });
export enum SharedLinkType { export enum SharedLinkType {
Album = 'ALBUM', Album = 'ALBUM',
@@ -404,6 +426,7 @@ export enum ManualJobName {
MemoryCleanup = 'memory-cleanup', MemoryCleanup = 'memory-cleanup',
MemoryCreate = 'memory-create', MemoryCreate = 'memory-create',
BackupDatabase = 'backup-database', BackupDatabase = 'backup-database',
PersonGroupMerge = 'person-group-merge',
} }
export const ManualJobNameSchema = z.enum(ManualJobName).describe('Manual job name').meta({ id: 'ManualJobName' }); export const ManualJobNameSchema = z.enum(ManualJobName).describe('Manual job name').meta({ id: 'ManualJobName' });
@@ -813,6 +836,7 @@ export enum JobName {
FacialRecognitionQueueAll = 'FacialRecognitionQueueAll', FacialRecognitionQueueAll = 'FacialRecognitionQueueAll',
FacialRecognition = 'FacialRecognition', FacialRecognition = 'FacialRecognition',
FacialRecognitionMerge = 'FacialRecognitionMerge',
FileDelete = 'FileDelete', FileDelete = 'FileDelete',
FileMigrationQueueAll = 'FileMigrationQueueAll', FileMigrationQueueAll = 'FileMigrationQueueAll',
+34
View File
@@ -149,6 +149,40 @@ where
"albumAssets"."livePhotoVideoId" "albumAssets"."livePhotoVideoId"
] && array[$2]::uuid[] ] && array[$2]::uuid[]
-- AccessRepository.asset.checkSharedAccess
select
"album_asset"."assetId"
from
"album_asset"
inner join "album_user" on "album_asset"."albumId" = "album_user"."albumId"
and "album_user"."userId" = $1
where
"album_asset"."assetId" in ($2)
and "album_asset"."albumId" in (
select
"album_user"."albumId"
from
"album_user"
where
(
"album_user"."permissions" @> $3::sharing_permission_enum[]
or $4 = any ("album_user"."permissions")
)
)
union
select
"asset"."id" as "assetId"
from
"partner"
inner join "asset" on "asset"."ownerId" = "partner"."sharedById"
and "asset"."id" in ($5)
where
"partner"."sharedWithId" = $6
and (
"partner"."permissions" @> $7::sharing_permission_enum[]
or $8 = any ("partner"."permissions")
)
-- AccessRepository.authDevice.checkOwnerAccess -- AccessRepository.authDevice.checkOwnerAccess
select select
"session"."id" "session"."id"
+52 -14
View File
@@ -182,18 +182,25 @@ select
from from
( (
select select
"asset_face".*, (
"person" as "person" select
to_json(obj)
from
(
select
"person".*
from
"face_cluster"
inner join "person" on "person"."faceClusterId" = "face_cluster"."id"
where
"face_cluster"."id" = "asset_face"."faceClusterId"
limit
$1
) as obj
) as "person",
"asset_face".*
from from
"asset_face" "asset_face"
left join lateral (
select
"person".*
from
"person"
where
"asset_face"."personId" = "person"."id"
) as "person" on true
where where
"asset_face"."assetId" = "asset"."id" "asset_face"."assetId" = "asset"."id"
and "asset_face"."deletedAt" is null and "asset_face"."deletedAt" is null
@@ -224,7 +231,7 @@ from
"asset" "asset"
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId" left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
where where
"asset"."id" = any ($1::uuid[]) "asset"."id" = any ($2::uuid[])
-- AssetRepository.deleteAll -- AssetRepository.deleteAll
delete from "asset" delete from "asset"
@@ -290,13 +297,44 @@ limit
-- AssetRepository.getById -- AssetRepository.getById
select select
"asset".* "asset".*,
(
select
coalesce(json_agg(agg), '[]')
from
(
select distinct
unnest("album_user"."permissions") as "permission"
from
"album_user"
inner join "album_asset" on "album_user"."albumId" = "album_asset"."albumId"
where
"album_asset"."assetId" = "asset"."id"
and "album_user"."userId" = "asset"."ownerId"
and "album_user"."albumId" in (
select
"album_user"."albumId"
from
"album_user"
where
"album_user"."userId" = $1
)
union
select distinct
unnest("partner"."permissions") as "permission"
from
"partner"
where
"partner"."sharedById" = "asset"."ownerId"
and "partner"."sharedWithId" = $2
) as agg
) as "permissions"
from from
"asset" "asset"
where where
"asset"."id" = $1::uuid "asset"."id" = $3::uuid
limit limit
$2 $4
-- AssetRepository.updateAll -- AssetRepository.updateAll
update "asset" update "asset"
+2 -2
View File
@@ -47,7 +47,7 @@ select
$1 as "one" $1 as "one"
from from
"asset_face" "asset_face"
inner join "person" on "person"."id" = "asset_face"."personId" inner join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
where where
"asset_face"."assetId" = "asset"."id" "asset_face"."assetId" = "asset"."id"
and "person"."isHidden" = $2 and "person"."isHidden" = $2
@@ -86,7 +86,7 @@ select
$1 as "one" $1 as "one"
from from
"asset_face" "asset_face"
inner join "person" on "person"."id" = "asset_face"."personId" inner join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
where where
"asset_face"."assetId" = "asset"."id" "asset_face"."assetId" = "asset"."id"
and "person"."isHidden" = $2 and "person"."isHidden" = $2
+181 -33
View File
@@ -3,9 +3,6 @@
-- PersonRepository.reassignFaces -- PersonRepository.reassignFaces
update "asset_face" update "asset_face"
set set
"personId" = $1
where
"asset_face"."personId" = $2
-- PersonRepository.delete -- PersonRepository.delete
delete from "person" delete from "person"
@@ -24,27 +21,64 @@ limit
3 3
-- PersonRepository.getAllForUser -- PersonRepository.getAllForUser
select select distinct
"person".* on ("person"."faceClusterId") "person".*
from from
"person" "person"
inner join "asset_face" on "asset_face"."personId" = "person"."id" inner join "asset_face" on "asset_face"."faceClusterId" = "person"."faceClusterId"
inner join "asset" on "asset_face"."assetId" = "asset"."id" inner join "asset" on "asset_face"."assetId" = "asset"."id"
and "asset"."visibility" = 'timeline' and "asset"."visibility" = 'timeline'
and "asset"."deletedAt" is null and "asset"."deletedAt" is null
where where
"person"."ownerId" = $1 (
"person"."ownerId" = $1
or (
exists (
select
from
"partner"
where
"partner"."sharedById" = "person"."ownerId"
and "partner"."sharedWithId" = $2
and (
$3 = any ("partner"."permissions")
or "partner"."permissions" @> $4
)
)
or exists (
select
from
"album_user"
where
"album_user"."albumId" in (
select
"album_user"."albumId"
from
"album_user"
where
"album_user"."userId" = $5
)
and "album_user"."userId" = "person"."ownerId"
and (
$6 = any ("album_user"."permissions")
or "album_user"."permissions" @> $7
)
)
)
)
and "asset_face"."deletedAt" is null and "asset_face"."deletedAt" is null
and "asset_face"."isVisible" is true and "asset_face"."isVisible" is true
and "person"."isHidden" = $2 and "person"."isHidden" = $8
group by group by
"person"."id" "person"."id"
having having
( (
"person"."name" != $3 "person"."name" != $9
or count("asset_face"."assetId") >= $4 or count("asset_face"."assetId") >= $10
) )
order by order by
"person"."faceClusterId",
"person"."ownerId" = $11 desc,
"person"."isHidden" asc, "person"."isHidden" asc,
"person"."isFavorite" desc, "person"."isFavorite" desc,
NULLIF(person.name, '') is null asc, NULLIF(person.name, '') is null asc,
@@ -52,16 +86,16 @@ order by
NULLIF(person.name, '') asc nulls last, NULLIF(person.name, '') asc nulls last,
"person"."createdAt" "person"."createdAt"
limit limit
$5 $12
offset offset
$6 $13
-- PersonRepository.getAllWithoutFaces -- PersonRepository.getAllWithoutFaces
select select
"person".* "person".*
from from
"person" "person"
left join "asset_face" on "asset_face"."personId" = "person"."id" left join "asset_face" on "asset_face"."faceClusterId" = "person"."faceClusterId"
where where
"asset_face"."deletedAt" is null "asset_face"."deletedAt" is null
and "asset_face"."isVisible" is true and "asset_face"."isVisible" is true
@@ -83,15 +117,26 @@ select
from from
"person" "person"
where where
"person"."id" = "asset_face"."personId" "person"."faceClusterId" = "asset_face"."faceClusterId"
order by
"person"."ownerId" = (
select
"asset"."ownerId"
from
"asset"
where
"asset"."id" = "asset_face"."assetId"
) desc
limit
$1
) as obj ) as obj
) as "person" ) as "person"
from from
"asset_face" "asset_face"
where where
"asset_face"."assetId" = $1 "asset_face"."assetId" = $2
and "asset_face"."deletedAt" is null and "asset_face"."deletedAt" is null
and "asset_face"."isVisible" = $2 and "asset_face"."isVisible" = $3
order by order by
"asset_face"."boundingBoxX1" asc "asset_face"."boundingBoxX1" asc
@@ -108,19 +153,30 @@ select
from from
"person" "person"
where where
"person"."id" = "asset_face"."personId" "person"."faceClusterId" = "asset_face"."faceClusterId"
order by
"person"."ownerId" = (
select
"asset"."ownerId"
from
"asset"
where
"asset"."id" = "asset_face"."assetId"
) desc
limit
$1
) as obj ) as obj
) as "person" ) as "person"
from from
"asset_face" "asset_face"
where where
"asset_face"."id" = $1 "asset_face"."id" = $2
and "asset_face"."deletedAt" is null and "asset_face"."deletedAt" is null
-- PersonRepository.getFaceForFacialRecognitionJob -- PersonRepository.getFaceForFacialRecognitionJob
select select
"asset_face"."id", "asset_face"."id",
"asset_face"."personId", "asset_face"."faceClusterId",
"asset_face"."sourceType", "asset_face"."sourceType",
( (
select select
@@ -190,7 +246,7 @@ where
-- PersonRepository.reassignFace -- PersonRepository.reassignFace
update "asset_face" update "asset_face"
set set
"personId" = $1 "faceClusterId" = $1
where where
"asset_face"."id" = $2 "asset_face"."id" = $2
@@ -209,9 +265,10 @@ where
"person"."ownerId" = $1 "person"."ownerId" = $1
and f_unaccent ("person"."name") %> f_unaccent ($2) and f_unaccent ("person"."name") %> f_unaccent ($2)
order by order by
f_unaccent ("person"."name") <->>> f_unaccent ($3) f_unaccent ("person"."name") <->>> f_unaccent ($3),
"person"."ownerId" = $4 desc
limit limit
$4 $5
-- PersonRepository.getDistinctNames -- PersonRepository.getDistinctNames
select distinct select distinct
@@ -234,9 +291,52 @@ from
and "asset"."visibility" = 'timeline' and "asset"."visibility" = 'timeline'
and "asset"."deletedAt" is null and "asset"."deletedAt" is null
where where
"asset_face"."deletedAt" is null (
"asset"."ownerId" = $1
or exists (
select
from
"partner"
where
"partner"."sharedById" = "asset"."ownerId"
and "partner"."sharedWithId" = $2
and (
$3 = any ("partner"."permissions")
or "partner"."permissions" @> $4
)
)
or exists (
select
from
"album_asset"
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
and "album_user"."userId" = $5
where
"album_asset"."assetId" = "asset"."id"
and "album_user"."albumId" in (
select
"album_user"."albumId"
from
"album_user"
where
"album_user"."userId" = "asset"."ownerId"
and (
$6 = any ("album_user"."permissions")
or "album_user"."permissions" @> $7
)
)
)
)
and "asset_face"."deletedAt" is null
and "asset_face"."isVisible" is true and "asset_face"."isVisible" is true
and "asset_face"."personId" = $1 and "asset_face"."faceClusterId" = (
select
"person"."faceClusterId"
from
"person"
where
"person"."id" = $8
)
-- PersonRepository.getNumberOfPeople -- PersonRepository.getNumberOfPeople
select select
@@ -256,7 +356,7 @@ where
from from
"asset_face" "asset_face"
where where
"asset_face"."personId" = "person"."id" "asset_face"."faceClusterId" = "person"."faceClusterId"
and "asset_face"."deletedAt" is null and "asset_face"."deletedAt" is null
and "asset_face"."isVisible" = $2 and "asset_face"."isVisible" = $2
and exists ( and exists (
@@ -269,7 +369,42 @@ where
and "asset"."deletedAt" is null and "asset"."deletedAt" is null
) )
) )
and "person"."ownerId" = $3 and (
"person"."ownerId" = $3
or (
exists (
select
from
"partner"
where
"partner"."sharedById" = "person"."ownerId"
and "partner"."sharedWithId" = $4
and (
$5 = any ("partner"."permissions")
or "partner"."permissions" @> $6
)
)
or exists (
select
from
"album_user"
where
"album_user"."albumId" in (
select
"album_user"."albumId"
from
"album_user"
where
"album_user"."userId" = $7
)
and "album_user"."userId" = "person"."ownerId"
and (
$8 = any ("album_user"."permissions")
or "album_user"."permissions" @> $9
)
)
)
)
-- PersonRepository.refreshFaces -- PersonRepository.refreshFaces
with with
@@ -299,14 +434,26 @@ select
from from
"person" "person"
where where
"person"."id" = "asset_face"."personId" "person"."faceClusterId" = "asset_face"."faceClusterId"
order by
"person"."ownerId" = (
select
"asset"."ownerId"
from
"asset"
where
"asset"."id" = "asset_face"."assetId"
) desc
limit
$1
) as obj ) as obj
) as "person" ) as "person"
from from
"asset_face" "asset_face"
inner join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
where where
"asset_face"."assetId" in ($1) "person"."id" in ($2)
and "asset_face"."personId" in ($2) and "asset_face"."assetId" in ($3)
and "asset_face"."deletedAt" is null and "asset_face"."deletedAt" is null
-- PersonRepository.getRandomFace -- PersonRepository.getRandomFace
@@ -315,7 +462,7 @@ select
from from
"asset_face" "asset_face"
where where
"asset_face"."personId" = $1 "asset_face"."faceClusterId" = $1
and "asset_face"."deletedAt" is null and "asset_face"."deletedAt" is null
and "asset_face"."isVisible" is true and "asset_face"."isVisible" is true
@@ -351,8 +498,9 @@ select
"asset_face"."id" "asset_face"."id"
from from
"asset_face" "asset_face"
inner join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
and "person"."id" = $1
inner join "asset" on "asset"."id" = "asset_face"."assetId" inner join "asset" on "asset"."id" = "asset_face"."assetId"
and "asset"."isOffline" = $1 and "asset"."isOffline" = $2
where where
"asset_face"."assetId" = $2 "asset_face"."assetId" = $3
and "asset_face"."personId" = $3
+222 -22
View File
@@ -10,15 +10,52 @@ where
"asset"."visibility" = $1 "asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2 and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3 and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[]) and (
and "asset"."isFavorite" = $5 "asset"."ownerId" = $4
or exists (
select
from
"partner"
where
"partner"."sharedById" = "asset"."ownerId"
and "partner"."sharedWithId" = $5
and (
$6 = any ("partner"."permissions")
or "partner"."permissions" @> $7
)
and "partner"."inTimeline" = $8
)
or exists (
select
from
"album_asset"
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
and "album_user"."userId" = $9
where
"album_asset"."assetId" = "asset"."id"
and "album_user"."inTimeline" = $10
and "album_user"."albumId" in (
select
"album_user"."albumId"
from
"album_user"
where
"album_user"."userId" = "asset"."ownerId"
and (
$11 = any ("album_user"."permissions")
or "album_user"."permissions" @> $12
)
)
)
)
and "asset"."isFavorite" = $13
and "asset"."deletedAt" is null and "asset"."deletedAt" is null
order by order by
"asset"."fileCreatedAt" desc "asset"."fileCreatedAt" desc
limit limit
$6 $14
offset offset
$7 $15
-- SearchRepository.searchStatistics -- SearchRepository.searchStatistics
select select
@@ -30,8 +67,45 @@ where
"asset"."visibility" = $1 "asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2 and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3 and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[]) and (
and "asset"."isFavorite" = $5 "asset"."ownerId" = $4
or exists (
select
from
"partner"
where
"partner"."sharedById" = "asset"."ownerId"
and "partner"."sharedWithId" = $5
and (
$6 = any ("partner"."permissions")
or "partner"."permissions" @> $7
)
and "partner"."inTimeline" = $8
)
or exists (
select
from
"album_asset"
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
and "album_user"."userId" = $9
where
"album_asset"."assetId" = "asset"."id"
and "album_user"."inTimeline" = $10
and "album_user"."albumId" in (
select
"album_user"."albumId"
from
"album_user"
where
"album_user"."userId" = "asset"."ownerId"
and (
$11 = any ("album_user"."permissions")
or "album_user"."permissions" @> $12
)
)
)
)
and "asset"."isFavorite" = $13
and "asset"."deletedAt" is null and "asset"."deletedAt" is null
-- SearchRepository.searchRandom -- SearchRepository.searchRandom
@@ -44,13 +118,50 @@ where
"asset"."visibility" = $1 "asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2 and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3 and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[]) and (
and "asset"."isFavorite" = $5 "asset"."ownerId" = $4
or exists (
select
from
"partner"
where
"partner"."sharedById" = "asset"."ownerId"
and "partner"."sharedWithId" = $5
and (
$6 = any ("partner"."permissions")
or "partner"."permissions" @> $7
)
and "partner"."inTimeline" = $8
)
or exists (
select
from
"album_asset"
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
and "album_user"."userId" = $9
where
"album_asset"."assetId" = "asset"."id"
and "album_user"."inTimeline" = $10
and "album_user"."albumId" in (
select
"album_user"."albumId"
from
"album_user"
where
"album_user"."userId" = "asset"."ownerId"
and (
$11 = any ("album_user"."permissions")
or "album_user"."permissions" @> $12
)
)
)
)
and "asset"."isFavorite" = $13
and "asset"."deletedAt" is null and "asset"."deletedAt" is null
order by order by
random() random()
limit limit
$6 $14
-- SearchRepository.searchLargeAssets -- SearchRepository.searchLargeAssets
select select
@@ -63,14 +174,51 @@ where
"asset"."visibility" = $1 "asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2 and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3 and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[]) and (
and "asset"."isFavorite" = $5 "asset"."ownerId" = $4
or exists (
select
from
"partner"
where
"partner"."sharedById" = "asset"."ownerId"
and "partner"."sharedWithId" = $5
and (
$6 = any ("partner"."permissions")
or "partner"."permissions" @> $7
)
and "partner"."inTimeline" = $8
)
or exists (
select
from
"album_asset"
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
and "album_user"."userId" = $9
where
"album_asset"."assetId" = "asset"."id"
and "album_user"."inTimeline" = $10
and "album_user"."albumId" in (
select
"album_user"."albumId"
from
"album_user"
where
"album_user"."userId" = "asset"."ownerId"
and (
$11 = any ("album_user"."permissions")
or "album_user"."permissions" @> $12
)
)
)
)
and "asset"."isFavorite" = $13
and "asset"."deletedAt" is null and "asset"."deletedAt" is null
and "asset_exif"."fileSizeInByte" > $6 and "asset_exif"."fileSizeInByte" > $14
order by order by
"asset_exif"."fileSizeInByte" desc "asset_exif"."fileSizeInByte" desc
limit limit
$7 $15
-- SearchRepository.searchSmart -- SearchRepository.searchSmart
begin begin
@@ -86,15 +234,52 @@ where
"asset"."visibility" = $1 "asset"."visibility" = $1
and "asset"."fileCreatedAt" >= $2 and "asset"."fileCreatedAt" >= $2
and "asset_exif"."lensModel" = $3 and "asset_exif"."lensModel" = $3
and "asset"."ownerId" = any ($4::uuid[]) and (
and "asset"."isFavorite" = $5 "asset"."ownerId" = $4
or exists (
select
from
"partner"
where
"partner"."sharedById" = "asset"."ownerId"
and "partner"."sharedWithId" = $5
and (
$6 = any ("partner"."permissions")
or "partner"."permissions" @> $7
)
and "partner"."inTimeline" = $8
)
or exists (
select
from
"album_asset"
inner join "album_user" on "album_user"."albumId" = "album_asset"."albumId"
and "album_user"."userId" = $9
where
"album_asset"."assetId" = "asset"."id"
and "album_user"."inTimeline" = $10
and "album_user"."albumId" in (
select
"album_user"."albumId"
from
"album_user"
where
"album_user"."userId" = "asset"."ownerId"
and (
$11 = any ("album_user"."permissions")
or "album_user"."permissions" @> $12
)
)
)
)
and "asset"."isFavorite" = $13
and "asset"."deletedAt" is null and "asset"."deletedAt" is null
order by order by
smart_search.embedding <=> $6 smart_search.embedding <=> $14
limit limit
$7 $15
offset offset
$8 $16
commit commit
-- SearchRepository.getEmbedding -- SearchRepository.getEmbedding
@@ -113,15 +298,30 @@ with
"cte" as ( "cte" as (
select select
"asset_face"."id", "asset_face"."id",
"asset_face"."personId", "asset_face"."faceClusterId",
face_search.embedding <=> $1 as "distance" face_search.embedding <=> $1 as "distance",
"asset"."ownerId"
from from
"asset_face" "asset_face"
inner join "asset" on "asset"."id" = "asset_face"."assetId" inner join "asset" on "asset"."id" = "asset_face"."assetId"
inner join "face_search" on "face_search"."faceId" = "asset_face"."id" inner join "face_search" on "face_search"."faceId" = "asset_face"."id"
left join "person" on "person"."id" = "asset_face"."personId" left join "person" on "person"."faceClusterId" = "asset_face"."faceClusterId"
where where
"asset"."ownerId" = any ($2::uuid[]) "asset"."ownerId" in (
select
"user"."id"
from
"user"
where
"user"."trustedGroupId" in (
select
"user"."trustedGroupId"
from
"user"
where
"user"."id" = any ($2::uuid[])
)
)
and "asset"."deletedAt" is null and "asset"."deletedAt" is null
order by order by
"distance" "distance"
+1 -1
View File
@@ -527,7 +527,7 @@ order by
select select
"asset_face"."id", "asset_face"."id",
"assetId", "assetId",
"personId", "faceClusterId",
"imageWidth", "imageWidth",
"imageHeight", "imageHeight",
"boundingBoxX1", "boundingBoxX1",
+70
View File
@@ -397,3 +397,73 @@ set
where where
"user"."deletedAt" is null "user"."deletedAt" is null
and "user"."id" = $2::uuid and "user"."id" = $2::uuid
-- UserRepository.getInSameTrustedGroup
select
"user"."id"
from
"user"
where
"user"."trustedGroupId" = (
select
"user"."trustedGroupId"
from
"user"
where
"user"."id" = $1
)
-- UserRepository.mergeTrustedGroups
update "user"
set
"trustedGroupId" = "u"."trustedGroupId"
from
"user" as "u"
where
"u"."id" = $1
and "user"."trustedGroupId" = (
select
"user"."trustedGroupId"
from
"user"
where
"user"."id" = $2
and "user"."trustedGroupId" != "u"."trustedGroupId"
)
-- UserRepository.updateTrustedGroups
update "user"
set
"trustedGroupId" = uuid_generate_v4 ()
where
"user"."trustedGroupId" = (
select
"user"."trustedGroupId"
from
"user"
where
"user"."id" = $1
)
and "user"."id" != $2
and "user"."id" not in (
select
"partner"."sharedById" as "userId"
from
"partner"
where
"sharedWithId" = $3
union
select
"album_user"."userId"
from
"album_user"
where
"album_user"."albumId" in (
select
"album_user"."albumId"
from
"album_user"
where
"album_user"."userId" = $4
)
)
+74 -1
View File
@@ -2,7 +2,9 @@ import { Injectable } from '@nestjs/common';
import { Kysely, NotNull, sql } from 'kysely'; import { Kysely, NotNull, sql } from 'kysely';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
import { AlbumUserRole, AssetVisibility } from 'src/enum'; import { AlbumUserRole, AssetVisibility, SharingPermission } from 'src/enum';
import { hasAssetPermissions } from 'src/repositories/asset.repository';
import { hasPermissions } from 'src/repositories/person.repository';
import { DB } from 'src/schema'; import { DB } from 'src/schema';
import { asUuid } from 'src/utils/database'; import { asUuid } from 'src/utils/database';
@@ -273,6 +275,46 @@ class AssetAccess {
return allowedIds; return allowedIds;
}); });
} }
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET, [SharingPermission.All]] })
async checkSharedAccess(userId: string, assetIds: Set<string>, permissions: SharingPermission[]) {
const ids = await this.db
.selectFrom('album_asset')
.select('album_asset.assetId')
.where('album_asset.assetId', 'in', [...assetIds])
.where('album_asset.albumId', 'in', (eb) =>
eb
.selectFrom('album_user')
.select('album_user.albumId')
.where((eb) =>
eb.or([
eb('album_user.permissions', '@>', sql<SharingPermission[]>`${permissions}::sharing_permission_enum[]`),
eb(eb.val(SharingPermission.All), '=', eb.fn.any('album_user.permissions')),
]),
),
)
.innerJoin('album_user', (join) =>
join.onRef('album_asset.albumId', '=', 'album_user.albumId').on('album_user.userId', '=', userId),
)
.union((eb) =>
eb
.selectFrom('partner')
.where('partner.sharedWithId', '=', userId)
.where((eb) =>
eb.or([
eb('partner.permissions', '@>', sql<SharingPermission[]>`${permissions}::sharing_permission_enum[]`),
eb(eb.val(SharingPermission.All), '=', eb.fn.any('partner.permissions')),
]),
)
.innerJoin('asset', (join) =>
join.onRef('asset.ownerId', '=', 'partner.sharedById').on('asset.id', 'in', [...assetIds]),
)
.select('asset.id as assetId'),
)
.execute();
return new Set(ids.map(({ assetId }) => assetId));
}
} }
class AuthDeviceAccess { class AuthDeviceAccess {
@@ -452,6 +494,37 @@ class PersonAccess {
.execute() .execute()
.then((faces) => new Set(faces.map((face) => face.id))); .then((faces) => new Set(faces.map((face) => face.id)));
} }
async checkSharedAccess(userId: string, personIds: Set<string>, permissions: SharingPermission[]) {
if (personIds.size === 0) {
return new Set<string>();
}
const ids = await this.db
.selectFrom('person')
.select('person.id')
.where('person.id', 'in', [...personIds])
.where(hasPermissions(userId, permissions))
.execute();
return new Set(ids.map(({ id }) => id));
}
async checkSharedFaceAccess(userId: string, faceIds: Set<string>, permissions: SharingPermission[]) {
if (faceIds.size === 0) {
return new Set<string>();
}
const ids = await this.db
.selectFrom('asset_face')
.select('asset_face.id')
.leftJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId'))
.where('asset_face.id', 'in', [...faceIds])
.where(hasAssetPermissions(userId, permissions))
.execute();
return new Set(ids.map(({ id }) => id));
}
} }
class PartnerAccess { class PartnerAccess {
@@ -38,4 +38,13 @@ export class AlbumUserRepository {
async delete({ userId, albumId }: AlbumPermissionId): Promise<void> { async delete({ userId, albumId }: AlbumPermissionId): Promise<void> {
await this.db.deleteFrom('album_user').where('userId', '=', userId).where('albumId', '=', albumId).execute(); await this.db.deleteFrom('album_user').where('userId', '=', userId).where('albumId', '=', albumId).execute();
} }
get({ userId, albumId }: AlbumPermissionId) {
return this.db
.selectFrom('album_user')
.select(['permissions', 'inTimeline'])
.where('userId', '=', userId)
.where('albumId', '=', albumId)
.executeTakeFirstOrThrow();
}
} }
+65 -5
View File
@@ -17,7 +17,15 @@ import { InjectKysely } from 'nestjs-kysely';
import { LockableProperty, Stack } from 'src/database'; import { LockableProperty, Stack } from 'src/database';
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { AssetFileType, AssetOrder, AssetOrderBy, AssetStatus, AssetType, AssetVisibility } from 'src/enum'; import {
AssetFileType,
AssetOrder,
AssetOrderBy,
AssetStatus,
AssetType,
AssetVisibility,
SharingPermission,
} from 'src/enum';
import { DB } from 'src/schema'; import { DB } from 'src/schema';
import { AssetAudioTable, AssetKeyframeTable, AssetVideoTable } from 'src/schema/tables/asset-av.table'; import { AssetAudioTable, AssetKeyframeTable, AssetVideoTable } from 'src/schema/tables/asset-av.table';
import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
@@ -41,6 +49,7 @@ import {
withFiles, withFiles,
withLibrary, withLibrary,
withOwner, withOwner,
withPermissions,
withSmartSearch, withSmartSearch,
withTagId, withTagId,
withTags, withTags,
@@ -165,6 +174,47 @@ const withBoundingBox = <T>(qb: SelectQueryBuilder<DB, 'asset' | 'asset_exif', T
); );
}; };
export const hasAssetPermissions =
(userId: string, permissions: SharingPermission[], ignoreTimelineVisibility: boolean = false) =>
(eb: ExpressionBuilder<DB, 'asset'>) =>
eb.or([
eb('asset.ownerId', '=', userId),
eb.exists(
eb
.selectFrom('partner')
.whereRef('partner.sharedById', '=', 'asset.ownerId')
.where('partner.sharedWithId', '=', userId)
.where((eb) =>
eb.or([
eb(eb.val(SharingPermission.All), '=', eb.fn.any('partner.permissions')),
eb('partner.permissions', '@>', eb.val(permissions)),
]),
)
.$if(!ignoreTimelineVisibility, (qb) => qb.where('partner.inTimeline', '=', true)),
),
eb.exists(
eb
.selectFrom('album_asset')
.whereRef('album_asset.assetId', '=', 'asset.id')
.innerJoin('album_user', (join) =>
join.onRef('album_user.albumId', '=', 'album_asset.albumId').on('album_user.userId', '=', userId),
)
.$if(!ignoreTimelineVisibility, (qb) => qb.where('album_user.inTimeline', '=', true))
.where('album_user.albumId', 'in', (eb) =>
eb
.selectFrom('album_user')
.select('album_user.albumId')
.whereRef('album_user.userId', '=', 'asset.ownerId')
.where((eb) =>
eb.or([
eb(eb.val(SharingPermission.All), '=', eb.fn.any('album_user.permissions')),
eb('album_user.permissions', '@>', eb.val(permissions)),
]),
),
),
),
]);
@Injectable() @Injectable()
export class AssetRepository { export class AssetRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {} constructor(@InjectKysely() private db: Kysely<DB>) {}
@@ -556,17 +606,22 @@ export class AssetRepository {
.executeTakeFirst(); .executeTakeFirst();
} }
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID, {}, DummyValue.UUID] })
getById( getById(
id: string, id: string,
{ exifInfo, faces, files, library, owner, smartSearch, stack, tags, edits }: GetByIdsRelations = {}, { exifInfo, faces, files, library, owner, smartSearch, stack, tags, edits }: GetByIdsRelations = {},
userId?: string,
) { ) {
return this.db return this.db
.selectFrom('asset') .selectFrom('asset')
.selectAll('asset') .selectAll('asset')
.where('asset.id', '=', asUuid(id)) .where('asset.id', '=', asUuid(id))
.$if(!!exifInfo, withExif) .$if(!!exifInfo, withExif)
.$if(!!faces, (qb) => qb.select(faces?.person ? withFacesAndPeople : withFaces).$narrowType<{ faces: NotNull }>()) .$if(!!faces, (qb) =>
qb
.select(faces?.person ? (eb) => withFacesAndPeople(eb, { userId }) : withFaces)
.$narrowType<{ faces: NotNull }>(),
)
.$if(!!library, (qb) => qb.select(withLibrary)) .$if(!!library, (qb) => qb.select(withLibrary))
.$if(!!owner, (qb) => qb.select(withOwner)) .$if(!!owner, (qb) => qb.select(withOwner))
.$if(!!smartSearch, withSmartSearch) .$if(!!smartSearch, withSmartSearch)
@@ -602,6 +657,7 @@ export class AssetRepository {
.$if(!!files, (qb) => qb.select(withFiles)) .$if(!!files, (qb) => qb.select(withFiles))
.$if(!!tags, (qb) => qb.select(withTags)) .$if(!!tags, (qb) => qb.select(withTags))
.$if(!!edits, (qb) => qb.select(withEdits)) .$if(!!edits, (qb) => qb.select(withEdits))
.$if(!!userId, (qb) => qb.select(withPermissions(userId!)))
.limit(1) .limit(1)
.executeTakeFirst(); .executeTakeFirst();
} }
@@ -744,7 +800,9 @@ export class AssetRepository {
) )
.where((eb) => eb.or([eb('asset.stackId', 'is', null), eb(eb.table('stack'), 'is not', null)])), .where((eb) => eb.or([eb('asset.stackId', 'is', null), eb(eb.table('stack'), 'is not', null)])),
) )
.$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!))) .$if(!!options.userIds, (qb) =>
qb.where(hasAssetPermissions(options.userIds![0], [SharingPermission.AssetRead], !!options.personId)),
)
.$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!)) .$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!))
.$if(!!options.assetType, (qb) => qb.where('asset.type', '=', options.assetType!)) .$if(!!options.assetType, (qb) => qb.where('asset.type', '=', options.assetType!))
.$if(options.isDuplicate !== undefined, (qb) => .$if(options.isDuplicate !== undefined, (qb) =>
@@ -829,7 +887,9 @@ export class AssetRepository {
), ),
) )
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!])) .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
.$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!))) .$if(!!options.userIds, (qb) =>
qb.where(hasAssetPermissions(options.userIds![0], [SharingPermission.AssetRead], !!options.personId)),
)
.$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!)) .$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!))
.$if(!!options.withStacked, (qb) => .$if(!!options.withStacked, (qb) =>
qb qb
+9 -6
View File
@@ -15,7 +15,7 @@ import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/mis
type JobMapItem = { type JobMapItem = {
jobName: JobName; jobName: JobName;
queueName: QueueName; queueName: QueueName;
handler: (job: JobOf<any>) => Promise<JobStatus>; handler: (job?: JobOf<any>) => Promise<JobStatus>;
label: string; label: string;
}; };
@@ -95,14 +95,17 @@ export class JobRepository {
} }
} }
async run({ name, data }: JobItem) { async run(job: JobItem) {
const item = this.handlers[name as JobName]; const item = this.handlers[job.name];
if (!item) { if (!item) {
this.logger.warn(`Skipping unknown job: "${name}"`); this.logger.warn(`Skipping unknown job: "${job.name}"`);
return JobStatus.Skipped; return JobStatus.Skipped;
} }
return item.handler(data); if ('data' in job) {
return item.handler(job.data);
}
return item.handler();
} }
setConcurrency(queueName: QueueName, concurrency: number) { setConcurrency(queueName: QueueName, concurrency: number) {
@@ -167,7 +170,7 @@ export class JobRepository {
const queueName = this.getQueueName(item.name); const queueName = this.getQueueName(item.name);
const job = { const job = {
name: item.name, name: item.name,
data: item.data || {}, data: ('data' in item ? item.data : undefined) || {},
options: this.getJobOptions(item) || undefined, options: this.getJobOptions(item) || undefined,
} as JobItem & { data: any; options: JobsOptions | undefined }; } as JobItem & { data: any; options: JobsOptions | undefined };
+1 -1
View File
@@ -73,7 +73,7 @@ export class MemoryRepository implements IBulkAsset {
eb.exists( eb.exists(
eb eb
.selectFrom('asset_face') .selectFrom('asset_face')
.innerJoin('person', 'person.id', 'asset_face.personId') .innerJoin('person', 'person.faceClusterId', 'asset_face.faceClusterId')
.select((eb) => eb.val(1).as('one')) .select((eb) => eb.val(1).as('one'))
.whereRef('asset_face.assetId', '=', 'asset.id') .whereRef('asset_face.assetId', '=', 'asset.id')
.where('person.isHidden', '=', true), .where('person.isHidden', '=', true),
+107 -29
View File
@@ -4,7 +4,8 @@ import { jsonObjectFrom } from 'kysely/helpers/postgres';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
import { AssetFace } from 'src/database'; import { AssetFace } from 'src/database';
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
import { AssetFileType, AssetVisibility, SourceType } from 'src/enum'; import { AssetFileType, AssetVisibility, SharingPermission, SourceType } from 'src/enum';
import { hasAssetPermissions } from 'src/repositories/asset.repository';
import { DB } from 'src/schema'; import { DB } from 'src/schema';
import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { FaceSearchTable } from 'src/schema/tables/face-search.table'; import { FaceSearchTable } from 'src/schema/tables/face-search.table';
@@ -33,9 +34,9 @@ export interface AssetFaceId {
} }
export interface UpdateFacesData { export interface UpdateFacesData {
oldPersonId?: string; oldFaceClusterId?: string;
faceIds?: string[]; faceIds?: string[];
newPersonId: string; newFaceClusterId: string;
} }
export interface PersonStatistics { export interface PersonStatistics {
@@ -54,7 +55,7 @@ export interface GetAllPeopleOptions {
} }
export interface GetAllFacesOptions { export interface GetAllFacesOptions {
personId?: string | null; faceClusterId?: string | null;
assetId?: string; assetId?: string;
sourceType?: SourceType; sourceType?: SourceType;
} }
@@ -63,9 +64,27 @@ export type UnassignFacesOptions = DeleteFacesOptions;
export type SelectFaceOptions = (keyof Selectable<AssetFaceTable>)[]; export type SelectFaceOptions = (keyof Selectable<AssetFaceTable>)[];
const withPerson = (eb: ExpressionBuilder<DB, 'asset_face'>) => { const withPerson = (eb: ExpressionBuilder<DB, 'asset_face'>, userId?: string) => {
return jsonObjectFrom( return jsonObjectFrom(
eb.selectFrom('person').selectAll('person').whereRef('person.id', '=', 'asset_face.personId'), eb
.selectFrom('person')
.selectAll('person')
.whereRef('person.faceClusterId', '=', 'asset_face.faceClusterId')
.$if(!!userId, (qb) =>
qb.where((eb) =>
eb.or([eb('person.ownerId', '=', userId!), hasPermissions(userId!, [SharingPermission.PersonRead])(eb)]),
),
)
.orderBy(
(eb) =>
eb(
'person.ownerId',
'=',
eb.selectFrom('asset').select('asset.ownerId').whereRef('asset.id', '=', 'asset_face.assetId'),
),
'desc',
)
.limit(1),
).as('person'); ).as('person');
}; };
@@ -75,16 +94,47 @@ const withFaceSearch = (eb: ExpressionBuilder<DB, 'asset_face'>) => {
).as('faceSearch'); ).as('faceSearch');
}; };
export const hasPermissions =
(userId: string, permissions: SharingPermission[]) => (eb: ExpressionBuilder<DB, 'person'>) =>
eb.or([
eb.exists((eb) =>
eb
.selectFrom('partner')
.whereRef('partner.sharedById', '=', 'person.ownerId')
.where('partner.sharedWithId', '=', userId)
.where((eb) =>
eb.or([
eb(eb.val(SharingPermission.All), '=', eb.fn.any('partner.permissions')),
eb('partner.permissions', '@>', eb.val(permissions)),
]),
),
),
eb.exists((eb) =>
eb
.selectFrom('album_user')
.where('album_user.albumId', 'in', (eb) =>
eb.selectFrom('album_user').select('album_user.albumId').where('album_user.userId', '=', userId),
)
.whereRef('album_user.userId', '=', 'person.ownerId')
.where((eb) =>
eb.or([
eb(eb.val(SharingPermission.All), '=', eb.fn.any('album_user.permissions')),
eb('album_user.permissions', '@>', eb.val(permissions)),
]),
),
),
]);
@Injectable() @Injectable()
export class PersonRepository { export class PersonRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {} constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] }) @GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] })
async reassignFaces({ oldPersonId, faceIds, newPersonId }: UpdateFacesData): Promise<number> { async reassignFaces({ oldFaceClusterId, faceIds, newFaceClusterId }: UpdateFacesData): Promise<number> {
const result = await this.db const result = await this.db
.updateTable('asset_face') .updateTable('asset_face')
.set({ personId: newPersonId }) .set({ faceClusterId: newFaceClusterId })
.$if(!!oldPersonId, (qb) => qb.where('asset_face.personId', '=', oldPersonId!)) .$if(!!oldFaceClusterId, (qb) => qb.where('asset_face.faceClusterId', '=', oldFaceClusterId!))
.$if(!!faceIds, (qb) => qb.where('asset_face.id', 'in', faceIds!)) .$if(!!faceIds, (qb) => qb.where('asset_face.id', 'in', faceIds!))
.executeTakeFirst(); .executeTakeFirst();
@@ -94,7 +144,7 @@ export class PersonRepository {
async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> { async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
await this.db await this.db
.updateTable('asset_face') .updateTable('asset_face')
.set({ personId: null }) .set({ faceClusterId: null })
.where('asset_face.sourceType', '=', sourceType) .where('asset_face.sourceType', '=', sourceType)
.execute(); .execute();
} }
@@ -117,8 +167,8 @@ export class PersonRepository {
return this.db return this.db
.selectFrom('asset_face') .selectFrom('asset_face')
.selectAll('asset_face') .selectAll('asset_face')
.$if(options.personId === null, (qb) => qb.where('asset_face.personId', 'is', null)) .$if(options.faceClusterId === null, (qb) => qb.where('asset_face.faceClusterId', 'is', null))
.$if(!!options.personId, (qb) => qb.where('asset_face.personId', '=', options.personId!)) .$if(!!options.faceClusterId, (qb) => qb.where('asset_face.faceClusterId', '=', options.faceClusterId!))
.$if(!!options.sourceType, (qb) => qb.where('asset_face.sourceType', '=', options.sourceType!)) .$if(!!options.sourceType, (qb) => qb.where('asset_face.sourceType', '=', options.sourceType!))
.$if(!!options.assetId, (qb) => qb.where('asset_face.assetId', '=', options.assetId!)) .$if(!!options.assetId, (qb) => qb.where('asset_face.assetId', '=', options.assetId!))
.where('asset_face.deletedAt', 'is', null) .where('asset_face.deletedAt', 'is', null)
@@ -153,16 +203,20 @@ export class PersonRepository {
const items = await this.db const items = await this.db
.selectFrom('person') .selectFrom('person')
.selectAll('person') .selectAll('person')
.innerJoin('asset_face', 'asset_face.personId', 'person.id') .innerJoin('asset_face', 'asset_face.faceClusterId', 'person.faceClusterId')
.innerJoin('asset', (join) => .innerJoin('asset', (join) =>
join join
.onRef('asset_face.assetId', '=', 'asset.id') .onRef('asset_face.assetId', '=', 'asset.id')
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) .on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
.on('asset.deletedAt', 'is', null), .on('asset.deletedAt', 'is', null),
) )
.where('person.ownerId', '=', userId) .where((eb) =>
eb.or([eb('person.ownerId', '=', userId), hasPermissions(userId, [SharingPermission.PersonRead])(eb)]),
)
.where('asset_face.deletedAt', 'is', null) .where('asset_face.deletedAt', 'is', null)
.where('asset_face.isVisible', 'is', true) .where('asset_face.isVisible', 'is', true)
.orderBy('person.faceClusterId')
.orderBy((eb) => eb('person.ownerId', '=', userId), 'desc')
.orderBy('person.isHidden', 'asc') .orderBy('person.isHidden', 'asc')
.orderBy('person.isFavorite', 'desc') .orderBy('person.isFavorite', 'desc')
.having((eb) => .having((eb) =>
@@ -171,6 +225,7 @@ export class PersonRepository {
eb((innerEb) => innerEb.fn.count('asset_face.assetId'), '>=', options?.minimumFaceCount || 1), eb((innerEb) => innerEb.fn.count('asset_face.assetId'), '>=', options?.minimumFaceCount || 1),
]), ]),
) )
.distinctOn('person.faceClusterId')
.groupBy('person.id') .groupBy('person.id')
.$if(!!options?.closestFaceAssetId, (qb) => .$if(!!options?.closestFaceAssetId, (qb) =>
qb.orderBy((eb) => qb.orderBy((eb) =>
@@ -209,7 +264,7 @@ export class PersonRepository {
return this.db return this.db
.selectFrom('person') .selectFrom('person')
.selectAll('person') .selectAll('person')
.leftJoin('asset_face', 'asset_face.personId', 'person.id') .leftJoin('asset_face', 'asset_face.faceClusterId', 'person.faceClusterId')
.where('asset_face.deletedAt', 'is', null) .where('asset_face.deletedAt', 'is', null)
.where('asset_face.isVisible', 'is', true) .where('asset_face.isVisible', 'is', true)
.having((eb) => eb.fn.count('asset_face.assetId'), '=', 0) .having((eb) => eb.fn.count('asset_face.assetId'), '=', 0)
@@ -218,13 +273,13 @@ export class PersonRepository {
} }
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
getFaces(assetId: string, options?: { isVisible?: boolean }) { getFaces(assetId: string, options: { isVisible?: boolean; userId?: string } = {}) {
const isVisible = options === undefined ? true : options.isVisible; const { isVisible = true, userId } = options;
return this.db return this.db
.selectFrom('asset_face') .selectFrom('asset_face')
.selectAll('asset_face') .selectAll('asset_face')
.select(withPerson) .select((eb) => withPerson(eb, userId))
.where('asset_face.assetId', '=', assetId) .where('asset_face.assetId', '=', assetId)
.where('asset_face.deletedAt', 'is', null) .where('asset_face.deletedAt', 'is', null)
.$if(isVisible !== undefined, (qb) => qb.where('asset_face.isVisible', '=', isVisible!)) .$if(isVisible !== undefined, (qb) => qb.where('asset_face.isVisible', '=', isVisible!))
@@ -248,7 +303,7 @@ export class PersonRepository {
getFaceForFacialRecognitionJob(id: string) { getFaceForFacialRecognitionJob(id: string) {
return this.db return this.db
.selectFrom('asset_face') .selectFrom('asset_face')
.select(['asset_face.id', 'asset_face.personId', 'asset_face.sourceType']) .select(['asset_face.id', 'asset_face.faceClusterId', 'asset_face.sourceType'])
.select((eb) => .select((eb) =>
jsonObjectFrom( jsonObjectFrom(
eb eb
@@ -289,10 +344,10 @@ export class PersonRepository {
} }
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
async reassignFace(assetFaceId: string, newPersonId: string): Promise<number> { async reassignFace(assetFaceId: string, newFaceClusterId: string): Promise<number> {
const result = await this.db const result = await this.db
.updateTable('asset_face') .updateTable('asset_face')
.set({ personId: newPersonId }) .set({ faceClusterId: newFaceClusterId })
.where('asset_face.id', '=', assetFaceId) .where('asset_face.id', '=', assetFaceId)
.executeTakeFirst(); .executeTakeFirst();
@@ -318,6 +373,7 @@ export class PersonRepository {
.where('person.ownerId', '=', userId) .where('person.ownerId', '=', userId)
.where(() => sql`f_unaccent("person"."name") %> f_unaccent(${personName})`) .where(() => sql`f_unaccent("person"."name") %> f_unaccent(${personName})`)
.orderBy(sql`f_unaccent("person"."name") <->>> f_unaccent(${personName})`) .orderBy(sql`f_unaccent("person"."name") <->>> f_unaccent(${personName})`)
.orderBy((eb) => eb('person.ownerId', '=', userId), 'desc')
.limit(100) .limit(100)
.$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false)) .$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false))
.execute(); .execute();
@@ -335,7 +391,7 @@ export class PersonRepository {
} }
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
async getStatistics(personId: string): Promise<PersonStatistics> { async getStatistics(userId: string, personId: string): Promise<PersonStatistics> {
const result = await this.db const result = await this.db
.selectFrom('asset_face') .selectFrom('asset_face')
.leftJoin('asset', (join) => .leftJoin('asset', (join) =>
@@ -344,10 +400,13 @@ export class PersonRepository {
.on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) .on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
.on('asset.deletedAt', 'is', null), .on('asset.deletedAt', 'is', null),
) )
.where(hasAssetPermissions(userId, [SharingPermission.AssetRead], true))
.select((eb) => eb.fn.count(eb.fn('distinct', ['asset.id'])).as('count')) .select((eb) => eb.fn.count(eb.fn('distinct', ['asset.id'])).as('count'))
.where('asset_face.deletedAt', 'is', null) .where('asset_face.deletedAt', 'is', null)
.where('asset_face.isVisible', 'is', true) .where('asset_face.isVisible', 'is', true)
.where('asset_face.personId', '=', personId) .where('asset_face.faceClusterId', '=', (eb) =>
eb.selectFrom('person').select('person.faceClusterId').where('person.id', '=', personId),
)
.executeTakeFirst(); .executeTakeFirst();
return { return {
@@ -364,7 +423,7 @@ export class PersonRepository {
eb.exists((eb) => eb.exists((eb) =>
eb eb
.selectFrom('asset_face') .selectFrom('asset_face')
.whereRef('asset_face.personId', '=', 'person.id') .whereRef('asset_face.faceClusterId', '=', 'person.faceClusterId')
.where('asset_face.deletedAt', 'is', null) .where('asset_face.deletedAt', 'is', null)
.where('asset_face.isVisible', '=', true) .where('asset_face.isVisible', '=', true)
.where((eb) => .where((eb) =>
@@ -378,13 +437,20 @@ export class PersonRepository {
), ),
), ),
) )
.where('person.ownerId', '=', userId) .where((eb) =>
eb.or([eb('person.ownerId', '=', userId), hasPermissions(userId, [SharingPermission.PersonRead])(eb)]),
)
.select((eb) => eb.fn.coalesce(eb.fn.countAll<number>(), zero).as('total')) .select((eb) => eb.fn.coalesce(eb.fn.countAll<number>(), zero).as('total'))
.select((eb) => eb.fn.coalesce(eb.fn.countAll<number>().filterWhere('isHidden', '=', true), zero).as('hidden')) .select((eb) => eb.fn.coalesce(eb.fn.countAll<number>().filterWhere('isHidden', '=', true), zero).as('hidden'))
.executeTakeFirstOrThrow(); .executeTakeFirstOrThrow();
} }
create(person: Insertable<PersonTable>) { async create(person: Insertable<PersonTable>) {
if (!person.faceClusterId) {
const { id } = await this.db.insertInto('face_cluster').defaultValues().returning('id').executeTakeFirstOrThrow();
person.faceClusterId = id;
}
return this.db.insertInto('person').values(person).returningAll().executeTakeFirstOrThrow(); return this.db.insertInto('person').values(person).returningAll().executeTakeFirstOrThrow();
} }
@@ -475,8 +541,9 @@ export class PersonRepository {
.selectFrom('asset_face') .selectFrom('asset_face')
.selectAll('asset_face') .selectAll('asset_face')
.select(withPerson) .select(withPerson)
.innerJoin('person', (join) => join.onRef('person.faceClusterId', '=', 'asset_face.faceClusterId'))
.where('person.id', 'in', personIds)
.where('asset_face.assetId', 'in', assetIds) .where('asset_face.assetId', 'in', assetIds)
.where('asset_face.personId', 'in', personIds)
.where('asset_face.deletedAt', 'is', null) .where('asset_face.deletedAt', 'is', null)
.execute(); .execute();
} }
@@ -486,7 +553,12 @@ export class PersonRepository {
return this.db return this.db
.selectFrom('asset_face') .selectFrom('asset_face')
.selectAll('asset_face') .selectAll('asset_face')
.where('asset_face.personId', '=', personId) .innerJoin('person', (join) =>
join.onRef('asset_face.faceClusterId', '=', 'person.faceClusterId').on('person.id', '=', personId),
)
.where('asset_face.assetId', 'in', (eb) =>
eb.selectFrom('asset').select('asset.id').whereRef('asset.ownerId', '=', 'person.ownerId'),
)
.where('asset_face.deletedAt', 'is', null) .where('asset_face.deletedAt', 'is', null)
.where('asset_face.isVisible', 'is', true) .where('asset_face.isVisible', 'is', true)
.executeTakeFirst(); .executeTakeFirst();
@@ -573,8 +645,14 @@ export class PersonRepository {
.selectFrom('asset_face') .selectFrom('asset_face')
.select('asset_face.id') .select('asset_face.id')
.where('asset_face.assetId', '=', assetId) .where('asset_face.assetId', '=', assetId)
.where('asset_face.personId', '=', personId) .innerJoin('person', (join) =>
join.onRef('person.faceClusterId', '=', 'asset_face.faceClusterId').on('person.id', '=', personId),
)
.innerJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId').on('asset.isOffline', '=', false)) .innerJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId').on('asset.isOffline', '=', false))
.executeTakeFirst(); .executeTakeFirst();
} }
getByFaceClusterId(faceClusterId: string) {
return this.db.selectFrom('person').selectAll().where('person.faceClusterId', '=', faceClusterId).execute();
}
} }

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