Compare commits

..

37 Commits

Author SHA1 Message Date
shenlong-tanwen
fa4c6aa562 chore: log failures from share intent upload 2025-12-12 11:12:27 -06:00
Alex
4b345e02ff fix: refresh appear in list after asset is added to a current or new album (#24523) 2025-12-11 11:06:53 -06:00
Yaros
8cf900bafa fix(mobile): local videos with '#' don't play on android (#24373)
* fix(mobile): videos with '#' don't play on android

* refactor: one line

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>

* fix: depend on platform

---------

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>
2025-12-11 10:57:37 -06:00
Yaros
59a3f0f455 feat(mobile): create new album from add to modal (#24431)
* feat(mobile): create new album from add to modal

* refactor: use statefulwidget instead of hook

* chore: rename createalbumbutton
2025-12-11 09:47:31 -06:00
Sergey Katsubo
c5d99711f7 fix(web): show inferred timezone in date editor (#24513)
fix(web): show inferred timezone of asset in date editor
2025-12-11 09:20:51 -06:00
Yaros
4c0a41723f feat(web): asset selection bar in tags view (#24522)
* feat(web): asset selection tab in tags view

* chore: remove unused imports
2025-12-11 15:20:29 +00:00
Bart van Velden
f73511a754 fix(docs): typo in maintenance mode command (#24518) 2025-12-11 09:19:33 -06:00
hubert-taieb
e637387082 fix(server): prevent metadata extraction failures on large video files (#24094)
* prevent  metadata extraction failures on large video files

Increases ExifTool timeout from 20s to 120s to prevent GPS metadata
extraction failures on large video files (>2GB, 10+ minutes).

Issue: Large videos timeout during metadata extraction, causing GPS
coordinates to be lost even though ExifTool can extract them given
enough time.

Testing: 2.6GB, 10:52min video that previously timed out now
successfully extracts GPS metadata.

* redundant comment

Increased task timeout for processing large videos.

* chore: lint

---------

Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-12-11 15:18:19 +00:00
renovate[bot]
baad38f0e6 fix(deps): update typescript-projects (#24476)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2025-12-11 00:13:06 +00:00
Min Idzelis
161147af51 feat: timeline-manager improvement to use AssetResponseDto efficiently (#24421) 2025-12-11 01:07:31 +01:00
renovate[bot]
cbdf5011f9 chore(deps): update docker.io/valkey/valkey:9 docker digest to fb8d272 (#24474)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 00:59:39 +01:00
renovate[bot]
f0f1d279c4 chore(deps): update prom/prometheus docker digest to d936808 (#24475)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 00:59:20 +01:00
renovate[bot]
5821f2fe61 chore(deps): update github-actions (#24477)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-11 00:59:03 +01:00
Noel S
4cbce072be fix(docs): slow upload speed with example nginx reverse proxy config (#24490)
* increase buffer size

* increase further

* increase buffer further
2025-12-10 22:29:36 +00:00
idubnori
5e5bb7e87d fix(mobile): versionStatus.message text overflow (#24504) 2025-12-10 16:18:55 -06:00
shenlong
b052893a1e feat(mobile): immich-ui icon button (#24502)
* feat(mobile): immich-ui icon button

* fix lint

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-12-10 16:18:01 -06:00
Kurt McKee
15e58595fd fix(mobile): iOS local permission dialog extra whitespace (#24491)
Fix a iOS rendering issue caused by extra whitespace
2025-12-10 16:17:08 -06:00
Alex
6d499c782a chore: update ui lib (#24483) 2025-12-09 17:27:01 -06:00
idubnori
7af99b8606 feat(mobile): move top bar buttons into kebabu menu in AssetViewer (#24461)
* chore(mobile):  i18n: "open_asset_info" in viewer kebab menu

* feat(mobile): move some top buttons into kebabu menu

* refactor(mobile): viewer kebab menu to use context-based button generation

* feat(mobile): refactor action button and kebab menu to use ConsumerWidget for improved state management

* feat(mobile): pass original theme to ViewerKebabMenu for consistent styling

* chore: styling

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-12-09 18:26:28 +00:00
Arnau Mora
01e39277e0 feat(mobile): Localized backup upload details page (#21136)
* Localized backup details page

# Conflicts:
#	i18n/en.json

* Format

* format fix

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-12-09 11:23:01 -06:00
Yaros
06e79703da fix(mobile): timeline bottom padding on selection (#24480) 2025-12-09 09:19:41 -06:00
Yaros
c360781565 fix(mobile): fix overflow text in backup card (#24448)
* fix(mobile): fix overflow text in backup card

* refactor: use intrinsicheight

* chore: fix spelling of entitycounttile
2025-12-09 09:03:29 -06:00
idubnori
287f6d5c94 fix(mobile): buttons inside AddActionButton color is the same as background color (#24460)
* fix: icon & text color in AddActionButton

* fix: use Divider
2025-12-08 14:29:31 -06:00
Simon Kubiak
fe9125a3d1 fix(web): [album table view] long album title overflows table row (#24450)
fix(web): long album title overflows vertically on album page in table view
2025-12-08 15:35:58 +00:00
Yaros
8b31936bb6 fix(mobile): cannot create album while name field is focused (#24449)
fix(mobile): create album disabled when focused
2025-12-08 09:33:01 -06:00
Sergey Katsubo
19958dfd83 fix(server): building docker image for different platforms on the same host (#24459)
Fix building docker image for different platforms on the same host

Use per-platform mise cache to avoid 'sh: 1: extism-js: not found'
This happens due to re-using cached installed binary for another platform
2025-12-08 09:15:43 -06:00
Alex
1e1cf0d1fe fix: build iOS fastlane installation (#24408) 2025-12-06 14:55:53 -06:00
Min Idzelis
879e0ea131 fix: thumbnail doesnt send mouseLeave events properly (#24423) 2025-12-06 21:52:06 +01:00
Sergey Katsubo
42136f9091 fix(server): update exiftool-vendored to v34 for more robust metadata extraction (#24424) 2025-12-06 14:45:59 -06:00
Harrison
1109c32891 fix(docs): websockets in nginx example (#24411)
Co-authored-by: Harrison <frith.harry@gmail.com>
2025-12-06 16:28:12 +00:00
idubnori
3c80049192 chore(mobile): add kebabu menu in asset viewer (#24387)
* feat(mobile): implement viewer kebab menu with about option

* feat: revert exisitng buttons, adjust label name

* unify MenuAnchor usage

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-12-05 19:51:59 +00:00
Hai Sullivan
8f1669efbe chore(mobile): smoother UI experience for iOS devices (#24397)
allows the tab pages to use the standard Material page transition during push/pop navigation
2025-12-05 11:02:04 -06:00
Robert Schäfer
146bf65d02 refactor(dev): remove ulimits for rootless docker (#24393)
Description
-----------

When I follow the [developer setup](https://docs.immich.app/developer/setup) I run into a permission error using rootless docker. A while ago I asked on Discord in [#contributing](https://discord.com/channels/979116623879368755/1071165397228855327/1442974448776122592) about these ulimits.

I suggest to remove the `ulimits` altogether. It seems that @ItalyPaleAle has left the setting just hoping that it could help somebody in the future. See the [PR description](https://github.com/immich-app/immich/pull/4556).

How Has This Been Tested?
-------------------------

Using rootless docker:

```
$ docker context ls
NAME         DESCRIPTION                               DOCKER ENDPOINT                     ERROR
default                                                unix:///var/run/docker.sock
rootless *                                             unix:///run/user/1000/docker.sock
```

Running `make` will fail because of permission errors:
```
$  docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
...
Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error setting rlimits for ready process: error setting rlimit type 7: operation not permitted
```

On my machine I have the following hard limit for "Maximum number of open file descriptors":
```
$ ulimit -nH
524288
```

I can confirm that the permission error is caused by the security restrictions of the operating system mentioned above:

Changing `docker/docker-compose.dev.yml` like ..

```
    ulimits:
      nofile:
        soft: 524289
        hard: 524289
```

.. will lead to a permission error whereas this ..

```
    ulimits:
      nofile:
        soft: 524288
        hard: 524288
```

.. starts fine.

Apparently the defaults for these limits are coming from [systemd](26b2085d54/man/systemd.exec.xml (L1122)) which is used on nearly every linux distribution. So my assumption is that almost any linux user who uses rootless docker will run into a permission error when starting the development setup.

Checklist:
----------

- [x] I have performed a self-review of my own code
- [x] I have made corresponding changes to the documentation if applicable
- [x] I have no unrelated changes in the PR.
- [ ] I have confirmed that any new dependencies are strictly necessary.
- [ ] I have written tests for new code (if applicable)
- [ ] I have followed naming conventions/patterns in the surrounding code
- [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc.
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`)
2025-12-05 09:26:20 -05:00
Daniel Dietzler
75a7c9c06c feat: sql tools array as default value (#24389) 2025-12-04 12:54:20 -05:00
Daniel Dietzler
ae8f5a6673 fix: prettier (#24386) 2025-12-04 16:10:42 +00:00
Jason Rasmussen
31f2c7b505 feat: header context menu (#24374) 2025-12-04 11:09:38 -05:00
Yaros
ba6687dde9 feat(web): search type selection dropdown (#24091)
* feat(web): search type selection dropdown

* chore: implement suggestions

* lint

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-12-04 04:10:12 +00:00
210 changed files with 3713 additions and 8053 deletions

View File

@@ -4,6 +4,6 @@
"format:fix": "prettier --write ." "format:fix": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"prettier": "^3.5.3" "prettier": "^3.7.4"
} }
} }

View File

@@ -108,7 +108,7 @@ jobs:
working-directory: ./mobile working-directory: ./mobile
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
- uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 - uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: '17' java-version: '17'
@@ -222,6 +222,7 @@ jobs:
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: '3.3' ruby-version: '3.3'
bundler-cache: true
working-directory: ./mobile/ios working-directory: ./mobile/ios
- name: Install CocoaPods dependencies - name: Install CocoaPods dependencies
@@ -229,13 +230,6 @@ jobs:
run: | run: |
pod install pod install
- name: Install Fastlane
working-directory: ./mobile/ios
run: |
gem install bundler
bundle config set --local path 'vendor/bundle'
bundle install
- name: Create API Key - name: Create API Key
env: env:
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}

View File

@@ -44,7 +44,7 @@ jobs:
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'

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@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
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@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 uses: github/codeql-action/autobuild@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
# 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@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5 uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'

View File

@@ -69,7 +69,7 @@ jobs:
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './docs/.nvmrc' node-version-file: './docs/.nvmrc'
cache: 'pnpm' cache: 'pnpm'

View File

@@ -16,7 +16,7 @@ jobs:
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@@ -32,7 +32,7 @@ jobs:
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'

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@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@@ -49,7 +49,7 @@ jobs:
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@@ -68,7 +68,7 @@ jobs:
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -126,7 +126,7 @@ jobs:
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@@ -144,7 +144,7 @@ jobs:
github-token: ${{ steps.generate-token.outputs.token }} github-token: ${{ steps.generate-token.outputs.token }}
- name: Create draft release - name: Create draft release
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with: with:
draft: true draft: true
tag_name: ${{ env.IMMICH_VERSION }} tag_name: ${{ env.IMMICH_VERSION }}

View File

@@ -17,7 +17,7 @@ jobs:
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@@ -36,7 +36,7 @@ jobs:
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -159,7 +159,7 @@ jobs:
- name: Create PR - name: Create PR
id: create-pr id: create-pr
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9 uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
commit-message: 'chore: release ${{ steps.bump-type.outputs.next }}' commit-message: 'chore: release ${{ steps.bump-type.outputs.next }}'

View File

@@ -52,7 +52,7 @@ jobs:
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0 uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@@ -80,7 +80,7 @@ jobs:
github-token: ${{ steps.generate-token.outputs.token }} github-token: ${{ steps.generate-token.outputs.token }}
- name: Create draft release - name: Create draft release
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with: with:
tag_name: ${{ steps.version.outputs.result }} tag_name: ${{ steps.version.outputs.result }}
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}

View File

@@ -31,7 +31,7 @@ jobs:
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './open-api/typescript-sdk/.nvmrc' node-version-file: './open-api/typescript-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'

View File

@@ -77,7 +77,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -121,7 +121,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -168,7 +168,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './cli/.nvmrc' node-version-file: './cli/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -210,7 +210,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './web/.nvmrc' node-version-file: './web/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -254,7 +254,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './web/.nvmrc' node-version-file: './web/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -292,7 +292,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './web/.nvmrc' node-version-file: './web/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -340,7 +340,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -387,7 +387,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -426,7 +426,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -481,7 +481,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './e2e/.nvmrc' node-version-file: './e2e/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -617,7 +617,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './.github/.nvmrc' node-version-file: './.github/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -668,7 +668,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
@@ -730,7 +730,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with: with:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'

View File

@@ -31,7 +31,7 @@
"eslint-plugin-unicorn": "^62.0.0", "eslint-plugin-unicorn": "^62.0.0",
"globals": "^16.0.0", "globals": "^16.0.0",
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"prettier": "^3.2.5", "prettier": "^3.7.4",
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"typescript-eslint": "^8.28.0", "typescript-eslint": "^8.28.0",

View File

@@ -58,10 +58,6 @@ services:
IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues
IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://docs.immich.app IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://docs.immich.app
IMMICH_THIRD_PARTY_SUPPORT_URL: https://docs.immich.app/community-guides IMMICH_THIRD_PARTY_SUPPORT_URL: https://docs.immich.app/community-guides
ulimits:
nofile:
soft: 1048576
hard: 1048576
ports: ports:
- 9230:9230 - 9230:9230
- 9231:9231 - 9231:9231
@@ -100,10 +96,6 @@ services:
- app-node_modules:/usr/src/app/node_modules - app-node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit - sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage - coverage:/usr/src/app/web/coverage
ulimits:
nofile:
soft: 1048576
hard: 1048576
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
immich-server: immich-server:
@@ -135,7 +127,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:4503e204c900a00ad393bec83c8c7c4c76b0529cd629e23b34b52011aefd1d27 image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1

View File

@@ -56,7 +56,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:4503e204c900a00ad393bec83c8c7c4c76b0529cd629e23b34b52011aefd1d27 image: docker.io/valkey/valkey:9@sha256:fb8d272e529ea567b9bf1302245796f21a2672b8368ca3fcb938ac334e613c8f
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
@@ -83,7 +83,7 @@ services:
container_name: immich_prometheus container_name: immich_prometheus
ports: ports:
- 9090:9090 - 9090:9090
image: prom/prometheus@sha256:49214755b6153f90a597adcbff0252cc61069f8ab69ce8411285cd4a560e8038 image: prom/prometheus@sha256:d936808bdea528155c0154a922cd42fd75716b8bb7ba302641350f9f3eaeba09
volumes: volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml - ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus - prometheus-data:/prometheus

View File

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

View File

@@ -24,6 +24,9 @@ server {
# disable buffering uploads to prevent OOM on reverse proxy server and make uploads twice as fast (no pause) # disable buffering uploads to prevent OOM on reverse proxy server and make uploads twice as fast (no pause)
proxy_request_buffering off; proxy_request_buffering off;
# increase body buffer to avoid limiting upload speed
client_body_buffer_size 1024k;
# Set headers # Set headers
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
@@ -32,8 +35,6 @@ server {
# enable websockets: http://nginx.org/en/docs/http/websocket.html # enable websockets: http://nginx.org/en/docs/http/websocket.html
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off; proxy_redirect off;
# set timeout # set timeout
@@ -43,6 +44,8 @@ server {
location / { location / {
proxy_pass http://<backend_url>:2283; proxy_pass http://<backend_url>:2283;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
} }
# useful when using Let's Encrypt http-01 challenge # useful when using Let's Encrypt http-01 challenge

View File

@@ -52,7 +52,7 @@ Password login has been enabled.
Disable Maintenance Mode Disable Maintenance Mode
``` ```
immich-admin disable-maintenace-mode immich-admin disable-maintenance-mode
Maintenance mode has been disabled. Maintenance mode has been disabled.
``` ```

View File

@@ -38,7 +38,7 @@
"@docusaurus/module-type-aliases": "~3.9.0", "@docusaurus/module-type-aliases": "~3.9.0",
"@docusaurus/tsconfig": "^3.7.0", "@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.7.0", "@docusaurus/types": "^3.7.0",
"prettier": "^3.2.4", "prettier": "^3.7.4",
"typescript": "^5.1.6" "typescript": "^5.1.6"
}, },
"browserslist": { "browserslist": {

View File

@@ -36,14 +36,14 @@
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^62.0.0", "eslint-plugin-unicorn": "^62.0.0",
"exiftool-vendored": "^33.0.0", "exiftool-vendored": "^34.0.0",
"globals": "^16.0.0", "globals": "^16.0.0",
"jose": "^5.6.3", "jose": "^5.6.3",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"oidc-provider": "^9.0.0", "oidc-provider": "^9.0.0",
"pg": "^8.11.3", "pg": "^8.11.3",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"prettier": "^3.2.5", "prettier": "^3.7.4",
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"socket.io-client": "^4.7.4", "socket.io-client": "^4.7.4",

View File

@@ -346,8 +346,6 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
duplicateId: null, duplicateId: null,
resized: true, resized: true,
checksum: asset.checksum, checksum: asset.checksum,
width: exifInfo.exifImageWidth ?? 1,
height: exifInfo.exifImageHeight ?? 1,
}; };
} }

View File

@@ -78,7 +78,6 @@
"exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.", "exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.",
"export_config_as_json_description": "Download the current system config as a JSON file", "export_config_as_json_description": "Download the current system config as a JSON file",
"external_libraries_page_description": "Admin external library page", "external_libraries_page_description": "Admin external library page",
"external_library_management": "External Library Management",
"face_detection": "Face detection", "face_detection": "Face detection",
"face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.", "face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.",
"facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"Reset\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.", "facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"Reset\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.",
@@ -653,6 +652,7 @@
"backup_options_page_title": "Backup options", "backup_options_page_title": "Backup options",
"backup_setting_subtitle": "Manage background and foreground upload settings", "backup_setting_subtitle": "Manage background and foreground upload settings",
"backup_settings_subtitle": "Manage upload settings", "backup_settings_subtitle": "Manage upload settings",
"backup_upload_details_page_more_details": "Tap for more details",
"backward": "Backward", "backward": "Backward",
"biometric_auth_enabled": "Biometric authentication enabled", "biometric_auth_enabled": "Biometric authentication enabled",
"biometric_locked_out": "You are locked out of biometric authentication", "biometric_locked_out": "You are locked out of biometric authentication",
@@ -719,6 +719,7 @@
"check_corrupt_asset_backup_button": "Perform check", "check_corrupt_asset_backup_button": "Perform check",
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
"check_logs": "Check Logs", "check_logs": "Check Logs",
"checksum": "Checksum",
"choose_matching_people_to_merge": "Choose matching people to merge", "choose_matching_people_to_merge": "Choose matching people to merge",
"city": "City", "city": "City",
"clear": "Clear", "clear": "Clear",
@@ -804,9 +805,6 @@
"created_at": "Created", "created_at": "Created",
"creating_linked_albums": "Creating linked albums...", "creating_linked_albums": "Creating linked albums...",
"crop": "Crop", "crop": "Crop",
"crop_aspect_ratio_fixed": "Fixed",
"crop_aspect_ratio_free": "Free",
"crop_aspect_ratio_original": "Original",
"curated_object_page_title": "Things", "curated_object_page_title": "Things",
"current_device": "Current device", "current_device": "Current device",
"current_pin_code": "Current PIN code", "current_pin_code": "Current PIN code",
@@ -935,9 +933,7 @@
"editor_close_without_save_prompt": "The changes will not be saved", "editor_close_without_save_prompt": "The changes will not be saved",
"editor_close_without_save_title": "Close editor?", "editor_close_without_save_title": "Close editor?",
"editor_crop_tool_h2_aspect_ratios": "Aspect ratios", "editor_crop_tool_h2_aspect_ratios": "Aspect ratios",
"editor_crop_tool_h2_mirror": "Mirror",
"editor_crop_tool_h2_rotation": "Rotation", "editor_crop_tool_h2_rotation": "Rotation",
"editor_reset_all_changes": "Reset all changes",
"email": "Email", "email": "Email",
"email_notifications": "Email notifications", "email_notifications": "Email notifications",
"empty_folder": "This folder is empty", "empty_folder": "This folder is empty",
@@ -1172,6 +1168,7 @@
"header_settings_header_name_input": "Header name", "header_settings_header_name_input": "Header name",
"header_settings_header_value_input": "Header value", "header_settings_header_value_input": "Header value",
"headers_settings_tile_title": "Custom proxy headers", "headers_settings_tile_title": "Custom proxy headers",
"height": "Height",
"hi_user": "Hi {name} ({email})", "hi_user": "Hi {name} ({email})",
"hide_all_people": "Hide all people", "hide_all_people": "Hide all people",
"hide_gallery": "Hide gallery", "hide_gallery": "Hide gallery",
@@ -1294,6 +1291,7 @@
"local": "Local", "local": "Local",
"local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server", "local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server",
"local_assets": "Local Assets", "local_assets": "Local Assets",
"local_id": "Local ID",
"local_media_summary": "Local Media Summary", "local_media_summary": "Local Media Summary",
"local_network": "Local network", "local_network": "Local network",
"local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
@@ -1410,8 +1408,6 @@
"minimize": "Minimize", "minimize": "Minimize",
"minute": "Minute", "minute": "Minute",
"minutes": "Minutes", "minutes": "Minutes",
"mirror_horizontal": "Horizontal",
"mirror_vertical": "Vertical",
"missing": "Missing", "missing": "Missing",
"mobile_app": "Mobile App", "mobile_app": "Mobile App",
"mobile_app_download_onboarding_note": "Download the companion mobile app using the following options", "mobile_app_download_onboarding_note": "Download the companion mobile app using the following options",
@@ -1765,8 +1761,6 @@
"role": "Role", "role": "Role",
"role_editor": "Editor", "role_editor": "Editor",
"role_viewer": "Viewer", "role_viewer": "Viewer",
"rotate_ccw": "CCW 90°",
"rotate_cw": "CW 90°",
"running": "Running", "running": "Running",
"save": "Save", "save": "Save",
"save_to_gallery": "Save to gallery", "save_to_gallery": "Save to gallery",
@@ -2228,6 +2222,7 @@
"week": "Week", "week": "Week",
"welcome": "Welcome", "welcome": "Welcome",
"welcome_to_immich": "Welcome to Immich", "welcome_to_immich": "Welcome to Immich",
"width": "Width",
"wifi_name": "Wi-Fi Name", "wifi_name": "Wi-Fi Name",
"workflow": "Workflow", "workflow": "Workflow",
"wrong_pin_code": "Wrong PIN code", "wrong_pin_code": "Wrong PIN code",

View File

@@ -137,8 +137,7 @@
<key>NSFaceIDUsageDescription</key> <key>NSFaceIDUsageDescription</key>
<string>We need to use FaceID to allow access to your locked folder</string> <string>We need to use FaceID to allow access to your locked folder</string>
<key>NSLocalNetworkUsageDescription</key> <key>NSLocalNetworkUsageDescription</key>
<string>We need local network permission to connect to the local server using IP address and <string>We need local network permission to connect to the local server using IP address and allow the casting feature to work</string>
allow the casting feature to work</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We require this permission to access the local WiFi name for background upload mechanism</string> <string>We require this permission to access the local WiFi name for background upload mechanism</string>
<key>NSLocationUsageDescription</key> <key>NSLocationUsageDescription</key>

View File

@@ -1,8 +1,10 @@
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/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
class AssetService { class AssetService {
final RemoteAssetRepository _remoteAssetRepository; final RemoteAssetRepository _remoteAssetRepository;
@@ -56,11 +58,22 @@ class AssetService {
} }
Future<double> getAspectRatio(BaseAsset asset) async { Future<double> getAspectRatio(BaseAsset asset) async {
bool isFlipped;
double? width; double? width;
double? height; double? height;
width = asset.width?.toDouble(); if (asset.hasRemote) {
height = asset.height?.toDouble(); final exif = await getExif(asset);
isFlipped = ExifDtoConverter.isOrientationFlipped(exif?.orientation);
width = asset.width?.toDouble();
height = asset.height?.toDouble();
} else if (asset is LocalAsset) {
isFlipped = CurrentPlatform.isAndroid && (asset.orientation == 90 || asset.orientation == 270);
width = asset.width?.toDouble();
height = asset.height?.toDouble();
} else {
isFlipped = false;
}
if (width == null || height == null) { if (width == null || height == null) {
if (asset.hasRemote) { if (asset.hasRemote) {
@@ -76,8 +89,10 @@ class AssetService {
} }
} }
if (width != null && height != null && height > 0) { final orientedWidth = isFlipped ? height : width;
return width / height; final orientedHeight = isFlipped ? width : height;
if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) {
return orientedWidth / orientedHeight;
} }
return 1.0; return 1.0;

View File

@@ -22,7 +22,6 @@ import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey; import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey;
import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole, UserMetadataKey; import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole, UserMetadataKey;
@@ -195,8 +194,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
livePhotoVideoId: Value(asset.livePhotoVideoId), livePhotoVideoId: Value(asset.livePhotoVideoId),
stackId: Value(asset.stackId), stackId: Value(asset.stackId),
libraryId: Value(asset.libraryId), libraryId: Value(asset.libraryId),
width: Value(asset.width),
height: Value(asset.height),
); );
batch.insert( batch.insert(
@@ -248,21 +245,10 @@ class SyncStreamRepository extends DriftDatabaseRepository {
await _db.batch((batch) { await _db.batch((batch) {
for (final exif in data) { for (final exif in data) {
int? width;
int? height;
if (ExifDtoConverter.isOrientationFlipped(exif.orientation)) {
width = exif.exifImageHeight;
height = exif.exifImageWidth;
} else {
width = exif.exifImageWidth;
height = exif.exifImageHeight;
}
batch.update( batch.update(
_db.remoteAssetEntity, _db.remoteAssetEntity,
RemoteAssetEntityCompanion(width: Value(width), height: Value(height)), RemoteAssetEntityCompanion(width: Value(exif.exifImageWidth), height: Value(exif.exifImageHeight)),
where: (row) => row.id.equals(exif.assetId) & row.width.isNull() & row.height.isNull(), where: (row) => row.id.equals(exif.assetId),
); );
} }
}); });

View File

@@ -98,7 +98,7 @@ class DriftUploadDetailPage extends ConsumerWidget {
), ),
), ),
Text( Text(
'Tap for more details', "backup_upload_details_page_more_details".t(context: context),
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
color: context.colorScheme.onSurface.withValues(alpha: 0.6), color: context.colorScheme.onSurface.withValues(alpha: 0.6),
), ),
@@ -239,14 +239,20 @@ class FileDetailDialog extends ConsumerWidget {
const SizedBox(height: 24), const SizedBox(height: 24),
if (asset != null) ...[ if (asset != null) ...[
_buildInfoSection(context, [ _buildInfoSection(context, [
_buildInfoRow(context, "Filename", path.basename(uploadStatus.filename)), _buildInfoRow(context, "filename".t(context: context), path.basename(uploadStatus.filename)),
_buildInfoRow(context, "Local ID", asset.id), _buildInfoRow(context, "local_id".t(context: context), asset.id),
_buildInfoRow(context, "File Size", formatHumanReadableBytes(uploadStatus.fileSize, 2)), _buildInfoRow(
if (asset.width != null) _buildInfoRow(context, "Width", "${asset.width}px"), context,
if (asset.height != null) _buildInfoRow(context, "Height", "${asset.height}px"), "file_size".t(context: context),
_buildInfoRow(context, "Created At", asset.createdAt.toString()), formatHumanReadableBytes(uploadStatus.fileSize, 2),
_buildInfoRow(context, "Updated At", asset.updatedAt.toString()), ),
if (asset.checksum != null) _buildInfoRow(context, "Checksum", asset.checksum!), if (asset.width != null) _buildInfoRow(context, "width".t(context: context), "${asset.width}px"),
if (asset.height != null)
_buildInfoRow(context, "height".t(context: context), "${asset.height}px"),
_buildInfoRow(context, "created_at".t(context: context), asset.createdAt.toString()),
_buildInfoRow(context, "updated_at".t(context: context), asset.updatedAt.toString()),
if (asset.checksum != null)
_buildInfoRow(context, "checksum".t(context: context), asset.checksum!),
]), ]),
], ],
], ],

View File

@@ -1,173 +0,0 @@
import 'dart:async';
import 'package:auto_route/auto_route.dart';
import 'package:drift/drift.dart' hide Column;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:logging/logging.dart';
final _features = [
_Feature(
name: 'Main Timeline',
icon: Icons.timeline_rounded,
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
),
_Feature(
name: 'Selection Mode Timeline',
icon: Icons.developer_mode_rounded,
onTap: (ctx, ref) async {
final user = ref.watch(currentUserProvider);
if (user == null) {
return Future.value();
}
final assets = await ref.read(remoteAssetRepositoryProvider).getSome(user.id);
final selectedAssets = await ctx.pushRoute<Set<BaseAsset>>(
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: assets.toSet()),
);
Logger("FeaturesInDevelopment").fine("Selected ${selectedAssets?.length ?? 0} assets");
return Future.value();
},
),
_Feature(name: '', icon: Icons.vertical_align_center_sharp, onTap: (_, __) => Future.value()),
_Feature(
name: 'Sync Local',
icon: Icons.photo_album_rounded,
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(),
),
_Feature(
name: 'Sync Local Full (1)',
icon: Icons.photo_library_rounded,
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(full: true),
),
_Feature(
name: 'Hash Local Assets (2)',
icon: Icons.numbers_outlined,
onTap: (_, ref) => ref.read(backgroundSyncProvider).hashAssets(),
),
_Feature(
name: 'Sync Remote (3)',
icon: Icons.refresh_rounded,
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncRemote(),
),
_Feature(
name: 'WAL Checkpoint',
icon: Icons.save_rounded,
onTap: (_, ref) => ref.read(driftProvider).customStatement("pragma wal_checkpoint(truncate)"),
),
_Feature(name: '', icon: Icons.vertical_align_center_sharp, onTap: (_, __) => Future.value()),
_Feature(
name: 'Clear Delta Checkpoint',
icon: Icons.delete_rounded,
onTap: (_, ref) => ref.read(nativeSyncApiProvider).clearSyncCheckpoint(),
),
_Feature(
name: 'Clear Local Data',
style: const TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
icon: Icons.delete_forever_rounded,
onTap: (_, ref) async {
final db = ref.read(driftProvider);
await db.localAssetEntity.deleteAll();
await db.localAlbumEntity.deleteAll();
await db.localAlbumAssetEntity.deleteAll();
},
),
_Feature(
name: 'Clear Remote Data',
style: const TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
icon: Icons.delete_sweep_rounded,
onTap: (_, ref) async {
final db = ref.read(driftProvider);
await db.remoteAssetEntity.deleteAll();
await db.remoteExifEntity.deleteAll();
await db.remoteAlbumEntity.deleteAll();
await db.remoteAlbumUserEntity.deleteAll();
await db.remoteAlbumAssetEntity.deleteAll();
await db.memoryEntity.deleteAll();
await db.memoryAssetEntity.deleteAll();
await db.stackEntity.deleteAll();
await db.personEntity.deleteAll();
await db.assetFaceEntity.deleteAll();
},
),
_Feature(
name: 'Local Media Summary',
style: const TextStyle(color: Colors.indigo, fontWeight: FontWeight.bold),
icon: Icons.table_chart_rounded,
onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()),
),
_Feature(
name: 'Remote Media Summary',
style: const TextStyle(color: Colors.indigo, fontWeight: FontWeight.bold),
icon: Icons.summarize_rounded,
onTap: (ctx, _) => ctx.pushRoute(const RemoteMediaSummaryRoute()),
),
_Feature(
name: 'Reset Sqlite',
icon: Icons.table_view_rounded,
style: const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
onTap: (_, ref) async {
final drift = ref.read(driftProvider);
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
final migrator = drift.createMigrator();
for (final entity in drift.allSchemaEntities) {
await migrator.drop(entity);
await migrator.create(entity);
}
},
),
];
@RoutePage()
class FeatInDevPage extends StatelessWidget {
const FeatInDevPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('features_in_development'.tr()), centerTitle: true),
body: Column(
children: [
Flexible(
flex: 1,
child: ListView.builder(
itemBuilder: (_, index) {
final feat = _features[index];
return Consumer(
builder: (ctx, ref, _) => ListTile(
title: Text(feat.name, style: feat.style),
trailing: Icon(feat.icon),
visualDensity: VisualDensity.compact,
onTap: () => unawaited(feat.onTap(ctx, ref)),
),
);
},
itemCount: _features.length,
),
),
const Divider(height: 0),
],
),
);
}
}
class _Feature {
const _Feature({required this.name, required this.icon, required this.onTap, this.style});
final String name;
final IconData icon;
final TextStyle? style;
final Future<void> Function(BuildContext, WidgetRef _) onTap;
}

View File

@@ -0,0 +1,51 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_ui/immich_ui.dart';
List<Widget> _showcaseBuilder(Function(ImmichVariant variant, ImmichColor color) builder) {
final children = <Widget>[];
final items = [
(variant: ImmichVariant.filled, title: "Filled Variant"),
(variant: ImmichVariant.ghost, title: "Ghost Variant"),
];
for (final (:variant, :title) in items) {
children.add(Text(title));
children.add(Row(spacing: 10, children: [for (var color in ImmichColor.values) builder(variant, color)]));
}
return children;
}
@RoutePage()
class ImmichUIShowcasePage extends StatelessWidget {
const ImmichUIShowcasePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Immich UI Showcase')),
body: Padding(
padding: const EdgeInsets.all(20),
child: SingleChildScrollView(
child: Column(
spacing: 10,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("IconButton", style: context.textTheme.titleLarge),
..._showcaseBuilder(
(variant, color) =>
ImmichIconButton(icon: Icons.favorite, color: color, variant: variant, onTap: () {}),
),
Text("CloseButton", style: context.textTheme.titleLarge),
..._showcaseBuilder((variant, color) => ImmichCloseButton(color: color, variant: variant, onTap: () {})),
],
),
),
),
);
}
}

View File

@@ -37,7 +37,7 @@ class DriftActivitiesPage extends HookConsumerWidget {
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(album.name), title: Text(album.name),
actions: [const LikeActivityActionButton(menuItem: true)], actions: [const LikeActivityActionButton(iconOnly: true)],
actionsPadding: const EdgeInsets.only(right: 8), actionsPadding: const EdgeInsets.only(right: 8),
), ),
body: activities.widgetWhen( body: activities.widgetWhen(

View File

@@ -27,8 +27,19 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
bool isAlbumTitleTextFieldFocus = false; bool isAlbumTitleTextFieldFocus = false;
Set<BaseAsset> selectedAssets = {}; Set<BaseAsset> selectedAssets = {};
@override
void initState() {
super.initState();
albumTitleController.addListener(_onTitleChanged);
}
void _onTitleChanged() {
setState(() {});
}
@override @override
void dispose() { void dispose() {
albumTitleController.removeListener(_onTitleChanged);
albumTitleController.dispose(); albumTitleController.dispose();
albumDescriptionController.dispose(); albumDescriptionController.dispose();
albumTitleTextFieldFocusNode.dispose(); albumTitleTextFieldFocusNode.dispose();

View File

@@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart';
import 'package:immich_ui/immich_ui.dart';
/// A widget for cropping an image. /// A widget for cropping an image.
/// This widget uses [HookWidget] to manage its lifecycle and state. It allows /// This widget uses [HookWidget] to manage its lifecycle and state. It allows
@@ -30,11 +31,13 @@ class DriftCropImagePage extends HookWidget {
appBar: AppBar( appBar: AppBar(
backgroundColor: context.scaffoldBackgroundColor, backgroundColor: context.scaffoldBackgroundColor,
title: Text("crop".tr()), title: Text("crop".tr()),
leading: CloseButton(color: context.primaryColor), leading: const ImmichCloseButton(),
actions: [ actions: [
IconButton( ImmichIconButton(
icon: Icon(Icons.done_rounded, color: context.primaryColor, size: 24), icon: Icons.done_rounded,
onPressed: () async { color: ImmichColor.primary,
variant: ImmichVariant.ghost,
onTap: () async {
final croppedImage = await cropController.croppedImage(); final croppedImage = await cropController.croppedImage();
unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true))); unawaited(context.pushRoute(DriftEditImageRoute(asset: asset, image: croppedImage, isEdited: true)));
}, },
@@ -72,17 +75,17 @@ class DriftCropImagePage extends HookWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
IconButton( ImmichIconButton(
icon: Icon(Icons.rotate_left, color: context.themeData.iconTheme.color), icon: Icons.rotate_left,
onPressed: () { variant: ImmichVariant.ghost,
cropController.rotateLeft(); color: ImmichColor.secondary,
}, onTap: () => cropController.rotateLeft(),
), ),
IconButton( ImmichIconButton(
icon: Icon(Icons.rotate_right, color: context.themeData.iconTheme.color), icon: Icons.rotate_right,
onPressed: () { variant: ImmichVariant.ghost,
cropController.rotateRight(); color: ImmichColor.secondary,
}, onTap: () => cropController.rotateRight(),
), ),
], ],
), ),

View File

@@ -21,12 +21,36 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee
enum AddToMenuItem { album, archive, unarchive, lockedFolder } enum AddToMenuItem { album, archive, unarchive, lockedFolder }
class AddActionButton extends ConsumerWidget { class AddActionButton extends ConsumerStatefulWidget {
const AddActionButton({super.key}); const AddActionButton({super.key, this.originalTheme});
Future<void> _showAddOptions(BuildContext context, WidgetRef ref) async { final ThemeData? originalTheme;
@override
ConsumerState<AddActionButton> createState() => _AddActionButtonState();
}
class _AddActionButtonState extends ConsumerState<AddActionButton> {
void _handleMenuSelection(AddToMenuItem selected) {
switch (selected) {
case AddToMenuItem.album:
_openAlbumSelector();
break;
case AddToMenuItem.archive:
performArchiveAction(context, ref, source: ActionSource.viewer);
break;
case AddToMenuItem.unarchive:
performUnArchiveAction(context, ref, source: ActionSource.viewer);
break;
case AddToMenuItem.lockedFolder:
performMoveToLockFolderAction(context, ref, source: ActionSource.viewer);
break;
}
}
List<Widget> _buildMenuChildren() {
final asset = ref.read(currentAssetNotifier); final asset = ref.read(currentAssetNotifier);
if (asset == null) return; if (asset == null) return [];
final user = ref.read(currentUserProvider); final user = ref.read(currentUserProvider);
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
@@ -35,84 +59,50 @@ class AddActionButton extends ConsumerWidget {
final hasRemote = asset is RemoteAsset; final hasRemote = asset is RemoteAsset;
final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived; final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived;
final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived; final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived;
final menuItemHeight = 30.0;
final List<PopupMenuEntry<AddToMenuItem>> items = [ return [
PopupMenuItem( Padding(
enabled: false, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
textStyle: context.textTheme.labelMedium, child: Text("add_to_bottom_bar".tr(), style: context.textTheme.labelMedium),
height: 40,
child: Text("add_to_bottom_bar".tr()),
), ),
PopupMenuItem( BaseActionButton(
height: menuItemHeight, iconData: Icons.photo_album_outlined,
value: AddToMenuItem.album, label: "album".tr(),
child: ListTile(leading: const Icon(Icons.photo_album_outlined), title: Text("album".tr())), menuItem: true,
onPressed: () => _handleMenuSelection(AddToMenuItem.album),
), ),
const PopupMenuDivider(),
PopupMenuItem(enabled: false, textStyle: context.textTheme.labelMedium, height: 40, child: Text("move_to".tr())),
if (isOwner) ...[ if (isOwner) ...[
const Divider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text("move_to".tr(), style: context.textTheme.labelMedium),
),
if (showArchive) if (showArchive)
PopupMenuItem( BaseActionButton(
height: menuItemHeight, iconData: Icons.archive_outlined,
value: AddToMenuItem.archive, label: "archive".tr(),
child: ListTile(leading: const Icon(Icons.archive_outlined), title: Text("archive".tr())), menuItem: true,
onPressed: () => _handleMenuSelection(AddToMenuItem.archive),
), ),
if (showUnarchive) if (showUnarchive)
PopupMenuItem( BaseActionButton(
height: menuItemHeight, iconData: Icons.unarchive_outlined,
value: AddToMenuItem.unarchive, label: "unarchive".tr(),
child: ListTile(leading: const Icon(Icons.unarchive_outlined), title: Text("unarchive".tr())), menuItem: true,
onPressed: () => _handleMenuSelection(AddToMenuItem.unarchive),
), ),
PopupMenuItem( BaseActionButton(
height: menuItemHeight, iconData: Icons.lock_outline,
value: AddToMenuItem.lockedFolder, label: "locked_folder".tr(),
child: ListTile(leading: const Icon(Icons.lock_outline), title: Text("locked_folder".tr())), menuItem: true,
onPressed: () => _handleMenuSelection(AddToMenuItem.lockedFolder),
), ),
], ],
]; ];
final AddToMenuItem? selected = await showMenu<AddToMenuItem>(
context: context,
color: context.themeData.scaffoldBackgroundColor,
position: _menuPosition(context),
items: items,
popUpAnimationStyle: AnimationStyle.noAnimation,
);
if (selected == null) {
return;
}
switch (selected) {
case AddToMenuItem.album:
_openAlbumSelector(context, ref);
break;
case AddToMenuItem.archive:
await performArchiveAction(context, ref, source: ActionSource.viewer);
break;
case AddToMenuItem.unarchive:
await performUnArchiveAction(context, ref, source: ActionSource.viewer);
break;
case AddToMenuItem.lockedFolder:
await performMoveToLockFolderAction(context, ref, source: ActionSource.viewer);
break;
}
} }
RelativeRect _menuPosition(BuildContext context) { void _openAlbumSelector() {
final renderObject = context.findRenderObject();
if (renderObject is! RenderBox) {
return RelativeRect.fill;
}
final size = renderObject.size;
final position = renderObject.localToGlobal(Offset.zero);
return RelativeRect.fromLTRB(position.dx, position.dy - size.height - 200, position.dx + size.width, position.dy);
}
void _openAlbumSelector(BuildContext context, WidgetRef ref) {
final currentAsset = ref.read(currentAssetNotifier); final currentAsset = ref.read(currentAssetNotifier);
if (currentAsset == null) { if (currentAsset == null) {
ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error); ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error);
@@ -120,7 +110,8 @@ class AddActionButton extends ConsumerWidget {
} }
final List<Widget> slivers = [ final List<Widget> slivers = [
AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(context, ref, album)), const CreateAlbumButton(),
AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(album)),
]; ];
showModalBottomSheet( showModalBottomSheet(
@@ -141,7 +132,7 @@ class AddActionButton extends ConsumerWidget {
); );
} }
Future<void> _addCurrentAssetToAlbum(BuildContext context, WidgetRef ref, RemoteAlbum album) async { Future<void> _addCurrentAssetToAlbum(RemoteAlbum album) async {
final latest = ref.read(currentAssetNotifier); final latest = ref.read(currentAssetNotifier);
if (latest == null) { if (latest == null) {
@@ -165,6 +156,9 @@ class AddActionButton extends ConsumerWidget {
context: context, context: context,
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}), msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
); );
// Invalidate using the asset's remote ID to refresh the "Appears in" list
ref.invalidate(albumsContainingAssetProvider(latest.remoteId!));
} }
if (!context.mounted) { if (!context.mounted) {
@@ -174,17 +168,38 @@ class AddActionButton extends ConsumerWidget {
} }
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context) {
final asset = ref.watch(currentAssetNotifier); final asset = ref.watch(currentAssetNotifier);
if (asset == null) { if (asset == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Builder(
builder: (buttonContext) { final themeData = widget.originalTheme ?? context.themeData;
return MenuAnchor(
consumeOutsideTap: true,
style: MenuStyle(
backgroundColor: WidgetStatePropertyAll(themeData.scaffoldBackgroundColor),
surfaceTintColor: const WidgetStatePropertyAll(Colors.grey),
elevation: const WidgetStatePropertyAll(4),
shape: const WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
),
menuChildren: widget.originalTheme != null
? [
Theme(
data: widget.originalTheme!,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: _buildMenuChildren()),
),
]
: _buildMenuChildren(),
builder: (context, controller, child) {
return BaseActionButton( return BaseActionButton(
iconData: Icons.add, iconData: Icons.add,
label: "add_to_bottom_bar".tr(), label: "add_to_bottom_bar".tr(),
onPressed: () => _showAddOptions(buttonContext, ref), onPressed: () => controller.isOpen ? controller.close() : controller.open(),
); );
}, },
); );

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
class BaseActionButton extends StatelessWidget { class BaseActionButton extends ConsumerWidget {
const BaseActionButton({ const BaseActionButton({
super.key, super.key,
required this.label, required this.label,
@@ -11,6 +12,7 @@ class BaseActionButton extends StatelessWidget {
this.onLongPressed, this.onLongPressed,
this.maxWidth = 90.0, this.maxWidth = 90.0,
this.minWidth, this.minWidth,
this.iconOnly = false,
this.menuItem = false, this.menuItem = false,
}); });
@@ -19,25 +21,42 @@ class BaseActionButton extends StatelessWidget {
final Color? iconColor; final Color? iconColor;
final double maxWidth; final double maxWidth;
final double? minWidth; final double? minWidth;
/// When true, renders only an IconButton without text label
final bool iconOnly;
/// When true, renders as a MenuItemButton for use in MenuAnchor menus
final bool menuItem; final bool menuItem;
final void Function()? onPressed; final void Function()? onPressed;
final void Function()? onLongPressed; final void Function()? onLongPressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final miniWidth = minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0); final miniWidth = minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0);
final iconTheme = IconTheme.of(context); final iconTheme = IconTheme.of(context);
final iconSize = iconTheme.size ?? 24.0; final iconSize = iconTheme.size ?? 24.0;
final iconColor = this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color; final iconColor = this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color;
final textColor = context.themeData.textTheme.labelLarge?.color; final textColor = context.themeData.textTheme.labelLarge?.color;
if (menuItem) { if (iconOnly) {
return IconButton( return IconButton(
onPressed: onPressed, onPressed: onPressed,
icon: Icon(iconData, size: iconSize, color: iconColor), icon: Icon(iconData, size: iconSize, color: iconColor),
); );
} }
if (menuItem) {
final theme = context.themeData;
final effectiveIconColor = iconColor ?? theme.iconTheme.color ?? theme.colorScheme.onSurfaceVariant;
return MenuItemButton(
style: MenuItemButton.styleFrom(alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16)),
leadingIcon: Icon(iconData, color: effectiveIconColor),
onPressed: onPressed,
child: Text(label, style: theme.textTheme.labelLarge?.copyWith(fontSize: 16)),
);
}
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth), constraints: BoxConstraints(maxWidth: maxWidth),
child: MaterialButton( child: MaterialButton(

View File

@@ -7,8 +7,9 @@ import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
class CastActionButton extends ConsumerWidget { class CastActionButton extends ConsumerWidget {
const CastActionButton({super.key, this.menuItem = true}); const CastActionButton({super.key, this.iconOnly = false, this.menuItem = false});
final bool iconOnly;
final bool menuItem; final bool menuItem;
@override @override
@@ -22,6 +23,7 @@ class CastActionButton extends ConsumerWidget {
onPressed: () { onPressed: () {
showDialog(context: context, builder: (context) => const CastDialog()); showDialog(context: context, builder: (context) => const CastDialog());
}, },
iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
); );
} }

View File

@@ -10,8 +10,9 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
class DownloadActionButton extends ConsumerWidget { class DownloadActionButton extends ConsumerWidget {
final ActionSource source; final ActionSource source;
final bool iconOnly;
final bool menuItem; final bool menuItem;
const DownloadActionButton({super.key, required this.source, this.menuItem = false}); const DownloadActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async { void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async {
if (!context.mounted) { if (!context.mounted) {
@@ -38,6 +39,7 @@ class DownloadActionButton extends ConsumerWidget {
iconData: Icons.download, iconData: Icons.download,
maxWidth: 95, maxWidth: 95,
label: "download".t(context: context), label: "download".t(context: context),
iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
onPressed: () => _onTap(context, ref, backgroundManager), onPressed: () => _onTap(context, ref, backgroundManager),
); );

View File

@@ -10,9 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
class FavoriteActionButton extends ConsumerWidget { class FavoriteActionButton extends ConsumerWidget {
final ActionSource source; final ActionSource source;
final bool iconOnly;
final bool menuItem; final bool menuItem;
const FavoriteActionButton({super.key, required this.source, this.menuItem = false}); const FavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
void _onTap(BuildContext context, WidgetRef ref) async { void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) { if (!context.mounted) {
@@ -44,6 +45,7 @@ class FavoriteActionButton extends ConsumerWidget {
return BaseActionButton( return BaseActionButton(
iconData: Icons.favorite_border_rounded, iconData: Icons.favorite_border_rounded,
label: "favorite".t(context: context), label: "favorite".t(context: context),
iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
onPressed: () => _onTap(context, ref), onPressed: () => _onTap(context, ref),
); );

View File

@@ -12,8 +12,9 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
class LikeActivityActionButton extends ConsumerWidget { class LikeActivityActionButton extends ConsumerWidget {
const LikeActivityActionButton({super.key, this.menuItem = false}); const LikeActivityActionButton({super.key, this.iconOnly = false, this.menuItem = false});
final bool iconOnly;
final bool menuItem; final bool menuItem;
@override @override
@@ -49,6 +50,7 @@ class LikeActivityActionButton extends ConsumerWidget {
iconData: liked != null ? Icons.favorite : Icons.favorite_border, iconData: liked != null ? Icons.favorite : Icons.favorite_border,
label: "like".t(context: context), label: "like".t(context: context),
onPressed: () => onTap(liked), onPressed: () => onTap(liked),
iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
); );
}, },
@@ -57,6 +59,7 @@ class LikeActivityActionButton extends ConsumerWidget {
loading: () => BaseActionButton( loading: () => BaseActionButton(
iconData: Icons.favorite_border, iconData: Icons.favorite_border,
label: "like".t(context: context), label: "like".t(context: context),
iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
), ),
error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])), error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])),

View File

@@ -5,8 +5,9 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
class MotionPhotoActionButton extends ConsumerWidget { class MotionPhotoActionButton extends ConsumerWidget {
const MotionPhotoActionButton({super.key, this.menuItem = true}); const MotionPhotoActionButton({super.key, this.iconOnly = false, this.menuItem = false});
final bool iconOnly;
final bool menuItem; final bool menuItem;
@override @override
@@ -17,6 +18,7 @@ class MotionPhotoActionButton extends ConsumerWidget {
iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded, iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded,
label: "play_motion_photo".t(context: context), label: "play_motion_photo".t(context: context),
onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle, onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle,
iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
); );
} }

View File

@@ -10,9 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
class UnFavoriteActionButton extends ConsumerWidget { class UnFavoriteActionButton extends ConsumerWidget {
final ActionSource source; final ActionSource source;
final bool iconOnly;
final bool menuItem; final bool menuItem;
const UnFavoriteActionButton({super.key, required this.source, this.menuItem = false}); const UnFavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
void _onTap(BuildContext context, WidgetRef ref) async { void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) { if (!context.mounted) {
@@ -45,6 +46,7 @@ class UnFavoriteActionButton extends ConsumerWidget {
iconData: Icons.favorite_rounded, iconData: Icons.favorite_rounded,
label: "unfavorite".t(context: context), label: "unfavorite".t(context: context),
onPressed: () => _onTap(context, ref), onPressed: () => _onTap(context, ref),
iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
); );
} }

View File

@@ -12,8 +12,10 @@ import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
@@ -766,3 +768,68 @@ class AddToAlbumHeader extends ConsumerWidget {
); );
} }
} }
class CreateAlbumButton extends ConsumerWidget {
const CreateAlbumButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
Future<void> onCreateAlbum() async {
var albumName = await showDialog<String?>(context: context, builder: (context) => const NewAlbumNameModal());
if (albumName == null) {
return;
}
final asset = ref.read(currentAssetNotifier);
if (asset == null) {
ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error);
return;
}
final album = await ref
.read(remoteAlbumProvider.notifier)
.createAlbum(title: albumName, assetIds: [asset.remoteId!]);
if (album == null) {
ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr());
return;
}
ImmichToast.show(
context: context,
msg: 'add_to_album_bottom_sheet_added'.tr(namedArgs: {'album': album.name}),
);
// Invalidate using the asset's remote ID to refresh the "Appears in" list
ref.invalidate(albumsContainingAssetProvider(asset.remoteId!));
context.pop();
}
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverToBoxAdapter(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("add_to_album", style: context.textTheme.titleSmall).tr(),
TextButton.icon(
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
minimumSize: const Size(0, 0),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: onCreateAlbum,
icon: Icon(Icons.add, color: context.primaryColor),
label: Text(
"common_create_new_album",
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 14),
).tr(),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,53 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class NewAlbumNameModal extends StatefulWidget {
const NewAlbumNameModal({super.key});
@override
State<NewAlbumNameModal> createState() => _NewAlbumNameModalState();
}
class _NewAlbumNameModalState extends State<NewAlbumNameModal> {
TextEditingController nameController = TextEditingController();
@override
void dispose() {
nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text("album_name", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
content: SingleChildScrollView(
child: TextFormField(
controller: nameController,
textCapitalization: TextCapitalization.words,
autofocus: true,
decoration: InputDecoration(hintText: 'name'.tr(), border: const OutlineInputBorder()),
),
),
actions: [
TextButton(
onPressed: () => context.pop(null),
child: Text(
"cancel",
style: TextStyle(color: Colors.red[300], fontWeight: FontWeight.bold),
).tr(),
),
TextButton(
onPressed: () {
context.pop(nameController.text.trim());
},
child: Text(
"create_album",
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold),
).tr(),
),
],
);
}
}

View File

@@ -38,11 +38,13 @@ class ViewerBottomBar extends ConsumerWidget {
opacity = 0; opacity = 0;
} }
final originalTheme = context.themeData;
final actions = <Widget>[ final actions = <Widget>[
const ShareActionButton(source: ActionSource.viewer), const ShareActionButton(source: ActionSource.viewer),
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
if (asset.type == AssetType.image) const EditImageActionButton(), if (asset.type == AssetType.image) const EditImageActionButton(),
if (asset.hasRemote) const AddActionButton(), if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
if (isOwner) ...[ if (isOwner) ...[
asset.isLocalOnly asset.isLocalOnly

View File

@@ -4,25 +4,19 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/domain/utils/event_stream.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/presentation/widgets/action_buttons/cast_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart';
import 'package:immich_mobile/providers/activity.provider.dart'; import 'package:immich_mobile/providers/activity.provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
const ViewerTopAppBar({super.key}); const ViewerTopAppBar({super.key});
@@ -41,15 +35,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final isInLockedView = ref.watch(inLockedViewProvider); final isInLockedView = ref.watch(inLockedViewProvider);
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
final timelineOrigin = ref.read(timelineServiceProvider).origin;
final showViewInTimelineButton =
timelineOrigin != TimelineOrigin.main &&
timelineOrigin != TimelineOrigin.deepLink &&
timelineOrigin != TimelineOrigin.trash &&
timelineOrigin != TimelineOrigin.archive &&
timelineOrigin != TimelineOrigin.localAlbum &&
isOwner;
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet)); final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity)); int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
@@ -62,11 +47,10 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
opacity = 0; opacity = 0;
} }
final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); final originalTheme = context.themeData;
final actions = <Widget>[ final actions = <Widget>[
if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, menuItem: true), if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
if (album != null && album.isActivityEnabled && album.isShared) if (album != null && album.isActivityEnabled && album.isShared)
IconButton( IconButton(
icon: const Icon(Icons.chat_outlined), icon: const Icon(Icons.chat_outlined),
@@ -74,28 +58,16 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true)); EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true));
}, },
), ),
if (showViewInTimelineButton)
IconButton(
onPressed: () async {
await context.maybePop();
await context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
EventStream.shared.emit(ScrollToDateEvent(asset.createdAt));
},
icon: const Icon(Icons.image_search),
tooltip: 'view_in_timeline'.t(context: context),
),
if (asset.hasRemote && isOwner && !asset.isFavorite) if (asset.hasRemote && isOwner && !asset.isFavorite)
const FavoriteActionButton(source: ActionSource.viewer, menuItem: true), const FavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
if (asset.hasRemote && isOwner && asset.isFavorite) if (asset.hasRemote && isOwner && asset.isFavorite)
const UnFavoriteActionButton(source: ActionSource.viewer, menuItem: true), const UnFavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true),
const _KebabMenu(), ViewerKebabMenu(originalTheme: originalTheme),
]; ];
final lockedViewActions = <Widget>[ final lockedViewActions = <Widget>[ViewerKebabMenu(originalTheme: originalTheme)];
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
const _KebabMenu(),
];
return IgnorePointer( return IgnorePointer(
ignoring: opacity < 255, ignoring: opacity < 255,
@@ -122,20 +94,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
Size get preferredSize => const Size.fromHeight(60.0); Size get preferredSize => const Size.fromHeight(60.0);
} }
class _KebabMenu extends ConsumerWidget {
const _KebabMenu();
@override
Widget build(BuildContext context, WidgetRef ref) {
return IconButton(
onPressed: () {
EventStream.shared.emit(const ViewerOpenBottomSheetEvent());
},
icon: const Icon(Icons.more_vert_rounded),
);
}
}
class _AppBarBackButton extends ConsumerWidget { class _AppBarBackButton extends ConsumerWidget {
const _AppBarBackButton(); const _AppBarBackButton();

View File

@@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/setting.service.dart'; import 'package:immich_mobile/domain/services/setting.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/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer_controls.widget.dart';
@@ -104,7 +105,12 @@ class NativeVideoViewer extends HookConsumerWidget {
throw Exception('No file found for the video'); throw Exception('No file found for the video');
} }
final source = await VideoSource.init(path: file.path, type: VideoSourceType.file); // Pass a file:// URI so Android's Uri.parse doesn't
// interpret characters like '#' as fragment identifiers.
final source = await VideoSource.init(
path: CurrentPlatform.isAndroid ? file.uri.toString() : file.path,
type: VideoSourceType.file,
);
return source; return source;
} }

View File

@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/utils/action_button.utils.dart';
class ViewerKebabMenu extends ConsumerWidget {
const ViewerKebabMenu({super.key, this.originalTheme});
final ThemeData? originalTheme;
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetNotifier);
if (asset == null) {
return const SizedBox.shrink();
}
final user = ref.watch(currentUserProvider);
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
final timelineOrigin = ref.read(timelineServiceProvider).origin;
final kebabContext = ViewerKebabMenuButtonContext(
asset: asset,
isOwner: isOwner,
isCasting: isCasting,
timelineOrigin: timelineOrigin,
originalTheme: originalTheme,
);
final menuChildren = ViewerKebabMenuButtonBuilder.build(kebabContext, context, ref);
return MenuAnchor(
consumeOutsideTap: true,
style: MenuStyle(
backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor),
surfaceTintColor: const WidgetStatePropertyAll(Colors.grey),
elevation: const WidgetStatePropertyAll(4),
shape: const WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
),
menuChildren: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 150),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: menuChildren,
),
),
],
builder: (context, controller, child) {
return IconButton(
icon: const Icon(Icons.more_vert_rounded),
onPressed: () => controller.isOpen ? controller.close() : controller.open(),
);
},
);
}
}

View File

@@ -324,7 +324,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
final topPadding = context.padding.top + (widget.appBar == null ? 0 : kToolbarHeight) + 10; final topPadding = context.padding.top + (widget.appBar == null ? 0 : kToolbarHeight) + 10;
const scrubberBottomPadding = 100.0; const scrubberBottomPadding = 100.0;
final bottomPadding = context.padding.bottom + (widget.appBar == null ? 0 : scrubberBottomPadding); const bottomSheetOpenModifier = 120.0;
final bottomPadding =
context.padding.bottom +
(widget.appBar == null ? 0 : scrubberBottomPadding) +
(isMultiSelectEnabled ? bottomSheetOpenModifier : 0);
final grid = CustomScrollView( final grid = CustomScrollView(
primary: true, primary: true,
@@ -347,7 +351,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
addRepaintBoundaries: false, addRepaintBoundaries: false,
), ),
), ),
const SliverPadding(padding: EdgeInsets.only(bottom: scrubberBottomPadding)), SliverPadding(padding: EdgeInsets.only(bottom: bottomPadding)),
], ],
); );

View File

@@ -11,6 +11,7 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/share_intent_service.dart'; import 'package:immich_mobile/services/share_intent_service.dart';
import 'package:immich_mobile/services/upload.service.dart'; import 'package:immich_mobile/services/upload.service.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
final shareIntentUploadProvider = StateNotifierProvider<ShareIntentUploadStateNotifier, List<ShareIntentAttachment>>( final shareIntentUploadProvider = StateNotifierProvider<ShareIntentUploadStateNotifier, List<ShareIntentAttachment>>(
@@ -25,6 +26,7 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
final AppRouter router; final AppRouter router;
final UploadService _uploadService; final UploadService _uploadService;
final ShareIntentService _shareIntentService; final ShareIntentService _shareIntentService;
final Logger _logger = Logger('ShareIntentUploadStateNotifier');
ShareIntentUploadStateNotifier(this.router, this._uploadService, this._shareIntentService) : super([]) { ShareIntentUploadStateNotifier(this.router, this._uploadService, this._shareIntentService) : super([]) {
_uploadService.taskStatusStream.listen(_updateUploadStatus); _uploadService.taskStatusStream.listen(_updateUploadStatus);
@@ -86,6 +88,8 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
for (final attachment in state) for (final attachment in state)
if (attachment.id == taskId.toInt()) attachment.copyWith(status: uploadStatus) else attachment, if (attachment.id == taskId.toInt()) attachment.copyWith(status: uploadStatus) else attachment,
]; ];
_logger.fine("Upload failed for asset: ${task.task.filename}, exception: ${task.exception}");
} }
void _taskProgressCallback(TaskProgressUpdate update) { void _taskProgressCallback(TaskProgressUpdate update) {

View File

@@ -78,9 +78,9 @@ import 'package:immich_mobile/pages/search/recently_taken.page.dart';
import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart';
import 'package:immich_mobile/pages/settings/sync_status.page.dart'; import 'package:immich_mobile/pages/settings/sync_status.page.dart';
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
import 'package:immich_mobile/presentation/pages/dev/ui_showcase.page.dart';
import 'package:immich_mobile/presentation/pages/download_info.page.dart'; import 'package:immich_mobile/presentation/pages/download_info.page.dart';
import 'package:immich_mobile/presentation/pages/drift_activities.page.dart'; import 'package:immich_mobile/presentation/pages/drift_activities.page.dart';
import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
@@ -167,7 +167,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: LoginRoute.page, guards: [_duplicateGuard]), AutoRoute(page: LoginRoute.page, guards: [_duplicateGuard]),
AutoRoute(page: ChangePasswordRoute.page), AutoRoute(page: ChangePasswordRoute.page),
AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false),
CustomRoute( AutoRoute(
page: TabControllerRoute.page, page: TabControllerRoute.page,
guards: [_authGuard, _duplicateGuard], guards: [_authGuard, _duplicateGuard],
children: [ children: [
@@ -176,9 +176,8 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
], ],
transitionsBuilder: TransitionsBuilders.fadeIn,
), ),
CustomRoute( AutoRoute(
page: TabShellRoute.page, page: TabShellRoute.page,
guards: [_authGuard, _duplicateGuard], guards: [_authGuard, _duplicateGuard],
children: [ children: [
@@ -187,7 +186,6 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DriftLibraryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftLibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
], ],
transitionsBuilder: TransitionsBuilders.fadeIn,
), ),
CustomRoute( CustomRoute(
page: GalleryViewerRoute.page, page: GalleryViewerRoute.page,
@@ -288,7 +286,6 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: ShareIntentRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: ShareIntentRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: LockedRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]), AutoRoute(page: LockedRoute.page, guards: [_authGuard, _lockedGuard, _duplicateGuard]),
AutoRoute(page: PinAuthRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: PinAuthRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: FeatInDevRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: LocalMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LocalMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: RemoteMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: RemoteMediaSummaryRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftBackupRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftBackupRoute.page, guards: [_authGuard, _duplicateGuard]),
@@ -340,6 +337,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftBackupAssetDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: AssetTroubleshootRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DownloadInfoRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DownloadInfoRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: ImmichUIShowcaseRoute.page, guards: [_authGuard, _duplicateGuard]),
// required to handle all deeplinks in deep_link.service.dart // required to handle all deeplinks in deep_link.service.dart
// auto_route_library#1722 // auto_route_library#1722
RedirectRoute(path: '*', redirectTo: '/'), RedirectRoute(path: '*', redirectTo: '/'),

View File

@@ -1648,22 +1648,6 @@ class FavoritesRoute extends PageRouteInfo<void> {
); );
} }
/// generated route for
/// [FeatInDevPage]
class FeatInDevRoute extends PageRouteInfo<void> {
const FeatInDevRoute({List<PageRouteInfo>? children})
: super(FeatInDevRoute.name, initialChildren: children);
static const String name = 'FeatInDevRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const FeatInDevPage();
},
);
}
/// generated route for /// generated route for
/// [FilterImagePage] /// [FilterImagePage]
class FilterImageRoute extends PageRouteInfo<FilterImageRouteArgs> { class FilterImageRoute extends PageRouteInfo<FilterImageRouteArgs> {
@@ -1831,6 +1815,22 @@ class HeaderSettingsRoute extends PageRouteInfo<void> {
); );
} }
/// generated route for
/// [ImmichUIShowcasePage]
class ImmichUIShowcaseRoute extends PageRouteInfo<void> {
const ImmichUIShowcaseRoute({List<PageRouteInfo>? children})
: super(ImmichUIShowcaseRoute.name, initialChildren: children);
static const String name = 'ImmichUIShowcaseRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const ImmichUIShowcasePage();
},
);
}
/// generated route for /// generated route for
/// [LibraryPage] /// [LibraryPage]
class LibraryRoute extends PageRouteInfo<void> { class LibraryRoute extends PageRouteInfo<void> {

View File

@@ -1,9 +1,18 @@
import 'package:flutter/widgets.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
@@ -19,6 +28,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_b
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/routing/router.dart';
class ActionButtonContext { class ActionButtonContext {
final BaseAsset asset; final BaseAsset asset;
@@ -164,3 +174,98 @@ class ActionButtonBuilder {
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList(); return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
} }
} }
class ViewerKebabMenuButtonContext {
final BaseAsset asset;
final bool isOwner;
final bool isCasting;
final TimelineOrigin timelineOrigin;
final ThemeData? originalTheme;
const ViewerKebabMenuButtonContext({
required this.asset,
required this.isOwner,
required this.isCasting,
required this.timelineOrigin,
this.originalTheme,
});
}
enum ViewerKebabMenuButtonType {
openInfo,
viewInTimeline,
cast,
download;
/// Defines which group each button belongs to.
/// Buttons in the same group will be displayed together,
/// with dividers separating different groups.
int get group => switch (this) {
ViewerKebabMenuButtonType.openInfo => 0,
ViewerKebabMenuButtonType.viewInTimeline => 1,
ViewerKebabMenuButtonType.cast => 1,
ViewerKebabMenuButtonType.download => 1,
};
bool shouldShow(ViewerKebabMenuButtonContext context) {
return switch (this) {
ViewerKebabMenuButtonType.openInfo => true,
ViewerKebabMenuButtonType.viewInTimeline =>
context.timelineOrigin != TimelineOrigin.main &&
context.timelineOrigin != TimelineOrigin.deepLink &&
context.timelineOrigin != TimelineOrigin.trash &&
context.timelineOrigin != TimelineOrigin.archive &&
context.timelineOrigin != TimelineOrigin.localAlbum &&
context.isOwner,
ViewerKebabMenuButtonType.cast => context.isCasting || context.asset.hasRemote,
ViewerKebabMenuButtonType.download => context.asset.isRemoteOnly,
};
}
ConsumerWidget buildButton(ViewerKebabMenuButtonContext context, BuildContext buildContext) {
return switch (this) {
ViewerKebabMenuButtonType.openInfo => BaseActionButton(
label: 'info'.tr(),
iconData: Icons.info_outline,
iconColor: context.originalTheme?.iconTheme.color,
menuItem: true,
onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent()),
),
ViewerKebabMenuButtonType.viewInTimeline => BaseActionButton(
label: 'view_in_timeline'.t(context: buildContext),
iconData: Icons.image_search,
iconColor: context.originalTheme?.iconTheme.color,
menuItem: true,
onPressed: () async {
await buildContext.maybePop();
await buildContext.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
EventStream.shared.emit(ScrollToDateEvent(context.asset.createdAt));
},
),
ViewerKebabMenuButtonType.cast => const CastActionButton(menuItem: true),
ViewerKebabMenuButtonType.download => const DownloadActionButton(source: ActionSource.viewer, menuItem: true),
};
}
}
class ViewerKebabMenuButtonBuilder {
static List<Widget> build(ViewerKebabMenuButtonContext context, BuildContext buildContext, WidgetRef ref) {
final visibleButtons = ViewerKebabMenuButtonType.values.where((type) => type.shouldShow(context)).toList();
if (visibleButtons.isEmpty) return [];
final List<Widget> result = [];
int? lastGroup;
for (final type in visibleButtons) {
if (lastGroup != null && type.group != lastGroup) {
result.add(const Divider(height: 1));
}
result.add(type.buildButton(context, buildContext).build(buildContext, ref));
lastGroup = type.group;
}
return result;
}
}

View File

@@ -53,16 +53,18 @@ class ServerUpdateNotification extends HookConsumerWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Expanded(
serverInfoState.versionStatus.message, child: Text(
textAlign: TextAlign.start, serverInfoState.versionStatus.message,
maxLines: 3, textAlign: TextAlign.start,
overflow: TextOverflow.ellipsis, maxLines: 3,
style: context.textTheme.labelLarge, overflow: TextOverflow.ellipsis,
style: context.textTheme.labelLarge,
),
), ),
if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate || if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate ||
serverInfoState.versionStatus == VersionStatus.clientOutOfDate) ...[ serverInfoState.versionStatus == VersionStatus.clientOutOfDate) ...[
const Spacer(), const SizedBox(width: 8),
TextButton( TextButton(
onPressed: openUpdateLink, onPressed: openUpdateLink,
style: TextButton.styleFrom( style: TextButton.styleFrom(

View File

@@ -155,8 +155,8 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)), ...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)),
if (kDebugMode || kProfileMode) if (kDebugMode || kProfileMode)
IconButton( IconButton(
icon: const Icon(Icons.science_rounded), icon: const Icon(Icons.palette_rounded),
onPressed: () => context.pushRoute(const FeatInDevRoute()), onPressed: () => context.pushRoute(const ImmichUIShowcaseRoute()),
), ),
if (isCasting) if (isCasting)
Padding( Padding(

View File

@@ -74,8 +74,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)), ...actions!.map((action) => Padding(padding: const EdgeInsets.only(right: 16), child: action)),
if ((kDebugMode || kProfileMode) && !isReadonlyModeEnabled) if ((kDebugMode || kProfileMode) && !isReadonlyModeEnabled)
IconButton( IconButton(
icon: const Icon(Icons.science_rounded), icon: const Icon(Icons.palette_rounded),
onPressed: () => context.pushRoute(const FeatInDevRoute()), onPressed: () => context.pushRoute(const ImmichUIShowcaseRoute()),
), ),
if (showUploadButton && !isReadonlyModeEnabled) if (showUploadButton && !isReadonlyModeEnabled)
const Padding(padding: EdgeInsets.only(right: 20), child: _BackupIndicator()), const Padding(padding: EdgeInsets.only(right: 20), child: _BackupIndicator()),

View File

@@ -2,26 +2,27 @@ import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart';
class EntitiyCountTile extends StatelessWidget { class EntityCountTile extends StatelessWidget {
final int count; final int count;
final String label; final String label;
final IconData icon; final IconData icon;
const EntitiyCountTile({super.key, required this.count, required this.label, required this.icon}); const EntityCountTile({super.key, required this.count, required this.label, required this.icon});
String zeroPadding(int number, int targetWidth) { String zeroPadding(int number, int targetWidth) {
final numStr = number.toString(); final numStr = number.toString();
return numStr.length < targetWidth ? "0" * (targetWidth - numStr.length) : ""; return numStr.length < targetWidth ? "0" * (targetWidth - numStr.length) : "";
} }
int calculateMaxDigits(double availableWidth) {
const double charWidth = 11.0;
return (availableWidth / charWidth).floor().clamp(1, 8);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final availableWidth = (screenWidth - 32 - 8) / 2;
const double charWidth = 11.0;
final maxDigits = ((availableWidth - 32) / charWidth).floor().clamp(1, 8);
return Container( return Container(
height: double.infinity,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.colorScheme.surfaceContainerLow, color: context.colorScheme.surfaceContainerLow,
@@ -29,7 +30,6 @@ class EntitiyCountTile extends StatelessWidget {
border: Border.all(width: 0.5, color: context.colorScheme.outline.withAlpha(25)), border: Border.all(width: 0.5, color: context.colorScheme.outline.withAlpha(25)),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Icon and Label // Icon and Label
@@ -38,33 +38,30 @@ class EntitiyCountTile extends StatelessWidget {
children: [ children: [
Icon(icon, color: context.primaryColor), Icon(icon, color: context.primaryColor),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Flexible(
label, child: Text(
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16), label,
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
),
), ),
], ],
), ),
const SizedBox(height: 12),
// Number // Number
LayoutBuilder( const Spacer(),
builder: (context, constraints) { RichText(
final maxDigits = calculateMaxDigits(constraints.maxWidth); text: TextSpan(
return RichText( style: const TextStyle(fontSize: 18, fontFamily: 'OverpassMono', fontWeight: FontWeight.w600),
text: TextSpan( children: [
style: const TextStyle(fontSize: 18, fontFamily: 'OverpassMono', fontWeight: FontWeight.w600), TextSpan(
children: [ text: zeroPadding(count, maxDigits),
TextSpan( style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)),
text: zeroPadding(count, maxDigits),
style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)),
),
TextSpan(
text: count.toString(),
style: TextStyle(color: context.primaryColor),
),
],
), ),
); TextSpan(
}, text: count.toString(),
style: TextStyle(color: context.primaryColor),
),
],
),
), ),
], ],
), ),

View File

@@ -282,76 +282,87 @@ class _SyncStatsCounts extends ConsumerWidget {
_SectionHeaderText(text: "assets".t(context: context)), _SectionHeaderText(text: "assets".t(context: context)),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex( // 1. Wrap in IntrinsicHeight
direction: Axis.horizontal, child: IntrinsicHeight(
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: Flex(
spacing: 8.0, direction: Axis.horizontal,
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
Expanded( // 2. Stretch children vertically to fill the IntrinsicHeight
child: EntitiyCountTile( crossAxisAlignment: CrossAxisAlignment.stretch,
label: "local".t(context: context), spacing: 8.0,
count: localAssetCount, children: [
icon: Icons.smartphone, Expanded(
child: EntityCountTile(
label: "local".t(context: context),
count: localAssetCount,
icon: Icons.smartphone,
),
), ),
), Expanded(
Expanded( child: EntityCountTile(
child: EntitiyCountTile( label: "remote".t(context: context),
label: "remote".t(context: context), count: remoteAssetCount,
count: remoteAssetCount, icon: Icons.cloud,
icon: Icons.cloud, ),
), ),
), ],
], ),
), ),
), ),
_SectionHeaderText(text: "albums".t(context: context)), _SectionHeaderText(text: "albums".t(context: context)),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex( child: IntrinsicHeight(
direction: Axis.horizontal, child: Flex(
mainAxisAlignment: MainAxisAlignment.spaceBetween, direction: Axis.horizontal,
spacing: 8.0, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ crossAxisAlignment: CrossAxisAlignment.stretch, // Added
Expanded( spacing: 8.0,
child: EntitiyCountTile( children: [
label: "local".t(context: context), Expanded(
count: localAlbumCount, child: EntityCountTile(
icon: Icons.smartphone, label: "local".t(context: context),
count: localAlbumCount,
icon: Icons.smartphone,
),
), ),
), Expanded(
Expanded( child: EntityCountTile(
child: EntitiyCountTile( label: "remote".t(context: context),
label: "remote".t(context: context), count: remoteAlbumCount,
count: remoteAlbumCount, icon: Icons.cloud,
icon: Icons.cloud, ),
), ),
), ],
], ),
), ),
), ),
_SectionHeaderText(text: "other".t(context: context)), _SectionHeaderText(text: "other".t(context: context)),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex( child: IntrinsicHeight(
direction: Axis.horizontal, child: Flex(
mainAxisAlignment: MainAxisAlignment.spaceBetween, direction: Axis.horizontal,
spacing: 8.0, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ crossAxisAlignment: CrossAxisAlignment.stretch, // Added
Expanded( spacing: 8.0,
child: EntitiyCountTile( children: [
label: "memories".t(context: context), Expanded(
count: memoryCount, child: EntityCountTile(
icon: Icons.calendar_today, label: "memories".t(context: context),
count: memoryCount,
icon: Icons.calendar_today,
),
), ),
), Expanded(
Expanded( child: EntityCountTile(
child: EntitiyCountTile( label: "hashed_assets".t(context: context),
label: "hashed_assets".t(context: context), count: localHashedCount,
count: localHashedCount, icon: Icons.tag,
icon: Icons.tag, ),
), ),
), ],
], ),
), ),
), ),
// To be removed once the experimental feature is stable // To be removed once the experimental feature is stable
@@ -364,26 +375,29 @@ class _SyncStatsCounts extends ConsumerWidget {
return counts.when( return counts.when(
data: (c) => Padding( data: (c) => Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex( child: IntrinsicHeight(
direction: Axis.horizontal, child: Flex(
mainAxisAlignment: MainAxisAlignment.spaceBetween, direction: Axis.horizontal,
spacing: 8.0, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ crossAxisAlignment: CrossAxisAlignment.stretch, // Added
Expanded( spacing: 8.0,
child: EntitiyCountTile( children: [
label: "local".t(context: context), Expanded(
count: c.total, child: EntityCountTile(
icon: Icons.delete_outline, label: "local".t(context: context),
count: c.total,
icon: Icons.delete_outline,
),
), ),
), Expanded(
Expanded( child: EntityCountTile(
child: EntitiyCountTile( label: "hashed_assets".t(context: context),
label: "hashed_assets".t(context: context), count: c.hashed,
count: c.hashed, icon: Icons.tag,
icon: Icons.tag, ),
), ),
), ],
], ),
), ),
), ),
loading: () => const CircularProgressIndicator(), loading: () => const CircularProgressIndicator(),

View File

@@ -101,9 +101,7 @@ Class | Method | HTTP request | Description
*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
*AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets | Delete assets *AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets | Delete assets
*AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original | Download original asset *AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original | Download original asset
*AssetsApi* | [**editAsset**](doc//AssetsApi.md#editasset) | **PUT** /assets/{id}/edits | Applies edits to an existing asset
*AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID *AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Retrieve assets by device ID
*AssetsApi* | [**getAssetEdits**](doc//AssetsApi.md#getassetedits) | **GET** /assets/{id}/edits | Retrieve edits for an existing asset
*AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} | Retrieve an asset *AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} | Retrieve an asset
*AssetsApi* | [**getAssetMetadata**](doc//AssetsApi.md#getassetmetadata) | **GET** /assets/{id}/metadata | Get asset metadata *AssetsApi* | [**getAssetMetadata**](doc//AssetsApi.md#getassetmetadata) | **GET** /assets/{id}/metadata | Get asset metadata
*AssetsApi* | [**getAssetMetadataByKey**](doc//AssetsApi.md#getassetmetadatabykey) | **GET** /assets/{id}/metadata/{key} | Retrieve asset metadata by key *AssetsApi* | [**getAssetMetadataByKey**](doc//AssetsApi.md#getassetmetadatabykey) | **GET** /assets/{id}/metadata/{key} | Retrieve asset metadata by key
@@ -111,7 +109,6 @@ Class | Method | HTTP request | Description
*AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | Get asset statistics *AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics | Get asset statistics
*AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random | Get random assets *AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random | Get random assets
*AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | Play asset video *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | Play asset video
*AssetsApi* | [**removeAssetEdits**](doc//AssetsApi.md#removeassetedits) | **DELETE** /assets/{id}/edits | Remove edits from an existing asset
*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset *AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset
*AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | Run an asset job *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | Run an asset job
*AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | Update an asset *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | Update an asset
@@ -346,8 +343,6 @@ Class | Method | HTTP request | Description
- [AssetCopyDto](doc//AssetCopyDto.md) - [AssetCopyDto](doc//AssetCopyDto.md)
- [AssetDeltaSyncDto](doc//AssetDeltaSyncDto.md) - [AssetDeltaSyncDto](doc//AssetDeltaSyncDto.md)
- [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md) - [AssetDeltaSyncResponseDto](doc//AssetDeltaSyncResponseDto.md)
- [AssetEditsDto](doc//AssetEditsDto.md)
- [AssetEditsDtoEditsInner](doc//AssetEditsDtoEditsInner.md)
- [AssetFaceCreateDto](doc//AssetFaceCreateDto.md) - [AssetFaceCreateDto](doc//AssetFaceCreateDto.md)
- [AssetFaceDeleteDto](doc//AssetFaceDeleteDto.md) - [AssetFaceDeleteDto](doc//AssetFaceDeleteDto.md)
- [AssetFaceResponseDto](doc//AssetFaceResponseDto.md) - [AssetFaceResponseDto](doc//AssetFaceResponseDto.md)
@@ -391,7 +386,6 @@ Class | Method | HTTP request | Description
- [CreateAlbumDto](doc//CreateAlbumDto.md) - [CreateAlbumDto](doc//CreateAlbumDto.md)
- [CreateLibraryDto](doc//CreateLibraryDto.md) - [CreateLibraryDto](doc//CreateLibraryDto.md)
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md) - [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
- [CropParameters](doc//CropParameters.md)
- [DatabaseBackupConfig](doc//DatabaseBackupConfig.md) - [DatabaseBackupConfig](doc//DatabaseBackupConfig.md)
- [DownloadArchiveInfo](doc//DownloadArchiveInfo.md) - [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
- [DownloadInfoDto](doc//DownloadInfoDto.md) - [DownloadInfoDto](doc//DownloadInfoDto.md)
@@ -400,11 +394,6 @@ Class | Method | HTTP request | Description
- [DownloadUpdate](doc//DownloadUpdate.md) - [DownloadUpdate](doc//DownloadUpdate.md)
- [DuplicateDetectionConfig](doc//DuplicateDetectionConfig.md) - [DuplicateDetectionConfig](doc//DuplicateDetectionConfig.md)
- [DuplicateResponseDto](doc//DuplicateResponseDto.md) - [DuplicateResponseDto](doc//DuplicateResponseDto.md)
- [EditAction](doc//EditAction.md)
- [EditActionCrop](doc//EditActionCrop.md)
- [EditActionListDto](doc//EditActionListDto.md)
- [EditActionMirror](doc//EditActionMirror.md)
- [EditActionRotate](doc//EditActionRotate.md)
- [EmailNotificationsResponse](doc//EmailNotificationsResponse.md) - [EmailNotificationsResponse](doc//EmailNotificationsResponse.md)
- [EmailNotificationsUpdate](doc//EmailNotificationsUpdate.md) - [EmailNotificationsUpdate](doc//EmailNotificationsUpdate.md)
- [ExifResponseDto](doc//ExifResponseDto.md) - [ExifResponseDto](doc//ExifResponseDto.md)
@@ -441,8 +430,6 @@ Class | Method | HTTP request | Description
- [MemoryUpdateDto](doc//MemoryUpdateDto.md) - [MemoryUpdateDto](doc//MemoryUpdateDto.md)
- [MergePersonDto](doc//MergePersonDto.md) - [MergePersonDto](doc//MergePersonDto.md)
- [MetadataSearchDto](doc//MetadataSearchDto.md) - [MetadataSearchDto](doc//MetadataSearchDto.md)
- [MirrorAxis](doc//MirrorAxis.md)
- [MirrorParameters](doc//MirrorParameters.md)
- [NotificationCreateDto](doc//NotificationCreateDto.md) - [NotificationCreateDto](doc//NotificationCreateDto.md)
- [NotificationDeleteAllDto](doc//NotificationDeleteAllDto.md) - [NotificationDeleteAllDto](doc//NotificationDeleteAllDto.md)
- [NotificationDto](doc//NotificationDto.md) - [NotificationDto](doc//NotificationDto.md)
@@ -502,7 +489,6 @@ Class | Method | HTTP request | Description
- [ReactionLevel](doc//ReactionLevel.md) - [ReactionLevel](doc//ReactionLevel.md)
- [ReactionType](doc//ReactionType.md) - [ReactionType](doc//ReactionType.md)
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md) - [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
- [RotateParameters](doc//RotateParameters.md)
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
- [SearchAssetResponseDto](doc//SearchAssetResponseDto.md) - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md)
- [SearchExploreItem](doc//SearchExploreItem.md) - [SearchExploreItem](doc//SearchExploreItem.md)

View File

@@ -95,8 +95,6 @@ part 'model/asset_bulk_upload_check_result.dart';
part 'model/asset_copy_dto.dart'; part 'model/asset_copy_dto.dart';
part 'model/asset_delta_sync_dto.dart'; part 'model/asset_delta_sync_dto.dart';
part 'model/asset_delta_sync_response_dto.dart'; part 'model/asset_delta_sync_response_dto.dart';
part 'model/asset_edits_dto.dart';
part 'model/asset_edits_dto_edits_inner.dart';
part 'model/asset_face_create_dto.dart'; part 'model/asset_face_create_dto.dart';
part 'model/asset_face_delete_dto.dart'; part 'model/asset_face_delete_dto.dart';
part 'model/asset_face_response_dto.dart'; part 'model/asset_face_response_dto.dart';
@@ -140,7 +138,6 @@ part 'model/contributor_count_response_dto.dart';
part 'model/create_album_dto.dart'; part 'model/create_album_dto.dart';
part 'model/create_library_dto.dart'; part 'model/create_library_dto.dart';
part 'model/create_profile_image_response_dto.dart'; part 'model/create_profile_image_response_dto.dart';
part 'model/crop_parameters.dart';
part 'model/database_backup_config.dart'; part 'model/database_backup_config.dart';
part 'model/download_archive_info.dart'; part 'model/download_archive_info.dart';
part 'model/download_info_dto.dart'; part 'model/download_info_dto.dart';
@@ -149,11 +146,6 @@ part 'model/download_response_dto.dart';
part 'model/download_update.dart'; part 'model/download_update.dart';
part 'model/duplicate_detection_config.dart'; part 'model/duplicate_detection_config.dart';
part 'model/duplicate_response_dto.dart'; part 'model/duplicate_response_dto.dart';
part 'model/edit_action.dart';
part 'model/edit_action_crop.dart';
part 'model/edit_action_list_dto.dart';
part 'model/edit_action_mirror.dart';
part 'model/edit_action_rotate.dart';
part 'model/email_notifications_response.dart'; part 'model/email_notifications_response.dart';
part 'model/email_notifications_update.dart'; part 'model/email_notifications_update.dart';
part 'model/exif_response_dto.dart'; part 'model/exif_response_dto.dart';
@@ -190,8 +182,6 @@ 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_person_dto.dart';
part 'model/metadata_search_dto.dart'; part 'model/metadata_search_dto.dart';
part 'model/mirror_axis.dart';
part 'model/mirror_parameters.dart';
part 'model/notification_create_dto.dart'; part 'model/notification_create_dto.dart';
part 'model/notification_delete_all_dto.dart'; part 'model/notification_delete_all_dto.dart';
part 'model/notification_dto.dart'; part 'model/notification_dto.dart';
@@ -251,7 +241,6 @@ part 'model/ratings_update.dart';
part 'model/reaction_level.dart'; part 'model/reaction_level.dart';
part 'model/reaction_type.dart'; part 'model/reaction_type.dart';
part 'model/reverse_geocoding_state_response_dto.dart'; part 'model/reverse_geocoding_state_response_dto.dart';
part 'model/rotate_parameters.dart';
part 'model/search_album_response_dto.dart'; part 'model/search_album_response_dto.dart';
part 'model/search_asset_response_dto.dart'; part 'model/search_asset_response_dto.dart';
part 'model/search_explore_item.dart'; part 'model/search_explore_item.dart';

View File

@@ -288,12 +288,10 @@ class AssetsApi {
/// ///
/// * [String] id (required): /// * [String] id (required):
/// ///
/// * [bool] edited:
///
/// * [String] key: /// * [String] key:
/// ///
/// * [String] slug: /// * [String] slug:
Future<Response> downloadAssetWithHttpInfo(String id, { bool? edited, String? key, String? slug, }) async { Future<Response> downloadAssetWithHttpInfo(String id, { String? key, String? slug, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/assets/{id}/original' final apiPath = r'/assets/{id}/original'
.replaceAll('{id}', id); .replaceAll('{id}', id);
@@ -305,9 +303,6 @@ class AssetsApi {
final headerParams = <String, String>{}; final headerParams = <String, String>{};
final formParams = <String, String>{}; final formParams = <String, String>{};
if (edited != null) {
queryParams.addAll(_queryParams('', 'edited', edited));
}
if (key != null) { if (key != null) {
queryParams.addAll(_queryParams('', 'key', key)); queryParams.addAll(_queryParams('', 'key', key));
} }
@@ -337,13 +332,11 @@ class AssetsApi {
/// ///
/// * [String] id (required): /// * [String] id (required):
/// ///
/// * [bool] edited:
///
/// * [String] key: /// * [String] key:
/// ///
/// * [String] slug: /// * [String] slug:
Future<MultipartFile?> downloadAsset(String id, { bool? edited, String? key, String? slug, }) async { Future<MultipartFile?> downloadAsset(String id, { String? key, String? slug, }) async {
final response = await downloadAssetWithHttpInfo(id, edited: edited, key: key, slug: slug, ); final response = await downloadAssetWithHttpInfo(id, key: key, slug: slug, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
@@ -357,67 +350,6 @@ class AssetsApi {
return null; return null;
} }
/// Applies edits to an existing asset
///
/// Applies a series of edit actions (crop, rotate, mirror) to the specified asset.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [EditActionListDto] editActionListDto (required):
Future<Response> editAssetWithHttpInfo(String id, EditActionListDto editActionListDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/assets/{id}/edits'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = editActionListDto;
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,
);
}
/// Applies edits to an existing asset
///
/// Applies a series of edit actions (crop, rotate, mirror) to the specified asset.
///
/// Parameters:
///
/// * [String] id (required):
///
/// * [EditActionListDto] editActionListDto (required):
Future<AssetEditsDto?> editAsset(String id, EditActionListDto editActionListDto,) async {
final response = await editAssetWithHttpInfo(id, editActionListDto,);
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), 'AssetEditsDto',) as AssetEditsDto;
}
return null;
}
/// Retrieve assets by device ID /// Retrieve assets by device ID
/// ///
/// Get all asset of a device that are in the database, ID only. /// Get all asset of a device that are in the database, ID only.
@@ -478,63 +410,6 @@ class AssetsApi {
return null; return null;
} }
/// Retrieve edits for an existing asset
///
/// Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
Future<Response> getAssetEditsWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/assets/{id}/edits'
.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,
);
}
/// Retrieve edits for an existing asset
///
/// Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset.
///
/// Parameters:
///
/// * [String] id (required):
Future<AssetEditsDto?> getAssetEdits(String id,) async {
final response = await getAssetEditsWithHttpInfo(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), 'AssetEditsDto',) as AssetEditsDto;
}
return null;
}
/// Retrieve an asset /// Retrieve an asset
/// ///
/// Retrieve detailed information about a specific asset. /// Retrieve detailed information about a specific asset.
@@ -998,55 +873,6 @@ class AssetsApi {
return null; return null;
} }
/// Remove edits from an existing asset
///
/// Removes all edit actions (crop, rotate, mirror) associated with the specified asset.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] id (required):
Future<Response> removeAssetEditsWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/assets/{id}/edits'
.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,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Remove edits from an existing asset
///
/// Removes all edit actions (crop, rotate, mirror) associated with the specified asset.
///
/// Parameters:
///
/// * [String] id (required):
Future<void> removeAssetEdits(String id,) async {
final response = await removeAssetEditsWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
}
/// Replace asset /// Replace asset
/// ///
/// Replace the asset with new file, without changing its id. /// Replace the asset with new file, without changing its id.
@@ -1592,14 +1418,12 @@ class AssetsApi {
/// ///
/// * [String] id (required): /// * [String] id (required):
/// ///
/// * [bool] edited:
///
/// * [String] key: /// * [String] key:
/// ///
/// * [AssetMediaSize] size: /// * [AssetMediaSize] size:
/// ///
/// * [String] slug: /// * [String] slug:
Future<Response> viewAssetWithHttpInfo(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { Future<Response> viewAssetWithHttpInfo(String id, { String? key, AssetMediaSize? size, String? slug, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/assets/{id}/thumbnail' final apiPath = r'/assets/{id}/thumbnail'
.replaceAll('{id}', id); .replaceAll('{id}', id);
@@ -1611,9 +1435,6 @@ class AssetsApi {
final headerParams = <String, String>{}; final headerParams = <String, String>{};
final formParams = <String, String>{}; final formParams = <String, String>{};
if (edited != null) {
queryParams.addAll(_queryParams('', 'edited', edited));
}
if (key != null) { if (key != null) {
queryParams.addAll(_queryParams('', 'key', key)); queryParams.addAll(_queryParams('', 'key', key));
} }
@@ -1646,15 +1467,13 @@ class AssetsApi {
/// ///
/// * [String] id (required): /// * [String] id (required):
/// ///
/// * [bool] edited:
///
/// * [String] key: /// * [String] key:
/// ///
/// * [AssetMediaSize] size: /// * [AssetMediaSize] size:
/// ///
/// * [String] slug: /// * [String] slug:
Future<MultipartFile?> viewAsset(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { Future<MultipartFile?> viewAsset(String id, { String? key, AssetMediaSize? size, String? slug, }) async {
final response = await viewAssetWithHttpInfo(id, edited: edited, key: key, size: size, slug: slug, ); final response = await viewAssetWithHttpInfo(id, key: key, size: size, slug: slug, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }

View File

@@ -238,10 +238,6 @@ class ApiClient {
return AssetDeltaSyncDto.fromJson(value); return AssetDeltaSyncDto.fromJson(value);
case 'AssetDeltaSyncResponseDto': case 'AssetDeltaSyncResponseDto':
return AssetDeltaSyncResponseDto.fromJson(value); return AssetDeltaSyncResponseDto.fromJson(value);
case 'AssetEditsDto':
return AssetEditsDto.fromJson(value);
case 'AssetEditsDtoEditsInner':
return AssetEditsDtoEditsInner.fromJson(value);
case 'AssetFaceCreateDto': case 'AssetFaceCreateDto':
return AssetFaceCreateDto.fromJson(value); return AssetFaceCreateDto.fromJson(value);
case 'AssetFaceDeleteDto': case 'AssetFaceDeleteDto':
@@ -328,8 +324,6 @@ class ApiClient {
return CreateLibraryDto.fromJson(value); return CreateLibraryDto.fromJson(value);
case 'CreateProfileImageResponseDto': case 'CreateProfileImageResponseDto':
return CreateProfileImageResponseDto.fromJson(value); return CreateProfileImageResponseDto.fromJson(value);
case 'CropParameters':
return CropParameters.fromJson(value);
case 'DatabaseBackupConfig': case 'DatabaseBackupConfig':
return DatabaseBackupConfig.fromJson(value); return DatabaseBackupConfig.fromJson(value);
case 'DownloadArchiveInfo': case 'DownloadArchiveInfo':
@@ -346,16 +340,6 @@ class ApiClient {
return DuplicateDetectionConfig.fromJson(value); return DuplicateDetectionConfig.fromJson(value);
case 'DuplicateResponseDto': case 'DuplicateResponseDto':
return DuplicateResponseDto.fromJson(value); return DuplicateResponseDto.fromJson(value);
case 'EditAction':
return EditActionTypeTransformer().decode(value);
case 'EditActionCrop':
return EditActionCrop.fromJson(value);
case 'EditActionListDto':
return EditActionListDto.fromJson(value);
case 'EditActionMirror':
return EditActionMirror.fromJson(value);
case 'EditActionRotate':
return EditActionRotate.fromJson(value);
case 'EmailNotificationsResponse': case 'EmailNotificationsResponse':
return EmailNotificationsResponse.fromJson(value); return EmailNotificationsResponse.fromJson(value);
case 'EmailNotificationsUpdate': case 'EmailNotificationsUpdate':
@@ -428,10 +412,6 @@ class ApiClient {
return MergePersonDto.fromJson(value); return MergePersonDto.fromJson(value);
case 'MetadataSearchDto': case 'MetadataSearchDto':
return MetadataSearchDto.fromJson(value); return MetadataSearchDto.fromJson(value);
case 'MirrorAxis':
return MirrorAxisTypeTransformer().decode(value);
case 'MirrorParameters':
return MirrorParameters.fromJson(value);
case 'NotificationCreateDto': case 'NotificationCreateDto':
return NotificationCreateDto.fromJson(value); return NotificationCreateDto.fromJson(value);
case 'NotificationDeleteAllDto': case 'NotificationDeleteAllDto':
@@ -550,8 +530,6 @@ class ApiClient {
return ReactionTypeTypeTransformer().decode(value); return ReactionTypeTypeTransformer().decode(value);
case 'ReverseGeocodingStateResponseDto': case 'ReverseGeocodingStateResponseDto':
return ReverseGeocodingStateResponseDto.fromJson(value); return ReverseGeocodingStateResponseDto.fromJson(value);
case 'RotateParameters':
return RotateParameters.fromJson(value);
case 'SearchAlbumResponseDto': case 'SearchAlbumResponseDto':
return SearchAlbumResponseDto.fromJson(value); return SearchAlbumResponseDto.fromJson(value);
case 'SearchAssetResponseDto': case 'SearchAssetResponseDto':

View File

@@ -91,9 +91,6 @@ String parameterToString(dynamic value) {
if (value is Colorspace) { if (value is Colorspace) {
return ColorspaceTypeTransformer().encode(value).toString(); return ColorspaceTypeTransformer().encode(value).toString();
} }
if (value is EditAction) {
return EditActionTypeTransformer().encode(value).toString();
}
if (value is ImageFormat) { if (value is ImageFormat) {
return ImageFormatTypeTransformer().encode(value).toString(); return ImageFormatTypeTransformer().encode(value).toString();
} }
@@ -115,9 +112,6 @@ String parameterToString(dynamic value) {
if (value is MemoryType) { if (value is MemoryType) {
return MemoryTypeTypeTransformer().encode(value).toString(); return MemoryTypeTypeTransformer().encode(value).toString();
} }
if (value is MirrorAxis) {
return MirrorAxisTypeTransformer().encode(value).toString();
}
if (value is NotificationLevel) { if (value is NotificationLevel) {
return NotificationLevelTypeTransformer().encode(value).toString(); return NotificationLevelTypeTransformer().encode(value).toString();
} }

View File

@@ -1,108 +0,0 @@
//
// 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 AssetEditsDto {
/// Returns a new [AssetEditsDto] instance.
AssetEditsDto({
required this.assetId,
this.edits = const [],
});
String assetId;
/// list of edits
List<AssetEditsDtoEditsInner> edits;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetEditsDto &&
other.assetId == assetId &&
_deepEquality.equals(other.edits, edits);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(assetId.hashCode) +
(edits.hashCode);
@override
String toString() => 'AssetEditsDto[assetId=$assetId, edits=$edits]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'assetId'] = this.assetId;
json[r'edits'] = this.edits;
return json;
}
/// Returns a new [AssetEditsDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetEditsDto? fromJson(dynamic value) {
upgradeDto(value, "AssetEditsDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return AssetEditsDto(
assetId: mapValueOfType<String>(json, r'assetId')!,
edits: AssetEditsDtoEditsInner.listFromJson(json[r'edits']),
);
}
return null;
}
static List<AssetEditsDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetEditsDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetEditsDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetEditsDto> mapFromJson(dynamic json) {
final map = <String, AssetEditsDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetEditsDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetEditsDto-objects as value to a dart map
static Map<String, List<AssetEditsDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetEditsDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AssetEditsDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'assetId',
'edits',
};
}

View File

@@ -1,107 +0,0 @@
//
// 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 AssetEditsDtoEditsInner {
/// Returns a new [AssetEditsDtoEditsInner] instance.
AssetEditsDtoEditsInner({
required this.action,
required this.parameters,
});
EditAction action;
MirrorParameters parameters;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetEditsDtoEditsInner &&
other.action == action &&
other.parameters == parameters;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(action.hashCode) +
(parameters.hashCode);
@override
String toString() => 'AssetEditsDtoEditsInner[action=$action, parameters=$parameters]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'action'] = this.action;
json[r'parameters'] = this.parameters;
return json;
}
/// Returns a new [AssetEditsDtoEditsInner] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetEditsDtoEditsInner? fromJson(dynamic value) {
upgradeDto(value, "AssetEditsDtoEditsInner");
if (value is Map) {
final json = value.cast<String, dynamic>();
return AssetEditsDtoEditsInner(
action: EditAction.fromJson(json[r'action'])!,
parameters: MirrorParameters.fromJson(json[r'parameters'])!,
);
}
return null;
}
static List<AssetEditsDtoEditsInner> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetEditsDtoEditsInner>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetEditsDtoEditsInner.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetEditsDtoEditsInner> mapFromJson(dynamic json) {
final map = <String, AssetEditsDtoEditsInner>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetEditsDtoEditsInner.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetEditsDtoEditsInner-objects as value to a dart map
static Map<String, List<AssetEditsDtoEditsInner>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetEditsDtoEditsInner>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AssetEditsDtoEditsInner.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'action',
'parameters',
};
}

View File

@@ -23,7 +23,6 @@ class AssetResponseDto {
required this.fileCreatedAt, required this.fileCreatedAt,
required this.fileModifiedAt, required this.fileModifiedAt,
required this.hasMetadata, required this.hasMetadata,
required this.height,
required this.id, required this.id,
required this.isArchived, required this.isArchived,
required this.isFavorite, required this.isFavorite,
@@ -46,7 +45,6 @@ class AssetResponseDto {
this.unassignedFaces = const [], this.unassignedFaces = const [],
required this.updatedAt, required this.updatedAt,
required this.visibility, required this.visibility,
required this.width,
}); });
/// base64 encoded sha1 hash /// base64 encoded sha1 hash
@@ -79,8 +77,6 @@ class AssetResponseDto {
bool hasMetadata; bool hasMetadata;
num? height;
String id; String id;
bool isArchived; bool isArchived;
@@ -145,8 +141,6 @@ class AssetResponseDto {
AssetVisibility visibility; AssetVisibility visibility;
num? width;
@override @override
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto && bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
other.checksum == checksum && other.checksum == checksum &&
@@ -159,7 +153,6 @@ class AssetResponseDto {
other.fileCreatedAt == fileCreatedAt && other.fileCreatedAt == fileCreatedAt &&
other.fileModifiedAt == fileModifiedAt && other.fileModifiedAt == fileModifiedAt &&
other.hasMetadata == hasMetadata && other.hasMetadata == hasMetadata &&
other.height == height &&
other.id == id && other.id == id &&
other.isArchived == isArchived && other.isArchived == isArchived &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
@@ -181,8 +174,7 @@ class AssetResponseDto {
other.type == type && other.type == type &&
_deepEquality.equals(other.unassignedFaces, unassignedFaces) && _deepEquality.equals(other.unassignedFaces, unassignedFaces) &&
other.updatedAt == updatedAt && other.updatedAt == updatedAt &&
other.visibility == visibility && other.visibility == visibility;
other.width == width;
@override @override
int get hashCode => int get hashCode =>
@@ -197,7 +189,6 @@ class AssetResponseDto {
(fileCreatedAt.hashCode) + (fileCreatedAt.hashCode) +
(fileModifiedAt.hashCode) + (fileModifiedAt.hashCode) +
(hasMetadata.hashCode) + (hasMetadata.hashCode) +
(height == null ? 0 : height!.hashCode) +
(id.hashCode) + (id.hashCode) +
(isArchived.hashCode) + (isArchived.hashCode) +
(isFavorite.hashCode) + (isFavorite.hashCode) +
@@ -219,11 +210,10 @@ class AssetResponseDto {
(type.hashCode) + (type.hashCode) +
(unassignedFaces.hashCode) + (unassignedFaces.hashCode) +
(updatedAt.hashCode) + (updatedAt.hashCode) +
(visibility.hashCode) + (visibility.hashCode);
(width == null ? 0 : width!.hashCode);
@override @override
String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, 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, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility, width=$width]'; String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, 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, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -245,11 +235,6 @@ class AssetResponseDto {
json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String(); json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String();
json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String(); json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String();
json[r'hasMetadata'] = this.hasMetadata; json[r'hasMetadata'] = this.hasMetadata;
if (this.height != null) {
json[r'height'] = this.height;
} else {
// json[r'height'] = null;
}
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'isArchived'] = this.isArchived; json[r'isArchived'] = this.isArchived;
json[r'isFavorite'] = this.isFavorite; json[r'isFavorite'] = this.isFavorite;
@@ -300,11 +285,6 @@ class AssetResponseDto {
json[r'unassignedFaces'] = this.unassignedFaces; json[r'unassignedFaces'] = this.unassignedFaces;
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
json[r'visibility'] = this.visibility; json[r'visibility'] = this.visibility;
if (this.width != null) {
json[r'width'] = this.width;
} else {
// json[r'width'] = null;
}
return json; return json;
} }
@@ -327,9 +307,6 @@ class AssetResponseDto {
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!, fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!,
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!, fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!,
hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!, hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!,
height: json[r'height'] == null
? null
: num.parse('${json[r'height']}'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isArchived: mapValueOfType<bool>(json, r'isArchived')!, isArchived: mapValueOfType<bool>(json, r'isArchived')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!, isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
@@ -352,9 +329,6 @@ class AssetResponseDto {
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']), unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
updatedAt: mapDateTime(json, r'updatedAt', r'')!, updatedAt: mapDateTime(json, r'updatedAt', r'')!,
visibility: AssetVisibility.fromJson(json[r'visibility'])!, visibility: AssetVisibility.fromJson(json[r'visibility'])!,
width: json[r'width'] == null
? null
: num.parse('${json[r'width']}'),
); );
} }
return null; return null;
@@ -410,7 +384,6 @@ class AssetResponseDto {
'fileCreatedAt', 'fileCreatedAt',
'fileModifiedAt', 'fileModifiedAt',
'hasMetadata', 'hasMetadata',
'height',
'id', 'id',
'isArchived', 'isArchived',
'isFavorite', 'isFavorite',
@@ -424,7 +397,6 @@ class AssetResponseDto {
'type', 'type',
'updatedAt', 'updatedAt',
'visibility', 'visibility',
'width',
}; };
} }

View File

@@ -1,135 +0,0 @@
//
// 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 CropParameters {
/// Returns a new [CropParameters] instance.
CropParameters({
required this.height,
required this.width,
required this.x,
required this.y,
});
/// Height of the crop
///
/// Minimum value: 1
num height;
/// Width of the crop
///
/// Minimum value: 1
num width;
/// Top-Left X coordinate of crop
///
/// Minimum value: 0
num x;
/// Top-Left Y coordinate of crop
///
/// Minimum value: 0
num y;
@override
bool operator ==(Object other) => identical(this, other) || other is CropParameters &&
other.height == height &&
other.width == width &&
other.x == x &&
other.y == y;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(height.hashCode) +
(width.hashCode) +
(x.hashCode) +
(y.hashCode);
@override
String toString() => 'CropParameters[height=$height, width=$width, x=$x, y=$y]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'height'] = this.height;
json[r'width'] = this.width;
json[r'x'] = this.x;
json[r'y'] = this.y;
return json;
}
/// Returns a new [CropParameters] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static CropParameters? fromJson(dynamic value) {
upgradeDto(value, "CropParameters");
if (value is Map) {
final json = value.cast<String, dynamic>();
return CropParameters(
height: num.parse('${json[r'height']}'),
width: num.parse('${json[r'width']}'),
x: num.parse('${json[r'x']}'),
y: num.parse('${json[r'y']}'),
);
}
return null;
}
static List<CropParameters> listFromJson(dynamic json, {bool growable = false,}) {
final result = <CropParameters>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = CropParameters.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, CropParameters> mapFromJson(dynamic json) {
final map = <String, CropParameters>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = CropParameters.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of CropParameters-objects as value to a dart map
static Map<String, List<CropParameters>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<CropParameters>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = CropParameters.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'height',
'width',
'x',
'y',
};
}

View File

@@ -1,88 +0,0 @@
//
// 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 EditAction {
/// Instantiate a new enum with the provided [value].
const EditAction._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const crop = EditAction._(r'crop');
static const rotate = EditAction._(r'rotate');
static const mirror = EditAction._(r'mirror');
/// List of all possible values in this [enum][EditAction].
static const values = <EditAction>[
crop,
rotate,
mirror,
];
static EditAction? fromJson(dynamic value) => EditActionTypeTransformer().decode(value);
static List<EditAction> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EditAction>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EditAction.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [EditAction] to String,
/// and [decode] dynamic data back to [EditAction].
class EditActionTypeTransformer {
factory EditActionTypeTransformer() => _instance ??= const EditActionTypeTransformer._();
const EditActionTypeTransformer._();
String encode(EditAction data) => data.value;
/// Decodes a [dynamic value][data] to a EditAction.
///
/// 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.
EditAction? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'crop': return EditAction.crop;
case r'rotate': return EditAction.rotate;
case r'mirror': return EditAction.mirror;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [EditActionTypeTransformer] instance.
static EditActionTypeTransformer? _instance;
}

View File

@@ -1,107 +0,0 @@
//
// 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 EditActionCrop {
/// Returns a new [EditActionCrop] instance.
EditActionCrop({
required this.action,
required this.parameters,
});
EditAction action;
CropParameters parameters;
@override
bool operator ==(Object other) => identical(this, other) || other is EditActionCrop &&
other.action == action &&
other.parameters == parameters;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(action.hashCode) +
(parameters.hashCode);
@override
String toString() => 'EditActionCrop[action=$action, parameters=$parameters]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'action'] = this.action;
json[r'parameters'] = this.parameters;
return json;
}
/// Returns a new [EditActionCrop] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static EditActionCrop? fromJson(dynamic value) {
upgradeDto(value, "EditActionCrop");
if (value is Map) {
final json = value.cast<String, dynamic>();
return EditActionCrop(
action: EditAction.fromJson(json[r'action'])!,
parameters: CropParameters.fromJson(json[r'parameters'])!,
);
}
return null;
}
static List<EditActionCrop> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EditActionCrop>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EditActionCrop.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, EditActionCrop> mapFromJson(dynamic json) {
final map = <String, EditActionCrop>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = EditActionCrop.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of EditActionCrop-objects as value to a dart map
static Map<String, List<EditActionCrop>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<EditActionCrop>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = EditActionCrop.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'action',
'parameters',
};
}

View File

@@ -1,100 +0,0 @@
//
// 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 EditActionListDto {
/// Returns a new [EditActionListDto] instance.
EditActionListDto({
this.edits = const [],
});
/// list of edits
List<AssetEditsDtoEditsInner> edits;
@override
bool operator ==(Object other) => identical(this, other) || other is EditActionListDto &&
_deepEquality.equals(other.edits, edits);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(edits.hashCode);
@override
String toString() => 'EditActionListDto[edits=$edits]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'edits'] = this.edits;
return json;
}
/// Returns a new [EditActionListDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static EditActionListDto? fromJson(dynamic value) {
upgradeDto(value, "EditActionListDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return EditActionListDto(
edits: AssetEditsDtoEditsInner.listFromJson(json[r'edits']),
);
}
return null;
}
static List<EditActionListDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EditActionListDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EditActionListDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, EditActionListDto> mapFromJson(dynamic json) {
final map = <String, EditActionListDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = EditActionListDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of EditActionListDto-objects as value to a dart map
static Map<String, List<EditActionListDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<EditActionListDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = EditActionListDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'edits',
};
}

View File

@@ -1,107 +0,0 @@
//
// 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 EditActionMirror {
/// Returns a new [EditActionMirror] instance.
EditActionMirror({
required this.action,
required this.parameters,
});
EditAction action;
MirrorParameters parameters;
@override
bool operator ==(Object other) => identical(this, other) || other is EditActionMirror &&
other.action == action &&
other.parameters == parameters;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(action.hashCode) +
(parameters.hashCode);
@override
String toString() => 'EditActionMirror[action=$action, parameters=$parameters]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'action'] = this.action;
json[r'parameters'] = this.parameters;
return json;
}
/// Returns a new [EditActionMirror] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static EditActionMirror? fromJson(dynamic value) {
upgradeDto(value, "EditActionMirror");
if (value is Map) {
final json = value.cast<String, dynamic>();
return EditActionMirror(
action: EditAction.fromJson(json[r'action'])!,
parameters: MirrorParameters.fromJson(json[r'parameters'])!,
);
}
return null;
}
static List<EditActionMirror> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EditActionMirror>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EditActionMirror.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, EditActionMirror> mapFromJson(dynamic json) {
final map = <String, EditActionMirror>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = EditActionMirror.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of EditActionMirror-objects as value to a dart map
static Map<String, List<EditActionMirror>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<EditActionMirror>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = EditActionMirror.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'action',
'parameters',
};
}

View File

@@ -1,107 +0,0 @@
//
// 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 EditActionRotate {
/// Returns a new [EditActionRotate] instance.
EditActionRotate({
required this.action,
required this.parameters,
});
EditAction action;
RotateParameters parameters;
@override
bool operator ==(Object other) => identical(this, other) || other is EditActionRotate &&
other.action == action &&
other.parameters == parameters;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(action.hashCode) +
(parameters.hashCode);
@override
String toString() => 'EditActionRotate[action=$action, parameters=$parameters]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'action'] = this.action;
json[r'parameters'] = this.parameters;
return json;
}
/// Returns a new [EditActionRotate] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static EditActionRotate? fromJson(dynamic value) {
upgradeDto(value, "EditActionRotate");
if (value is Map) {
final json = value.cast<String, dynamic>();
return EditActionRotate(
action: EditAction.fromJson(json[r'action'])!,
parameters: RotateParameters.fromJson(json[r'parameters'])!,
);
}
return null;
}
static List<EditActionRotate> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EditActionRotate>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EditActionRotate.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, EditActionRotate> mapFromJson(dynamic json) {
final map = <String, EditActionRotate>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = EditActionRotate.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of EditActionRotate-objects as value to a dart map
static Map<String, List<EditActionRotate>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<EditActionRotate>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = EditActionRotate.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'action',
'parameters',
};
}

View File

@@ -1,85 +0,0 @@
//
// 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;
/// Axis to mirror along
class MirrorAxis {
/// Instantiate a new enum with the provided [value].
const MirrorAxis._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const horizontal = MirrorAxis._(r'horizontal');
static const vertical = MirrorAxis._(r'vertical');
/// List of all possible values in this [enum][MirrorAxis].
static const values = <MirrorAxis>[
horizontal,
vertical,
];
static MirrorAxis? fromJson(dynamic value) => MirrorAxisTypeTransformer().decode(value);
static List<MirrorAxis> listFromJson(dynamic json, {bool growable = false,}) {
final result = <MirrorAxis>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = MirrorAxis.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [MirrorAxis] to String,
/// and [decode] dynamic data back to [MirrorAxis].
class MirrorAxisTypeTransformer {
factory MirrorAxisTypeTransformer() => _instance ??= const MirrorAxisTypeTransformer._();
const MirrorAxisTypeTransformer._();
String encode(MirrorAxis data) => data.value;
/// Decodes a [dynamic value][data] to a MirrorAxis.
///
/// 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.
MirrorAxis? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'horizontal': return MirrorAxis.horizontal;
case r'vertical': return MirrorAxis.vertical;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [MirrorAxisTypeTransformer] instance.
static MirrorAxisTypeTransformer? _instance;
}

View File

@@ -1,100 +0,0 @@
//
// 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 MirrorParameters {
/// Returns a new [MirrorParameters] instance.
MirrorParameters({
required this.axis,
});
/// Axis to mirror along
MirrorAxis axis;
@override
bool operator ==(Object other) => identical(this, other) || other is MirrorParameters &&
other.axis == axis;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(axis.hashCode);
@override
String toString() => 'MirrorParameters[axis=$axis]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'axis'] = this.axis;
return json;
}
/// Returns a new [MirrorParameters] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static MirrorParameters? fromJson(dynamic value) {
upgradeDto(value, "MirrorParameters");
if (value is Map) {
final json = value.cast<String, dynamic>();
return MirrorParameters(
axis: MirrorAxis.fromJson(json[r'axis'])!,
);
}
return null;
}
static List<MirrorParameters> listFromJson(dynamic json, {bool growable = false,}) {
final result = <MirrorParameters>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = MirrorParameters.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, MirrorParameters> mapFromJson(dynamic json) {
final map = <String, MirrorParameters>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = MirrorParameters.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of MirrorParameters-objects as value to a dart map
static Map<String, List<MirrorParameters>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<MirrorParameters>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = MirrorParameters.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'axis',
};
}

View File

@@ -43,8 +43,6 @@ class Permission {
static const assetPeriodUpload = Permission._(r'asset.upload'); static const assetPeriodUpload = Permission._(r'asset.upload');
static const assetPeriodReplace = Permission._(r'asset.replace'); static const assetPeriodReplace = Permission._(r'asset.replace');
static const assetPeriodCopy = Permission._(r'asset.copy'); static const assetPeriodCopy = Permission._(r'asset.copy');
static const assetPeriodDerive = Permission._(r'asset.derive');
static const assetPeriodEdit = Permission._(r'asset.edit');
static const albumPeriodCreate = Permission._(r'album.create'); static const albumPeriodCreate = Permission._(r'album.create');
static const albumPeriodRead = Permission._(r'album.read'); static const albumPeriodRead = Permission._(r'album.read');
static const albumPeriodUpdate = Permission._(r'album.update'); static const albumPeriodUpdate = Permission._(r'album.update');
@@ -193,8 +191,6 @@ class Permission {
assetPeriodUpload, assetPeriodUpload,
assetPeriodReplace, assetPeriodReplace,
assetPeriodCopy, assetPeriodCopy,
assetPeriodDerive,
assetPeriodEdit,
albumPeriodCreate, albumPeriodCreate,
albumPeriodRead, albumPeriodRead,
albumPeriodUpdate, albumPeriodUpdate,
@@ -378,8 +374,6 @@ class PermissionTypeTransformer {
case r'asset.upload': return Permission.assetPeriodUpload; case r'asset.upload': return Permission.assetPeriodUpload;
case r'asset.replace': return Permission.assetPeriodReplace; case r'asset.replace': return Permission.assetPeriodReplace;
case r'asset.copy': return Permission.assetPeriodCopy; case r'asset.copy': return Permission.assetPeriodCopy;
case r'asset.derive': return Permission.assetPeriodDerive;
case r'asset.edit': return Permission.assetPeriodEdit;
case r'album.create': return Permission.albumPeriodCreate; case r'album.create': return Permission.albumPeriodCreate;
case r'album.read': return Permission.albumPeriodRead; case r'album.read': return Permission.albumPeriodRead;
case r'album.update': return Permission.albumPeriodUpdate; case r'album.update': return Permission.albumPeriodUpdate;

View File

@@ -1,100 +0,0 @@
//
// 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 RotateParameters {
/// Returns a new [RotateParameters] instance.
RotateParameters({
required this.angle,
});
/// Rotation angle in degrees
num angle;
@override
bool operator ==(Object other) => identical(this, other) || other is RotateParameters &&
other.angle == angle;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(angle.hashCode);
@override
String toString() => 'RotateParameters[angle=$angle]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'angle'] = this.angle;
return json;
}
/// Returns a new [RotateParameters] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static RotateParameters? fromJson(dynamic value) {
upgradeDto(value, "RotateParameters");
if (value is Map) {
final json = value.cast<String, dynamic>();
return RotateParameters(
angle: num.parse('${json[r'angle']}'),
);
}
return null;
}
static List<RotateParameters> listFromJson(dynamic json, {bool growable = false,}) {
final result = <RotateParameters>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = RotateParameters.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, RotateParameters> mapFromJson(dynamic json) {
final map = <String, RotateParameters>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = RotateParameters.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of RotateParameters-objects as value to a dart map
static Map<String, List<RotateParameters>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<RotateParameters>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = RotateParameters.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'angle',
};
}

View File

@@ -18,7 +18,6 @@ class SyncAssetV1 {
required this.duration, required this.duration,
required this.fileCreatedAt, required this.fileCreatedAt,
required this.fileModifiedAt, required this.fileModifiedAt,
required this.height,
required this.id, required this.id,
required this.isFavorite, required this.isFavorite,
required this.libraryId, required this.libraryId,
@@ -30,7 +29,6 @@ class SyncAssetV1 {
required this.thumbhash, required this.thumbhash,
required this.type, required this.type,
required this.visibility, required this.visibility,
required this.width,
}); });
String checksum; String checksum;
@@ -43,8 +41,6 @@ class SyncAssetV1 {
DateTime? fileModifiedAt; DateTime? fileModifiedAt;
int? height;
String id; String id;
bool isFavorite; bool isFavorite;
@@ -67,8 +63,6 @@ class SyncAssetV1 {
AssetVisibility visibility; AssetVisibility visibility;
int? width;
@override @override
bool operator ==(Object other) => identical(this, other) || other is SyncAssetV1 && bool operator ==(Object other) => identical(this, other) || other is SyncAssetV1 &&
other.checksum == checksum && other.checksum == checksum &&
@@ -76,7 +70,6 @@ class SyncAssetV1 {
other.duration == duration && other.duration == duration &&
other.fileCreatedAt == fileCreatedAt && other.fileCreatedAt == fileCreatedAt &&
other.fileModifiedAt == fileModifiedAt && other.fileModifiedAt == fileModifiedAt &&
other.height == height &&
other.id == id && other.id == id &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
other.libraryId == libraryId && other.libraryId == libraryId &&
@@ -87,8 +80,7 @@ class SyncAssetV1 {
other.stackId == stackId && other.stackId == stackId &&
other.thumbhash == thumbhash && other.thumbhash == thumbhash &&
other.type == type && other.type == type &&
other.visibility == visibility && other.visibility == visibility;
other.width == width;
@override @override
int get hashCode => int get hashCode =>
@@ -98,7 +90,6 @@ class SyncAssetV1 {
(duration == null ? 0 : duration!.hashCode) + (duration == null ? 0 : duration!.hashCode) +
(fileCreatedAt == null ? 0 : fileCreatedAt!.hashCode) + (fileCreatedAt == null ? 0 : fileCreatedAt!.hashCode) +
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) + (fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
(height == null ? 0 : height!.hashCode) +
(id.hashCode) + (id.hashCode) +
(isFavorite.hashCode) + (isFavorite.hashCode) +
(libraryId == null ? 0 : libraryId!.hashCode) + (libraryId == null ? 0 : libraryId!.hashCode) +
@@ -109,11 +100,10 @@ class SyncAssetV1 {
(stackId == null ? 0 : stackId!.hashCode) + (stackId == null ? 0 : stackId!.hashCode) +
(thumbhash == null ? 0 : thumbhash!.hashCode) + (thumbhash == null ? 0 : thumbhash!.hashCode) +
(type.hashCode) + (type.hashCode) +
(visibility.hashCode) + (visibility.hashCode);
(width == null ? 0 : width!.hashCode);
@override @override
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, height=$height, id=$id, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility, width=$width]'; String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -137,11 +127,6 @@ class SyncAssetV1 {
json[r'fileModifiedAt'] = this.fileModifiedAt!.toUtc().toIso8601String(); json[r'fileModifiedAt'] = this.fileModifiedAt!.toUtc().toIso8601String();
} else { } else {
// json[r'fileModifiedAt'] = null; // json[r'fileModifiedAt'] = null;
}
if (this.height != null) {
json[r'height'] = this.height;
} else {
// json[r'height'] = null;
} }
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'isFavorite'] = this.isFavorite; json[r'isFavorite'] = this.isFavorite;
@@ -174,11 +159,6 @@ class SyncAssetV1 {
} }
json[r'type'] = this.type; json[r'type'] = this.type;
json[r'visibility'] = this.visibility; json[r'visibility'] = this.visibility;
if (this.width != null) {
json[r'width'] = this.width;
} else {
// json[r'width'] = null;
}
return json; return json;
} }
@@ -196,7 +176,6 @@ class SyncAssetV1 {
duration: mapValueOfType<String>(json, r'duration'), duration: mapValueOfType<String>(json, r'duration'),
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r''), fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r''),
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''), fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''),
height: mapValueOfType<int>(json, r'height'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!, isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
libraryId: mapValueOfType<String>(json, r'libraryId'), libraryId: mapValueOfType<String>(json, r'libraryId'),
@@ -208,7 +187,6 @@ class SyncAssetV1 {
thumbhash: mapValueOfType<String>(json, r'thumbhash'), thumbhash: mapValueOfType<String>(json, r'thumbhash'),
type: AssetTypeEnum.fromJson(json[r'type'])!, type: AssetTypeEnum.fromJson(json[r'type'])!,
visibility: AssetVisibility.fromJson(json[r'visibility'])!, visibility: AssetVisibility.fromJson(json[r'visibility'])!,
width: mapValueOfType<int>(json, r'width'),
); );
} }
return null; return null;
@@ -261,7 +239,6 @@ class SyncAssetV1 {
'duration', 'duration',
'fileCreatedAt', 'fileCreatedAt',
'fileModifiedAt', 'fileModifiedAt',
'height',
'id', 'id',
'isFavorite', 'isFavorite',
'libraryId', 'libraryId',
@@ -273,7 +250,6 @@ class SyncAssetV1 {
'thumbhash', 'thumbhash',
'type', 'type',
'visibility', 'visibility',
'width',
}; };
} }

View File

@@ -0,0 +1,3 @@
export 'src/buttons/close_button.dart';
export 'src/buttons/icon_button.dart';
export 'src/types.dart';

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:immich_ui/src/buttons/icon_button.dart';
import 'package:immich_ui/src/types.dart';
class ImmichCloseButton extends StatelessWidget {
final VoidCallback? onTap;
final ImmichVariant variant;
final ImmichColor color;
const ImmichCloseButton({
super.key,
this.onTap,
this.color = ImmichColor.primary,
this.variant = ImmichVariant.ghost,
});
@override
Widget build(BuildContext context) => ImmichIconButton(
key: key,
icon: Icons.close,
color: color,
variant: variant,
onTap: onTap ?? () => Navigator.of(context).pop(),
);
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:immich_ui/src/types.dart';
class ImmichIconButton extends StatelessWidget {
final IconData icon;
final VoidCallback onTap;
final ImmichVariant variant;
final ImmichColor color;
const ImmichIconButton({
super.key,
required this.icon,
required this.onTap,
this.color = ImmichColor.primary,
this.variant = ImmichVariant.filled,
});
@override
Widget build(BuildContext context) {
final background = switch (variant) {
ImmichVariant.filled => switch (color) {
ImmichColor.primary => Theme.of(context).colorScheme.primary,
ImmichColor.secondary => Theme.of(context).colorScheme.secondary,
},
ImmichVariant.ghost => Colors.transparent,
};
final foreground = switch (variant) {
ImmichVariant.filled => switch (color) {
ImmichColor.primary => Theme.of(context).colorScheme.onPrimary,
ImmichColor.secondary => Theme.of(context).colorScheme.onSecondary,
},
ImmichVariant.ghost => switch (color) {
ImmichColor.primary => Theme.of(context).colorScheme.primary,
ImmichColor.secondary => Theme.of(context).colorScheme.secondary,
},
};
return IconButton(
icon: Icon(icon),
onPressed: onTap,
style: IconButton.styleFrom(
backgroundColor: background,
foregroundColor: foreground,
),
);
}
}

View File

@@ -0,0 +1,9 @@
enum ImmichVariant {
filled,
ghost,
}
enum ImmichColor {
primary,
secondary,
}

View File

@@ -0,0 +1,55 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
sdks:
dart: ">=3.8.0-0 <4.0.0"

View File

@@ -0,0 +1,12 @@
name: immich_ui
publish_to: none
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true

View File

@@ -1015,6 +1015,13 @@ packages:
relative: true relative: true
source: path source: path
version: "0.0.0" version: "0.0.0"
immich_ui:
dependency: "direct main"
description:
path: "packages/ui"
relative: true
source: path
version: "0.0.0"
integration_test: integration_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter

View File

@@ -43,6 +43,8 @@ dependencies:
hooks_riverpod: ^2.6.1 hooks_riverpod: ^2.6.1
http: ^1.5.0 http: ^1.5.0
image_picker: ^1.2.0 image_picker: ^1.2.0
immich_ui:
path: './packages/ui'
intl: ^0.20.2 intl: ^0.20.2
isar: isar:
git: git:

View File

@@ -1,185 +0,0 @@
import 'package:drift/drift.dart' as drift;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:openapi/api.dart';
SyncUserV1 _createUser({String id = 'user-1'}) {
return SyncUserV1(
id: id,
name: 'Test User',
email: 'test@test.com',
deletedAt: null,
avatarColor: null,
hasProfileImage: false,
profileChangedAt: DateTime(2024, 1, 1),
);
}
SyncAssetV1 _createAsset({
required String id,
required String checksum,
required String fileName,
String ownerId = 'user-1',
int? width,
int? height,
}) {
return SyncAssetV1(
id: id,
checksum: checksum,
originalFileName: fileName,
type: AssetTypeEnum.IMAGE,
ownerId: ownerId,
isFavorite: false,
fileCreatedAt: DateTime(2024, 1, 1),
fileModifiedAt: DateTime(2024, 1, 1),
localDateTime: DateTime(2024, 1, 1),
visibility: AssetVisibility.timeline,
width: width,
height: height,
deletedAt: null,
duration: null,
libraryId: null,
livePhotoVideoId: null,
stackId: null,
thumbhash: null,
);
}
SyncAssetExifV1 _createExif({
required String assetId,
required int width,
required int height,
required String orientation,
}) {
return SyncAssetExifV1(
assetId: assetId,
exifImageWidth: width,
exifImageHeight: height,
orientation: orientation,
city: null,
country: null,
dateTimeOriginal: null,
description: null,
exposureTime: null,
fNumber: null,
fileSizeInByte: null,
focalLength: null,
fps: null,
iso: null,
latitude: null,
lensModel: null,
longitude: null,
make: null,
model: null,
modifyDate: null,
profileDescription: null,
projectionType: null,
rating: null,
state: null,
timeZone: null,
);
}
void main() {
late Drift db;
late SyncStreamRepository sut;
setUp(() async {
db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
sut = SyncStreamRepository(db);
});
tearDown(() async {
await db.close();
});
group('SyncStreamRepository - Dimension swapping based on orientation', () {
test('swaps dimensions for asset with rotated orientation', () async {
final flippedOrientations = ['5', '6', '7', '8', '90', '-90'];
for (final orientation in flippedOrientations) {
final assetId = 'asset-$orientation-degrees';
await sut.updateUsersV1([_createUser()]);
final asset = _createAsset(
id: assetId,
checksum: 'checksum-$orientation',
fileName: 'rotated_$orientation.jpg',
);
await sut.updateAssetsV1([asset]);
final exif = _createExif(
assetId: assetId,
width: 1920,
height: 1080,
orientation: orientation, // EXIF orientation value for 90 degrees CW
);
await sut.updateAssetsExifV1([exif]);
final query = db.remoteAssetEntity.select()..where((tbl) => tbl.id.equals(assetId));
final result = await query.getSingle();
expect(result.width, equals(1080));
expect(result.height, equals(1920));
}
});
test('does not swap dimensions for asset with normal orientation', () async {
final nonFlippedOrientations = ['1', '2', '3', '4'];
for (final orientation in nonFlippedOrientations) {
final assetId = 'asset-$orientation-degrees';
await sut.updateUsersV1([_createUser()]);
final asset = _createAsset(id: assetId, checksum: 'checksum-$orientation', fileName: 'normal_$orientation.jpg');
await sut.updateAssetsV1([asset]);
final exif = _createExif(
assetId: assetId,
width: 1920,
height: 1080,
orientation: orientation, // EXIF orientation value for normal
);
await sut.updateAssetsExifV1([exif]);
final query = db.remoteAssetEntity.select()..where((tbl) => tbl.id.equals(assetId));
final result = await query.getSingle();
expect(result.width, equals(1920));
expect(result.height, equals(1080));
}
});
test('does not update dimensions if asset already has width and height', () async {
const assetId = 'asset-with-dimensions';
const existingWidth = 1920;
const existingHeight = 1080;
const exifWidth = 3840;
const exifHeight = 2160;
await sut.updateUsersV1([_createUser()]);
final asset = _createAsset(
id: assetId,
checksum: 'checksum-with-dims',
fileName: 'with_dimensions.jpg',
width: existingWidth,
height: existingHeight,
);
await sut.updateAssetsV1([asset]);
final exif = _createExif(assetId: assetId, width: exifWidth, height: exifHeight, orientation: '6');
await sut.updateAssetsExifV1([exif]);
// Verify the asset still has original dimensions (not updated from EXIF)
final query = db.remoteAssetEntity.select()..where((tbl) => tbl.id.equals(assetId));
final result = await query.getSingle();
expect(result.width, equals(existingWidth), reason: 'Width should remain as originally set');
expect(result.height, equals(existingHeight), reason: 'Height should remain as originally set');
});
});
}

View File

@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/domain/services/asset.service.dart'; import 'package:immich_mobile/domain/services/asset.service.dart';
@@ -21,6 +22,42 @@ void main() {
}); });
group('getAspectRatio', () { group('getAspectRatio', () {
test('flips dimensions on Android for 90° and 270° orientations', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
for (final orientation in [90, 270]) {
final localAsset = TestUtils.createLocalAsset(
id: 'local-$orientation',
width: 1920,
height: 1080,
orientation: orientation,
);
final result = await sut.getAspectRatio(localAsset);
expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip on Android');
}
});
test('does not flip dimensions on iOS regardless of orientation', () async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
addTearDown(() => debugDefaultTargetPlatformOverride = null);
for (final orientation in [0, 90, 270]) {
final localAsset = TestUtils.createLocalAsset(
id: 'local-$orientation',
width: 1920,
height: 1080,
orientation: orientation,
);
final result = await sut.getAspectRatio(localAsset);
expect(result, 1920 / 1080, reason: 'iOS should never flip dimensions');
}
});
test('fetches dimensions from remote repository when missing from asset', () async { test('fetches dimensions from remote repository when missing from asset', () async {
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null); final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null);
@@ -75,23 +112,54 @@ void main() {
expect(result, 1.0); expect(result, 1.0);
}); });
test('handles local asset with remoteId and uses remote dimensions', () async { test('handles local asset with remoteId and uses exif from remote', () async {
final localAsset = TestUtils.createLocalAsset( final localAsset = TestUtils.createLocalAsset(
id: 'local-1', id: 'local-1',
remoteId: 'remote-1', remoteId: 'remote-1',
width: null, width: 1920,
height: null, height: 1080,
orientation: 0, orientation: 0,
); );
when( final exif = const ExifInfo(orientation: '6');
() => mockRemoteAssetRepository.get('remote-1'),
).thenAnswer((_) async => TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 1080)); when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif);
final result = await sut.getAspectRatio(localAsset); final result = await sut.getAspectRatio(localAsset);
verify(() => mockRemoteAssetRepository.get('remote-1')).called(1);
expect(result, 1920 / 1080); expect(result, 1080 / 1920);
});
test('handles various flipped EXIF orientations correctly', () async {
final flippedOrientations = ['5', '6', '7', '8', '90', '-90'];
for (final orientation in flippedOrientations) {
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080);
final exif = ExifInfo(orientation: orientation);
when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif);
final result = await sut.getAspectRatio(remoteAsset);
expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip dimensions');
}
});
test('handles various non-flipped EXIF orientations correctly', () async {
final nonFlippedOrientations = ['1', '2', '3', '4'];
for (final orientation in nonFlippedOrientations) {
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080);
final exif = ExifInfo(orientation: orientation);
when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif);
final result = await sut.getAspectRatio(remoteAsset);
expect(result, 1920 / 1080, reason: 'Orientation $orientation should NOT flip dimensions');
}
}); });
}); });
} }

View File

@@ -94,11 +94,25 @@ abstract final class SyncStreamStub {
required String ack, required String ack,
DateTime? trashedAt, DateTime? trashedAt,
}) { }) {
return _assetV1(id: id, checksum: checksum, deletedAt: trashedAt ?? DateTime(2025, 1, 1), ack: ack); return _assetV1(
id: id,
checksum: checksum,
deletedAt: trashedAt ?? DateTime(2025, 1, 1),
ack: ack,
);
} }
static SyncEvent assetModified({required String id, required String checksum, required String ack}) { static SyncEvent assetModified({
return _assetV1(id: id, checksum: checksum, deletedAt: null, ack: ack); required String id,
required String checksum,
required String ack,
}) {
return _assetV1(
id: id,
checksum: checksum,
deletedAt: null,
ack: ack,
);
} }
static SyncEvent _assetV1({ static SyncEvent _assetV1({
@@ -126,8 +140,6 @@ abstract final class SyncStreamStub {
thumbhash: null, thumbhash: null,
type: AssetTypeEnum.IMAGE, type: AssetTypeEnum.IMAGE,
visibility: AssetVisibility.timeline, visibility: AssetVisibility.timeline,
width: null,
height: null,
), ),
ack: ack, ack: ack,
); );

View File

@@ -45,17 +45,5 @@ void main() {
addDefault(value, keys, defaultValue); addDefault(value, keys, defaultValue);
expect(value['alpha']['beta'], 'gamma'); expect(value['alpha']['beta'], 'gamma');
}); });
test('addDefault with null', () {
dynamic value = jsonDecode("""
{
"download": {
"archiveSize": 4294967296,
"includeEmbeddedVideos": false
}
}
""");
expect(value['download']['unknownKey'], isNull);
});
}); });
} }

View File

@@ -3187,173 +3187,6 @@
"x-immich-state": "Stable" "x-immich-state": "Stable"
} }
}, },
"/assets/{id}/edits": {
"delete": {
"description": "Removes all edit actions (crop, rotate, mirror) associated with the specified asset.",
"operationId": "removeAssetEdits",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"204": {
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Remove edits from an existing asset",
"tags": [
"Assets"
],
"x-immich-history": [
{
"version": "v2",
"state": "Added"
},
{
"version": "v2",
"state": "Beta"
}
],
"x-immich-permission": "asset.edit",
"x-immich-state": "Beta"
},
"get": {
"description": "Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset.",
"operationId": "getAssetEdits",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AssetEditsDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Retrieve edits for an existing asset",
"tags": [
"Assets"
],
"x-immich-history": [
{
"version": "v2",
"state": "Added"
},
{
"version": "v2",
"state": "Beta"
}
],
"x-immich-permission": "asset.read",
"x-immich-state": "Beta"
},
"put": {
"description": "Applies a series of edit actions (crop, rotate, mirror) to the specified asset.",
"operationId": "editAsset",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EditActionListDto"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AssetEditsDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"summary": "Applies edits to an existing asset",
"tags": [
"Assets"
],
"x-immich-history": [
{
"version": "v2",
"state": "Added"
},
{
"version": "v2",
"state": "Beta"
}
],
"x-immich-permission": "asset.edit",
"x-immich-state": "Beta"
}
},
"/assets/{id}/metadata": { "/assets/{id}/metadata": {
"get": { "get": {
"description": "Retrieve all metadata key-value pairs associated with the specified asset.", "description": "Retrieve all metadata key-value pairs associated with the specified asset.",
@@ -3683,15 +3516,6 @@
"description": "Downloads the original file of the specified asset.", "description": "Downloads the original file of the specified asset.",
"operationId": "downloadAsset", "operationId": "downloadAsset",
"parameters": [ "parameters": [
{
"name": "edited",
"required": false,
"in": "query",
"schema": {
"default": true,
"type": "boolean"
}
},
{ {
"name": "id", "name": "id",
"required": true, "required": true,
@@ -3852,14 +3676,6 @@
"description": "Retrieve the thumbnail image for the specified asset.", "description": "Retrieve the thumbnail image for the specified asset.",
"operationId": "viewAsset", "operationId": "viewAsset",
"parameters": [ "parameters": [
{
"name": "edited",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{ {
"name": "id", "name": "id",
"required": true, "required": true,
@@ -15290,36 +15106,6 @@
], ],
"type": "object" "type": "object"
}, },
"AssetEditsDto": {
"properties": {
"assetId": {
"format": "uuid",
"type": "string"
},
"edits": {
"description": "list of edits",
"items": {
"anyOf": [
{
"$ref": "#/components/schemas/EditActionCrop"
},
{
"$ref": "#/components/schemas/EditActionRotate"
},
{
"$ref": "#/components/schemas/EditActionMirror"
}
]
},
"type": "array"
}
},
"required": [
"assetId",
"edits"
],
"type": "object"
},
"AssetFaceCreateDto": { "AssetFaceCreateDto": {
"properties": { "properties": {
"assetId": { "assetId": {
@@ -15920,10 +15706,6 @@
"hasMetadata": { "hasMetadata": {
"type": "boolean" "type": "boolean"
}, },
"height": {
"nullable": true,
"type": "number"
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -16044,10 +15826,6 @@
"$ref": "#/components/schemas/AssetVisibility" "$ref": "#/components/schemas/AssetVisibility"
} }
] ]
},
"width": {
"nullable": true,
"type": "number"
} }
}, },
"required": [ "required": [
@@ -16059,7 +15837,6 @@
"fileCreatedAt", "fileCreatedAt",
"fileModifiedAt", "fileModifiedAt",
"hasMetadata", "hasMetadata",
"height",
"id", "id",
"isArchived", "isArchived",
"isFavorite", "isFavorite",
@@ -16072,8 +15849,7 @@
"thumbhash", "thumbhash",
"type", "type",
"updatedAt", "updatedAt",
"visibility", "visibility"
"width"
], ],
"type": "object" "type": "object"
}, },
@@ -16437,37 +16213,6 @@
], ],
"type": "object" "type": "object"
}, },
"CropParameters": {
"properties": {
"height": {
"description": "Height of the crop",
"minimum": 1,
"type": "number"
},
"width": {
"description": "Width of the crop",
"minimum": 1,
"type": "number"
},
"x": {
"description": "Top-Left X coordinate of crop",
"minimum": 0,
"type": "number"
},
"y": {
"description": "Top-Left Y coordinate of crop",
"minimum": 0,
"type": "number"
}
},
"required": [
"height",
"width",
"x",
"y"
],
"type": "object"
},
"DatabaseBackupConfig": { "DatabaseBackupConfig": {
"properties": { "properties": {
"cronExpression": { "cronExpression": {
@@ -16612,96 +16357,6 @@
], ],
"type": "object" "type": "object"
}, },
"EditAction": {
"enum": [
"crop",
"rotate",
"mirror"
],
"type": "string"
},
"EditActionCrop": {
"properties": {
"action": {
"allOf": [
{
"$ref": "#/components/schemas/EditAction"
}
]
},
"parameters": {
"$ref": "#/components/schemas/CropParameters"
}
},
"required": [
"action",
"parameters"
],
"type": "object"
},
"EditActionListDto": {
"properties": {
"edits": {
"description": "list of edits",
"items": {
"anyOf": [
{
"$ref": "#/components/schemas/EditActionCrop"
},
{
"$ref": "#/components/schemas/EditActionRotate"
},
{
"$ref": "#/components/schemas/EditActionMirror"
}
]
},
"type": "array"
}
},
"required": [
"edits"
],
"type": "object"
},
"EditActionMirror": {
"properties": {
"action": {
"allOf": [
{
"$ref": "#/components/schemas/EditAction"
}
]
},
"parameters": {
"$ref": "#/components/schemas/MirrorParameters"
}
},
"required": [
"action",
"parameters"
],
"type": "object"
},
"EditActionRotate": {
"properties": {
"action": {
"allOf": [
{
"$ref": "#/components/schemas/EditAction"
}
]
},
"parameters": {
"$ref": "#/components/schemas/RotateParameters"
}
},
"required": [
"action",
"parameters"
],
"type": "object"
},
"EmailNotificationsResponse": { "EmailNotificationsResponse": {
"properties": { "properties": {
"albumInvite": { "albumInvite": {
@@ -17712,30 +17367,6 @@
}, },
"type": "object" "type": "object"
}, },
"MirrorAxis": {
"description": "Axis to mirror along",
"enum": [
"horizontal",
"vertical"
],
"type": "string"
},
"MirrorParameters": {
"properties": {
"axis": {
"allOf": [
{
"$ref": "#/components/schemas/MirrorAxis"
}
],
"description": "Axis to mirror along"
}
},
"required": [
"axis"
],
"type": "object"
},
"NotificationCreateDto": { "NotificationCreateDto": {
"properties": { "properties": {
"data": { "data": {
@@ -18216,8 +17847,6 @@
"asset.upload", "asset.upload",
"asset.replace", "asset.replace",
"asset.copy", "asset.copy",
"asset.derive",
"asset.edit",
"album.create", "album.create",
"album.read", "album.read",
"album.update", "album.update",
@@ -19274,18 +18903,6 @@
], ],
"type": "object" "type": "object"
}, },
"RotateParameters": {
"properties": {
"angle": {
"description": "Rotation angle in degrees",
"type": "number"
}
},
"required": [
"angle"
],
"type": "object"
},
"SearchAlbumResponseDto": { "SearchAlbumResponseDto": {
"properties": { "properties": {
"count": { "count": {
@@ -21007,10 +20624,6 @@
"nullable": true, "nullable": true,
"type": "string" "type": "string"
}, },
"height": {
"nullable": true,
"type": "integer"
},
"id": { "id": {
"type": "string" "type": "string"
}, },
@@ -21057,10 +20670,6 @@
"$ref": "#/components/schemas/AssetVisibility" "$ref": "#/components/schemas/AssetVisibility"
} }
] ]
},
"width": {
"nullable": true,
"type": "integer"
} }
}, },
"required": [ "required": [
@@ -21069,7 +20678,6 @@
"duration", "duration",
"fileCreatedAt", "fileCreatedAt",
"fileModifiedAt", "fileModifiedAt",
"height",
"id", "id",
"isFavorite", "isFavorite",
"libraryId", "libraryId",
@@ -21080,8 +20688,7 @@
"stackId", "stackId",
"thumbhash", "thumbhash",
"type", "type",
"visibility", "visibility"
"width"
], ],
"type": "object" "type": "object"
}, },

View File

@@ -349,7 +349,6 @@ export type AssetResponseDto = {
/** The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken. */ /** The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken. */
fileModifiedAt: string; fileModifiedAt: string;
hasMetadata: boolean; hasMetadata: boolean;
height: number | null;
id: string; id: string;
isArchived: boolean; isArchived: boolean;
isFavorite: boolean; isFavorite: boolean;
@@ -374,7 +373,6 @@ export type AssetResponseDto = {
/** The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. */ /** The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. */
updatedAt: string; updatedAt: string;
visibility: AssetVisibility; visibility: AssetVisibility;
width: number | null;
}; };
export type ContributorCountResponseDto = { export type ContributorCountResponseDto = {
assetCount: number; assetCount: number;
@@ -555,45 +553,6 @@ export type UpdateAssetDto = {
rating?: number; rating?: number;
visibility?: AssetVisibility; visibility?: AssetVisibility;
}; };
export type CropParameters = {
/** Height of the crop */
height: number;
/** Width of the crop */
width: number;
/** Top-Left X coordinate of crop */
x: number;
/** Top-Left Y coordinate of crop */
y: number;
};
export type EditActionCrop = {
action: EditAction;
parameters: CropParameters;
};
export type RotateParameters = {
/** Rotation angle in degrees */
angle: number;
};
export type EditActionRotate = {
action: EditAction;
parameters: RotateParameters;
};
export type MirrorParameters = {
/** Axis to mirror along */
axis: MirrorAxis;
};
export type EditActionMirror = {
action: EditAction;
parameters: MirrorParameters;
};
export type AssetEditsDto = {
assetId: string;
/** list of edits */
edits: (EditActionCrop | EditActionRotate | EditActionMirror)[];
};
export type EditActionListDto = {
/** list of edits */
edits: (EditActionCrop | EditActionRotate | EditActionMirror)[];
};
export type AssetMetadataResponseDto = { export type AssetMetadataResponseDto = {
key: AssetMetadataKey; key: AssetMetadataKey;
updatedAt: string; updatedAt: string;
@@ -2566,46 +2525,6 @@ export function updateAsset({ id, updateAssetDto }: {
body: updateAssetDto body: updateAssetDto
}))); })));
} }
/**
* Remove edits from an existing asset
*/
export function removeAssetEdits({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchText(`/assets/${encodeURIComponent(id)}/edits`, {
...opts,
method: "DELETE"
}));
}
/**
* Retrieve edits for an existing asset
*/
export function getAssetEdits({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: AssetEditsDto;
}>(`/assets/${encodeURIComponent(id)}/edits`, {
...opts
}));
}
/**
* Applies edits to an existing asset
*/
export function editAsset({ id, editActionListDto }: {
id: string;
editActionListDto: EditActionListDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: AssetEditsDto;
}>(`/assets/${encodeURIComponent(id)}/edits`, oazapfts.json({
...opts,
method: "PUT",
body: editActionListDto
})));
}
/** /**
* Get asset metadata * Get asset metadata
*/ */
@@ -2677,8 +2596,7 @@ export function getAssetOcr({ id }: {
/** /**
* Download original asset * Download original asset
*/ */
export function downloadAsset({ edited, id, key, slug }: { export function downloadAsset({ id, key, slug }: {
edited?: boolean;
id: string; id: string;
key?: string; key?: string;
slug?: string; slug?: string;
@@ -2687,7 +2605,6 @@ export function downloadAsset({ edited, id, key, slug }: {
status: 200; status: 200;
data: Blob; data: Blob;
}>(`/assets/${encodeURIComponent(id)}/original${QS.query(QS.explode({ }>(`/assets/${encodeURIComponent(id)}/original${QS.query(QS.explode({
edited,
key, key,
slug slug
}))}`, { }))}`, {
@@ -2718,8 +2635,7 @@ export function replaceAsset({ id, key, slug, assetMediaReplaceDto }: {
/** /**
* View asset thumbnail * View asset thumbnail
*/ */
export function viewAsset({ edited, id, key, size, slug }: { export function viewAsset({ id, key, size, slug }: {
edited?: boolean;
id: string; id: string;
key?: string; key?: string;
size?: AssetMediaSize; size?: AssetMediaSize;
@@ -2729,7 +2645,6 @@ export function viewAsset({ edited, id, key, size, slug }: {
status: 200; status: 200;
data: Blob; data: Blob;
}>(`/assets/${encodeURIComponent(id)}/thumbnail${QS.query(QS.explode({ }>(`/assets/${encodeURIComponent(id)}/thumbnail${QS.query(QS.explode({
edited,
key, key,
size, size,
slug slug
@@ -5304,8 +5219,6 @@ export enum Permission {
AssetUpload = "asset.upload", AssetUpload = "asset.upload",
AssetReplace = "asset.replace", AssetReplace = "asset.replace",
AssetCopy = "asset.copy", AssetCopy = "asset.copy",
AssetDerive = "asset.derive",
AssetEdit = "asset.edit",
AlbumCreate = "album.create", AlbumCreate = "album.create",
AlbumRead = "album.read", AlbumRead = "album.read",
AlbumUpdate = "album.update", AlbumUpdate = "album.update",
@@ -5454,15 +5367,6 @@ export enum AssetJobName {
RegenerateThumbnail = "regenerate-thumbnail", RegenerateThumbnail = "regenerate-thumbnail",
TranscodeVideo = "transcode-video" TranscodeVideo = "transcode-video"
} }
export enum EditAction {
Crop = "crop",
Rotate = "rotate",
Mirror = "mirror"
}
export enum MirrorAxis {
Horizontal = "horizontal",
Vertical = "vertical"
}
export enum AssetMediaSize { export enum AssetMediaSize {
Fullsize = "fullsize", Fullsize = "fullsize",
Preview = "preview", Preview = "preview",

View File

@@ -15,9 +15,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz",
"integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -32,9 +32,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz",
"integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -49,9 +49,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz",
"integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -66,9 +66,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz",
"integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -83,9 +83,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz",
"integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -100,9 +100,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz",
"integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -117,9 +117,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz",
"integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -134,9 +134,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz",
"integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -151,9 +151,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz",
"integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -168,9 +168,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz",
"integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -185,9 +185,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz",
"integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -202,9 +202,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz",
"integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -219,9 +219,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz",
"integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -236,9 +236,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz",
"integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -253,9 +253,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz",
"integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -270,9 +270,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz",
"integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -287,9 +287,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz",
"integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -304,9 +304,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz",
"integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -321,9 +321,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz",
"integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -338,9 +338,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz",
"integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -355,9 +355,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz",
"integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -372,9 +372,9 @@
} }
}, },
"node_modules/@esbuild/openharmony-arm64": { "node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz",
"integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -389,9 +389,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz",
"integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -406,9 +406,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz",
"integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -423,9 +423,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz",
"integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -440,9 +440,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz",
"integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -467,9 +467,9 @@
} }
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.27.0", "version": "0.27.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz",
"integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@@ -480,32 +480,32 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.0", "@esbuild/aix-ppc64": "0.27.1",
"@esbuild/android-arm": "0.27.0", "@esbuild/android-arm": "0.27.1",
"@esbuild/android-arm64": "0.27.0", "@esbuild/android-arm64": "0.27.1",
"@esbuild/android-x64": "0.27.0", "@esbuild/android-x64": "0.27.1",
"@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-arm64": "0.27.1",
"@esbuild/darwin-x64": "0.27.0", "@esbuild/darwin-x64": "0.27.1",
"@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.1",
"@esbuild/freebsd-x64": "0.27.0", "@esbuild/freebsd-x64": "0.27.1",
"@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm": "0.27.1",
"@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-arm64": "0.27.1",
"@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-ia32": "0.27.1",
"@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-loong64": "0.27.1",
"@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-mips64el": "0.27.1",
"@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-ppc64": "0.27.1",
"@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-riscv64": "0.27.1",
"@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-s390x": "0.27.1",
"@esbuild/linux-x64": "0.27.0", "@esbuild/linux-x64": "0.27.1",
"@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.1",
"@esbuild/netbsd-x64": "0.27.0", "@esbuild/netbsd-x64": "0.27.1",
"@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.1",
"@esbuild/openbsd-x64": "0.27.0", "@esbuild/openbsd-x64": "0.27.1",
"@esbuild/openharmony-arm64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.1",
"@esbuild/sunos-x64": "0.27.0", "@esbuild/sunos-x64": "0.27.1",
"@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-arm64": "0.27.1",
"@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-ia32": "0.27.1",
"@esbuild/win32-x64": "0.27.0" "@esbuild/win32-x64": "0.27.1"
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {

1351
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -50,13 +50,15 @@ RUN --mount=type=cache,id=pnpm-cli,target=/buildcache/pnpm-store \
FROM builder AS plugins FROM builder AS plugins
ARG TARGETPLATFORM
COPY --from=ghcr.io/jdx/mise:2025.11.3@sha256:ac26f5978c0e2783f3e68e58ce75eddb83e41b89bf8747c503bac2aa9baf22c5 /usr/local/bin/mise /usr/local/bin/mise COPY --from=ghcr.io/jdx/mise:2025.11.3@sha256:ac26f5978c0e2783f3e68e58ce75eddb83e41b89bf8747c503bac2aa9baf22c5 /usr/local/bin/mise /usr/local/bin/mise
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY ./plugins/mise.toml ./plugins/ COPY ./plugins/mise.toml ./plugins/
ENV MISE_TRUSTED_CONFIG_PATHS=/usr/src/app/plugins/mise.toml ENV MISE_TRUSTED_CONFIG_PATHS=/usr/src/app/plugins/mise.toml
ENV MISE_DATA_DIR=/buildcache/mise ENV MISE_DATA_DIR=/buildcache/mise
RUN --mount=type=cache,id=mise-tools,target=/buildcache/mise \ RUN --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
mise install --cd plugins mise install --cd plugins
COPY ./plugins ./plugins/ COPY ./plugins ./plugins/
@@ -66,7 +68,7 @@ RUN --mount=type=cache,id=pnpm-plugins,target=/buildcache/pnpm-store \
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \ --mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \ --mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
--mount=type=cache,id=mise-tools,target=/buildcache/mise \ --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
cd plugins && mise run build cd plugins && mise run build
FROM ghcr.io/immich-app/base-server-prod:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29 FROM ghcr.io/immich-app/base-server-prod:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29

View File

@@ -70,7 +70,7 @@
"cookie": "^1.0.2", "cookie": "^1.0.2",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cron": "4.3.3", "cron": "4.3.3",
"exiftool-vendored": "^33.0.0", "exiftool-vendored": "^34.0.0",
"express": "^5.1.0", "express": "^5.1.0",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
@@ -110,7 +110,6 @@
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"tailwindcss-preset-email": "^1.4.0", "tailwindcss-preset-email": "^1.4.0",
"thumbhash": "^0.1.1", "thumbhash": "^0.1.1",
"transformation-matrix": "^3.1.0",
"ua-parser-js": "^2.0.0", "ua-parser-js": "^2.0.0",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"validator": "^13.12.0" "validator": "^13.12.0"
@@ -129,8 +128,8 @@
"@types/cookie-parser": "^1.4.8", "@types/cookie-parser": "^1.4.8",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/fluent-ffmpeg": "^2.1.21", "@types/fluent-ffmpeg": "^2.1.21",
"@types/js-yaml": "^4.0.9",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.14.197", "@types/lodash": "^4.14.197",
"@types/luxon": "^3.6.2", "@types/luxon": "^3.6.2",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
@@ -154,7 +153,7 @@
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"node-gyp": "^12.0.0", "node-gyp": "^12.0.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"prettier": "^3.0.2", "prettier": "^3.7.4",
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
"sql-formatter": "^15.0.0", "sql-formatter": "^15.0.0",
"supertest": "^7.1.0", "supertest": "^7.1.0",

View File

@@ -33,7 +33,6 @@ import {
CheckExistingAssetsDto, CheckExistingAssetsDto,
UploadFieldName, UploadFieldName,
} from 'src/dtos/asset-media.dto'; } from 'src/dtos/asset-media.dto';
import { AssetDownloadOriginalDto } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { ApiTag, ImmichHeader, Permission, RouteKey } from 'src/enum'; import { ApiTag, ImmichHeader, Permission, RouteKey } from 'src/enum';
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
@@ -95,11 +94,10 @@ export class AssetMediaController {
async downloadAsset( async downloadAsset(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@Query() { edited }: AssetDownloadOriginalDto,
@Res() res: Response, @Res() res: Response,
@Next() next: NextFunction, @Next() next: NextFunction,
) { ) {
await sendFile(res, next, () => this.service.downloadOriginal(auth, id, edited ?? true), this.logger); await sendFile(res, next, () => this.service.downloadOriginal(auth, id), this.logger);
} }
@Put(':id/original') @Put(':id/original')

View File

@@ -17,7 +17,6 @@ import {
UpdateAssetDto, UpdateAssetDto,
} from 'src/dtos/asset.dto'; } from 'src/dtos/asset.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { AssetEditsDto, EditActionListDto } from 'src/dtos/editing.dto';
import { AssetOcrResponseDto } from 'src/dtos/ocr.dto'; import { AssetOcrResponseDto } from 'src/dtos/ocr.dto';
import { ApiTag, Permission, RouteKey } from 'src/enum'; import { ApiTag, Permission, RouteKey } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
@@ -198,42 +197,4 @@ export class AssetController {
deleteAssetMetadata(@Auth() auth: AuthDto, @Param() { id, key }: AssetMetadataRouteParams): Promise<void> { deleteAssetMetadata(@Auth() auth: AuthDto, @Param() { id, key }: AssetMetadataRouteParams): Promise<void> {
return this.service.deleteMetadataByKey(auth, id, key); return this.service.deleteMetadataByKey(auth, id, key);
} }
@Put(':id/edits')
@Authenticated({ permission: Permission.AssetEdit })
@Endpoint({
summary: 'Applies edits to an existing asset',
description: 'Applies a series of edit actions (crop, rotate, mirror) to the specified asset.',
history: new HistoryBuilder().added('v2').beta('v2'),
})
editAsset(
@Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto,
@Body() dto: EditActionListDto,
): Promise<AssetEditsDto> {
return this.service.editAsset(auth, id, dto);
}
@Get(':id/edits')
@Authenticated({ permission: Permission.AssetRead })
@Endpoint({
summary: 'Retrieve edits for an existing asset',
description: 'Retrieve a series of edit actions (crop, rotate, mirror) associated with the specified asset.',
history: new HistoryBuilder().added('v2').beta('v2'),
})
getAssetEdits(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<AssetEditsDto> {
return this.service.getAssetEdits(auth, id);
}
@Delete(':id/edits')
@Authenticated({ permission: Permission.AssetEdit })
@HttpCode(HttpStatus.NO_CONTENT)
@Endpoint({
summary: 'Remove edits from an existing asset',
description: 'Removes all edit actions (crop, rotate, mirror) associated with the specified asset.',
history: new HistoryBuilder().added('v2').beta('v2'),
})
removeAssetEdits(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.removeAssetEdits(auth, id);
}
} }

View File

@@ -24,13 +24,7 @@ export interface MoveRequest {
}; };
} }
export type GeneratedImageType = export type GeneratedImageType = AssetPathType.Preview | AssetPathType.Thumbnail | AssetPathType.FullSize;
| AssetPathType.Preview
| AssetPathType.Thumbnail
| AssetPathType.FullSize
| AssetPathType.EditedPreview
| AssetPathType.EditedThumbnail
| AssetPathType.EditedFullSize;
export type GeneratedAssetType = GeneratedImageType | AssetPathType.EncodedVideo; export type GeneratedAssetType = GeneratedImageType | AssetPathType.EncodedVideo;
export type ThumbnailPathEntity = { id: string; ownerId: string }; export type ThumbnailPathEntity = { id: string; ownerId: string };

View File

@@ -272,7 +272,6 @@ export type AssetFace = {
person?: Person | null; person?: Person | null;
updatedAt: Date; updatedAt: Date;
updateId: string; updateId: string;
isVisible: boolean;
}; };
export type Plugin = Selectable<PluginTable>; export type Plugin = Selectable<PluginTable>;
@@ -341,8 +340,6 @@ export const columns = {
'asset.originalPath', 'asset.originalPath',
'asset.ownerId', 'asset.ownerId',
'asset.type', 'asset.type',
'asset.width',
'asset.height',
], ],
assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'], assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'],
authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'], authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'],
@@ -393,8 +390,6 @@ export const columns = {
'asset.livePhotoVideoId', 'asset.livePhotoVideoId',
'asset.stackId', 'asset.stackId',
'asset.libraryId', 'asset.libraryId',
'asset.width',
'asset.height',
], ],
syncAlbumUser: ['album_user.albumId as albumId', 'album_user.userId as userId', 'album_user.role'], syncAlbumUser: ['album_user.albumId as albumId', 'album_user.userId as userId', 'album_user.role'],
syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'], syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'],

View File

@@ -19,9 +19,6 @@ export enum AssetMediaSize {
export class AssetMediaOptionsDto { export class AssetMediaOptionsDto {
@ValidateEnum({ enum: AssetMediaSize, name: 'AssetMediaSize', optional: true }) @ValidateEnum({ enum: AssetMediaSize, name: 'AssetMediaSize', optional: true })
size?: AssetMediaSize; size?: AssetMediaSize;
@ValidateBoolean({ optional: true })
edited?: boolean;
} }
export enum UploadFieldName { export enum UploadFieldName {

View File

@@ -3,7 +3,6 @@ import { Selectable } from 'kysely';
import { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database'; import { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database';
import { HistoryBuilder, Property } from 'src/decorators'; import { HistoryBuilder, Property } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { EditActionItem } from 'src/dtos/editing.dto';
import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto'; import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto';
import { import {
AssetFaceWithoutPersonResponseDto, AssetFaceWithoutPersonResponseDto,
@@ -14,8 +13,6 @@ import {
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto'; import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { ImageDimensions } from 'src/types';
import { getDimensions } from 'src/utils/asset.util';
import { hexOrBufferToBase64 } from 'src/utils/bytes'; import { hexOrBufferToBase64 } from 'src/utils/bytes';
import { mimeTypes } from 'src/utils/mime-types'; import { mimeTypes } from 'src/utils/mime-types';
import { ValidateEnum } from 'src/validation'; import { ValidateEnum } from 'src/validation';
@@ -37,8 +34,6 @@ export class SanitizedAssetResponseDto {
duration!: string; duration!: string;
livePhotoVideoId?: string | null; livePhotoVideoId?: string | null;
hasMetadata!: boolean; hasMetadata!: boolean;
width!: number | null;
height!: number | null;
} }
export class AssetResponseDto extends SanitizedAssetResponseDto { export class AssetResponseDto extends SanitizedAssetResponseDto {
@@ -112,7 +107,6 @@ export type MapAsset = {
deviceId: string; deviceId: string;
duplicateId: string | null; duplicateId: string | null;
duration: string | null; duration: string | null;
edits?: EditActionItem[];
encodedVideoPath: string | null; encodedVideoPath: string | null;
exifInfo?: Selectable<Exif> | null; exifInfo?: Selectable<Exif> | null;
faces?: AssetFace[]; faces?: AssetFace[];
@@ -135,8 +129,6 @@ export type MapAsset = {
tags?: Tag[]; tags?: Tag[];
thumbhash: Buffer<ArrayBufferLike> | null; thumbhash: Buffer<ArrayBufferLike> | null;
type: AssetType; type: AssetType;
width: number | null;
height: number | null;
}; };
export class AssetStackResponseDto { export class AssetStackResponseDto {
@@ -155,20 +147,16 @@ export type AssetMapOptions = {
}; };
// TODO: this is inefficient // TODO: this is inefficient
const peopleWithFaces = ( const peopleWithFaces = (faces?: AssetFace[]): PersonWithFacesResponseDto[] => {
faces?: AssetFace[],
edits?: EditActionItem[],
assetDimensions?: ImageDimensions,
): PersonWithFacesResponseDto[] => {
const result: PersonWithFacesResponseDto[] = []; const result: PersonWithFacesResponseDto[] = [];
if (faces && edits && assetDimensions) { if (faces) {
for (const face of faces) { for (const face of faces) {
if (face.person) { if (face.person) {
const existingPersonEntry = result.find((item) => item.id === face.person!.id); const existingPersonEntry = result.find((item) => item.id === face.person!.id);
if (existingPersonEntry) { if (existingPersonEntry) {
existingPersonEntry.faces.push(face); existingPersonEntry.faces.push(face);
} else { } else {
result.push({ ...mapPerson(face.person!), faces: [mapFacesWithoutPerson(face, edits, assetDimensions)] }); result.push({ ...mapPerson(face.person!), faces: [mapFacesWithoutPerson(face)] });
} }
} }
} }
@@ -202,14 +190,10 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
duration: entity.duration ?? '0:00:00.00000', duration: entity.duration ?? '0:00:00.00000',
livePhotoVideoId: entity.livePhotoVideoId, livePhotoVideoId: entity.livePhotoVideoId,
hasMetadata: false, hasMetadata: false,
width: entity.width,
height: entity.height,
}; };
return sanitizedAssetResponse as AssetResponseDto; return sanitizedAssetResponse as AssetResponseDto;
} }
const assetDimensions = entity.exifInfo ? getDimensions(entity.exifInfo) : undefined;
return { return {
id: entity.id, id: entity.id,
createdAt: entity.createdAt, createdAt: entity.createdAt,
@@ -235,7 +219,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined, exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
livePhotoVideoId: entity.livePhotoVideoId, livePhotoVideoId: entity.livePhotoVideoId,
tags: entity.tags?.map((tag) => mapTag(tag)), tags: entity.tags?.map((tag) => mapTag(tag)),
people: peopleWithFaces(entity.faces, entity.edits, assetDimensions), people: peopleWithFaces(entity.faces),
unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)), unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)),
checksum: hexOrBufferToBase64(entity.checksum)!, checksum: hexOrBufferToBase64(entity.checksum)!,
stack: withStack ? mapStack(entity) : undefined, stack: withStack ? mapStack(entity) : undefined,
@@ -243,7 +227,5 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
hasMetadata: true, hasMetadata: true,
duplicateId: entity.duplicateId, duplicateId: entity.duplicateId,
resized: true, resized: true,
width: entity.width,
height: entity.height,
}; };
} }

View File

@@ -197,11 +197,6 @@ export class AssetCopyDto {
favorite?: boolean; favorite?: boolean;
} }
export class AssetDownloadOriginalDto {
@ValidateBoolean({ optional: true, default: true })
edited?: boolean;
}
export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { export const mapStats = (stats: AssetStats): AssetStatsResponseDto => {
return { return {
images: stats[AssetType.Image], images: stats[AssetType.Image],

View File

@@ -1,122 +0,0 @@
import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger';
import { ClassConstructor, plainToInstance, Transform, Type } from 'class-transformer';
import { IsEnum, IsInt, Min, ValidateNested } from 'class-validator';
import { IsAxisAlignedRotation, ValidateUUID } from 'src/validation';
export enum EditAction {
Crop = 'crop',
Rotate = 'rotate',
Mirror = 'mirror',
}
export enum MirrorAxis {
Horizontal = 'horizontal',
Vertical = 'vertical',
}
export class CropParameters {
@IsInt()
@Min(0)
@ApiProperty({ description: 'Top-Left X coordinate of crop' })
x!: number;
@IsInt()
@Min(0)
@ApiProperty({ description: 'Top-Left Y coordinate of crop' })
y!: number;
@IsInt()
@Min(1)
@ApiProperty({ description: 'Width of the crop' })
width!: number;
@IsInt()
@Min(1)
@ApiProperty({ description: 'Height of the crop' })
height!: number;
}
export class RotateParameters {
@IsAxisAlignedRotation()
@ApiProperty({ description: 'Rotation angle in degrees' })
angle!: number;
}
export class MirrorParameters {
@IsEnum(MirrorAxis)
@ApiProperty({ enum: MirrorAxis, enumName: 'MirrorAxis', description: 'Axis to mirror along' })
axis!: MirrorAxis;
}
class EditActionBase {
@IsEnum(EditAction)
@ApiProperty({ enum: EditAction, enumName: 'EditAction' })
action!: EditAction;
}
export class EditActionCrop extends EditActionBase {
@ValidateNested()
@Type(() => CropParameters)
@ApiProperty({ type: CropParameters })
parameters!: CropParameters;
}
export class EditActionRotate extends EditActionBase {
@ValidateNested()
@Type(() => RotateParameters)
@ApiProperty({ type: RotateParameters })
parameters!: RotateParameters;
}
export class EditActionMirror extends EditActionBase {
@ValidateNested()
@Type(() => MirrorParameters)
@ApiProperty({ type: MirrorParameters })
parameters!: MirrorParameters;
}
export type EditActionItem =
| {
action: EditAction.Crop;
parameters: CropParameters;
}
| {
action: EditAction.Rotate;
parameters: RotateParameters;
}
| {
action: EditAction.Mirror;
parameters: MirrorParameters;
};
export type EditActionParameter = {
[EditAction.Crop]: CropParameters;
[EditAction.Rotate]: RotateParameters;
[EditAction.Mirror]: MirrorParameters;
};
type EditActions = EditActionCrop | EditActionRotate | EditActionMirror;
const actionToClass: Record<EditAction, ClassConstructor<EditActions>> = {
[EditAction.Crop]: EditActionCrop,
[EditAction.Rotate]: EditActionRotate,
[EditAction.Mirror]: EditActionMirror,
} as const;
const getActionClass = (item: { action: EditAction }): ClassConstructor<EditActions> => actionToClass[item.action];
@ApiExtraModels(EditActionRotate, EditActionMirror, EditActionCrop)
export class EditActionListDto {
/** list of edits */
@ValidateNested({ each: true })
@Transform(({ value: edits }) =>
Array.isArray(edits) ? edits.map((item) => plainToInstance(getActionClass(item), item)) : edits,
)
@ApiProperty({ anyOf: Object.values(actionToClass).map((target) => ({ $ref: getSchemaPath(target) })) })
edits!: EditActionItem[];
}
export class AssetEditsDto extends EditActionListDto {
@ValidateUUID()
@ApiProperty()
assetId!: string;
}

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