Compare commits

..

113 Commits

Author SHA1 Message Date
Alex Tran
25848b78f9 Up version for release 2022-10-26 11:24:04 -05:00
Alex
f94176a910 feat(web) dark mode (#867) 2022-10-26 11:10:48 -05:00
Alex Tran
ae96508e15 Fixed unit test 2022-10-25 22:15:17 -05:00
Alex
95ebf815eb feat(web) styling server stats page (#866) 2022-10-25 21:41:46 -05:00
Jonas Janz
b713fb5650 feat(docker) revert ubuntu base image (#863)
* feat(docker) revert ubuntu base image

This PR reverts the base image for immich-server back to alpine

Adds LICENSE to all Images
Quiets apt-get commands when building
ensures write-permission for root group on app folders

Signed-off-by: PixelJonas <5434875+PixelJonas@users.noreply.github.com>

* Test build old Docker content

* Revert and retry

Signed-off-by: PixelJonas <5434875+PixelJonas@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-10-25 14:18:37 -05:00
Alex
6159c83fd2 feat(mobile) duplicated asset upload handling mechanism (#853) 2022-10-25 09:51:03 -05:00
Jonas Janz
f1af17bf4d feat(immich-server) use ubuntu base-image (#851)
this changes the base-image for immich-server from
`node:16-alpine3.14`
to
`node:16-slim`

There is an open issue with alpine DNS resolving which
breaks immich-microservice when deployed on
kubernetes.

This fixes https://github.com/immich-app/immich-charts/issues/4

Signed-off-by: PixelJonas <5434875+PixelJonas@users.noreply.github.com>

Signed-off-by: PixelJonas <5434875+PixelJonas@users.noreply.github.com>
2022-10-24 14:59:07 -05:00
Alex
a87c1c1210 fix(mobile) not possible to sign out when option is enable (#860) 2022-10-24 14:45:58 -05:00
Alex
e63d165b65 chore(server) add workflow dispatcher to sdk repository (#859) 2022-10-24 12:55:16 -05:00
Alex Tran
9411770253 update readme 2022-10-23 22:21:48 -05:00
Alex Tran
dc80ac1c88 Remove openapi generator - move to TeamCity CI/CD for this job 2022-10-23 18:19:21 -05:00
Alex Tran
bb055628cc Fixed api generation action 2022-10-23 17:56:16 -05:00
Alex Tran
390bcdb8c6 Fixed api generation action 2022-10-23 17:53:11 -05:00
Alex Tran
d95bcb46ad Fixed api generation action 2022-10-23 17:52:52 -05:00
Alex
7b954e21e7 fix(server): add permission for server stats api (#854) 2022-10-23 17:01:41 -05:00
Zeeshan Khan
a6eea4d096 feat(web) add asset count stats on admin page (#843) 2022-10-23 16:54:54 -05:00
Alex
2c189d5c78 fix(server): force best effort to decode thumbnail image (#847) 2022-10-22 11:40:25 -05:00
Alex
85a80fd032 Added changlog 2022-10-21 13:19:04 -05:00
Zeeshan Khan
0309b47515 fixes(mobile) back navigation issue on android (#841) 2022-10-21 13:05:44 -05:00
bo0tzz
95d8f60389 feat(server)Log username and IP address on failed login attempt 2022-10-21 11:04:01 -05:00
Alex Tran
1ec7122381 Up version for release 2022-10-19 20:07:53 -05:00
Alex
061b229e12 feat(mobile): Cache assets and albums for faster loading speed
feat(mobile): Cache assets and albums for faster loading speed
2022-10-19 15:53:15 -05:00
Matthias Rupp
3617433858 Refactor abstract class to separate file 2022-10-19 22:03:54 +02:00
Alex
d6d525cc1b fix(mobile) back button navigation Android
fixes #310 back button navigation
2022-10-19 14:51:48 -05:00
Alex
e752290458 Merge pull request #839 from immich-app/dependabot/github_actions/docker/setup-buildx-action-2.2.1
chore(deps): bump docker/setup-buildx-action from 2.1.0 to 2.2.1
2022-10-18 09:27:47 -05:00
Matthias Rupp
d77e25425e Add cache for shared albums 2022-10-18 14:06:35 +02:00
dependabot[bot]
028c0249a3 chore(deps): bump docker/setup-buildx-action from 2.1.0 to 2.2.1
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.1.0 to 2.2.1.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.1.0...v2.2.1)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-18 10:45:12 +00:00
Zeeshan Khan
a3ca5307a5 fixes #310 back button navigation 2022-10-17 13:04:17 -05:00
Matthias Rupp
6796462b13 Switch to plain fs based caching mechanism 2022-10-17 18:02:43 +02:00
Matthias Rupp
d08475d5af Switch to lazyBox 2022-10-17 16:40:51 +02:00
Matthias Rupp
d310c77fc8 Add album list response caching 2022-10-17 14:53:27 +02:00
Matthias Rupp
75d8ca1306 Invalidation on logout and timing measurements 2022-10-16 09:50:31 +02:00
Matthias Rupp
894eea739e JSON based caching 2022-10-15 23:20:15 +02:00
Matthias Rupp
1156290377 Add asset response cache 2022-10-14 23:57:55 +02:00
Alex Tran
c271f0c224 Up version for release 2022-10-14 16:21:26 -05:00
Alex
a7f14dc103 feat(mobile): cosmetic update 2022-10-14 16:17:14 -05:00
Alex Tran
f05d5bdb9e Added haptic feedback to incorect login 2022-10-14 16:13:35 -05:00
Alex Tran
e99c400f59 Added haptic feedback to tab bar switching 2022-10-14 16:04:21 -05:00
Alex Tran
e38166837d Merge branch 'main' of github.com:immich-app/immich into dev/mobile-cosmetic-improvement 2022-10-14 15:58:26 -05:00
Alex
d43a08eb71 feat(mobile) integrate new grid system to upstream 2022-10-14 15:39:16 -05:00
Alex Tran
293e713af6 Hide bottom app bar when multiselect enabled 2022-10-14 15:37:15 -05:00
Alex Tran
03866b4c31 Merge branch 'feature/albums-new-grid' of github.com:immich-app/immich into feature/albums-new-grid 2022-10-14 14:52:07 -05:00
Alex Tran
4f2c08525f Merge upstream 2022-10-14 14:52:00 -05:00
Matthias Rupp
2c12f53937 Fix storage indicator settings 2022-10-14 21:17:23 +02:00
Alex Tran
c88e5f9be2 Add haptic feedback on swiping asset 2022-10-14 11:26:10 -05:00
Alex Tran
0f51a9794e fixed delete dialog colors 2022-10-14 11:15:19 -05:00
Alex Tran
edd1f49e57 Update text for foreground backup to make things clearer 2022-10-14 10:50:40 -05:00
Alex
4df0cf2d07 Merge pull request #821 from immich-app/feature/jwt-bits-warning
Log a warning if JWT_SECRET key does not have enough bits
2022-10-14 08:42:27 -05:00
bo0tzz
87ba99755b Remove unused variable 2022-10-13 22:17:31 +02:00
bo0tzz
c03f860f8e Log a warning if JWT_SECRET key does not have enough bits 2022-10-13 21:54:29 +02:00
Alex
f2e0e3f345 Merge pull request #820 from immich-app/dependabot/github_actions/docker/setup-qemu-action-2.1.0
chore(deps): bump docker/setup-qemu-action from 2.0.0 to 2.1.0
2022-10-13 08:44:37 -05:00
dependabot[bot]
fee652dfd7 chore(deps): bump docker/setup-qemu-action from 2.0.0 to 2.1.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-13 10:55:56 +00:00
Alex
839446a88d Merge pull request #816 from immich-app/dependabot/github_actions/docker/build-push-action-3.2.0
chore(deps): bump docker/build-push-action from 3.1.1 to 3.2.0
2022-10-12 08:50:14 -05:00
Alex
028b8c8bcc Merge pull request #815 from immich-app/dependabot/github_actions/docker/setup-buildx-action-2.1.0
chore(deps): bump docker/setup-buildx-action from 2.0.0 to 2.1.0
2022-10-12 08:50:02 -05:00
dependabot[bot]
64b1d4ca3b chore(deps): bump docker/build-push-action from 3.1.1 to 3.2.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3.1.1 to 3.2.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3.1.1...v3.2.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-12 10:47:08 +00:00
dependabot[bot]
c6cbee6563 chore(deps): bump docker/setup-buildx-action from 2.0.0 to 2.1.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-12 10:47:05 +00:00
Alex
a406f6e7cc feat(server): Remove default JWT_SECRET value in .env 2022-10-12 03:52:06 -05:00
bo0tzz
9869b92c2b Generate random JWT_SECRET value in install.sh 2022-10-12 09:34:10 +02:00
bo0tzz
00549eed79 Uncomment JWT_SECRET in default .env
Co-authored-by: Alex <alex.tran1502@gmail.com>
2022-10-12 09:18:43 +02:00
bo0tzz
0c4968dc30 Fix: Remove default JWT_SECRET value in .env 2022-10-11 21:13:37 +02:00
Alex
704335c898 Merge pull request #801 from EvilOlaf/patch-1 2022-10-10 05:06:07 -05:00
Werner
ec74feea5a Typo/minor cosmetics 2022-10-10 07:46:23 +02:00
Alex
2f5cc3059a Merge pull request #799 from AnTheMaker/patch-1 2022-10-09 11:00:25 -05:00
An | Anton Röhm
4355485581 lower z-index of #immich-scrubbable-scrollbar 2022-10-09 16:55:10 +02:00
An | Anton Röhm
342c3254cb add z-index to #account-info-panel 2022-10-09 16:54:21 +02:00
Alex
5fc82dfaa2 Merge pull request #793 from immich-app/fix/update-reverse-geocoder
Update local-reverse-geocoder to 0.12.5
2022-10-08 15:40:42 -05:00
Matthias Rupp
6ab6507db9 Revert changes to albums 2022-10-08 13:18:45 +02:00
Matthias Rupp
3c807ae86e Exernalize multiselect state 2022-10-08 13:08:56 +02:00
Alex Tran
9bfacaa39a Specific specific type for enum value for openapi generator to work correctly 2022-10-07 14:30:15 -05:00
Alex Tran
a2882a4908 Added additional type to enum of openapi 2022-10-07 14:26:16 -05:00
Alex
1adc64a352 chore: add GitHub action to generate SDK in Rust/Typescript/Dart 2022-10-07 09:52:11 -05:00
Alex Tran
c28863966b Remove build on PR 2022-10-07 09:50:04 -05:00
Alex Tran
14dc679332 Added SDK to Rust 2022-10-07 09:46:10 -05:00
Alex Tran
17085dd8a0 Added SDK to Rust 2022-10-07 09:39:22 -05:00
Alex Tran
82b8313da0 Fix test 2022-10-07 09:16:45 -05:00
Alex Tran
4f7e764fa0 Fix typing 2022-10-07 09:15:05 -05:00
Alex
d52da8bbea Merge pull request #794 from immich-app/792-bug-encoded-videos-are-not-removed-when-original-asset-is-deleted
fix(server): Delete encoded video when deleting file
2022-10-07 08:47:56 -05:00
Alex Tran
cdddcad784 fix(server): Delete encoded video when deleting file 2022-10-07 08:47:13 -05:00
bo0tzz
38767cad0f Update local-reverse-geocoder to 0.12.5
This version includes a fix to the error handling in that library, which
was causing our code to silently fail and loop.
See https://github.com/tomayac/local-reverse-geocoder/issues/58 for more detail.
2022-10-07 12:14:27 +02:00
Alex Tran
c3d7dda61f Added generation for dart 2022-10-06 17:23:05 -05:00
Alex Tran
c4e32ce159 Rename repo 2022-10-06 16:15:36 -05:00
Alex Tran
6355a07dc4 Added github token custom 2022-10-06 16:09:15 -05:00
Alex Tran
0e3fb41e73 fixed 2022-10-06 15:48:38 -05:00
Alex Tran
fdac5af5ee Added github token 2022-10-06 15:47:47 -05:00
Alex Tran
0e509ceafa Added permissionf or github bot 2022-10-06 15:45:30 -05:00
Matthias Rupp
6b84534632 Get rid of home page state provider 2022-10-06 22:41:56 +02:00
Alex Tran
fc255b558d fix 2022-10-06 15:40:01 -05:00
Alex Tran
9e54e30011 git push force 2022-10-06 15:38:38 -05:00
Alex Tran
77312ce2e0 Force push 2022-10-06 15:37:03 -05:00
Alex Tran
9a6d29d6e7 Add global config for git 2022-10-06 15:34:42 -05:00
Alex Tran
2cb7517f64 Fix url 2022-10-06 15:33:07 -05:00
Alex Tran
3228882fc0 Authenticate 2022-10-06 15:32:19 -05:00
Alex Tran
6804e3dc73 Fixed 2022-10-06 15:27:31 -05:00
Alex Tran
f9af61a5ca Manually push to repo 2022-10-06 15:21:17 -05:00
Alex Tran
a94b443f13 Push to typescript sdk repo 2022-10-06 15:11:09 -05:00
Alex Tran
fd06aa2135 Add workflow to PR to test 2022-10-06 15:04:24 -05:00
Alex Tran
dd0f40559d added github action file' 2022-10-06 14:59:54 -05:00
Alex Tran
471a60dcb0 Added explicit type for job count 2022-10-06 12:43:02 -05:00
Alex Tran
46994c3355 Up version for release 2022-10-06 12:11:12 -05:00
Alex Tran
642811869c Fixed staging action runs only in PR 2022-10-06 11:38:56 -05:00
Alex Tran
3be4697487 Added docker build and push with PR number as tag for easy testing in production environment 2022-10-06 11:34:27 -05:00
Fynn Petersen-Frey
a3aca4acb5 feat(mobile) Run background service after being killed (#789) 2022-10-06 11:32:45 -05:00
Alex
7587f858ae feat(server/web) Add manual job trigger mechanism to the web (#767) 2022-10-06 11:25:54 -05:00
bo0tzz
854c214bc0 Fix: Use boolean comparison for DISABLE_REVERSE_GEOCODING config (#787) 2022-10-05 15:18:57 -05:00
Fynn Petersen-Frey
5dfce4db34 feat(mobile): background backup progress notifications (#781)
* settings to configure upload progress notifications (none/standard/detailed)
* use native Android notifications to show progress information
* e.g. 50% (30/60) assets
* e.g. Uploading asset XYZ - 25% (2/8MB)
* no longer show errors if canceled by system (losing network)
2022-10-05 09:59:35 -05:00
Zack Pollard
95467fa3c1 Merge pull request #785 from bivainis/patch-1
chore: fix github action name
2022-10-05 12:38:46 +01:00
Gediminas Bivainis
4ec3453558 chore: fix github action name 2022-10-05 12:19:11 +02:00
Matthias Rupp
a117e897ca Move selection logic to asset grid class 2022-10-01 19:19:40 +02:00
Matthias Rupp
347ac70063 Make new asset grid the default 2022-10-01 10:38:11 +02:00
Matthias Rupp
50842ef815 Add tests 2022-09-30 11:38:00 +02:00
Matthias Rupp
1970a64f6f Use new asset grid for search result page 2022-09-30 11:05:54 +02:00
Matthias Rupp
dd71a53f5e Hide scroll handle for lists < 100 assets 2022-09-30 10:47:31 +02:00
Matthias Rupp
8440d9890c Improve scrolling performance in albums and search 2022-09-29 21:53:35 +02:00
242 changed files with 7237 additions and 1931 deletions

View File

@@ -17,17 +17,17 @@ jobs:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.1.1
uses: docker/build-push-action@v3.2.0
with:
context: ./server
file: ./server/Dockerfile
@@ -45,17 +45,17 @@ jobs:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning
uses: docker/build-push-action@v3.1.1
uses: docker/build-push-action@v3.2.0
with:
context: ./machine-learning
file: ./machine-learning/Dockerfile
@@ -72,17 +72,17 @@ jobs:
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Web
uses: docker/build-push-action@v3.1.1
uses: docker/build-push-action@v3.2.0
with:
context: ./web
file: ./web/Dockerfile
@@ -100,17 +100,17 @@ jobs:
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Proxy
uses: docker/build-push-action@v3.1.1
uses: docker/build-push-action@v3.2.0
with:
context: ./nginx
file: ./nginx/Dockerfile

View File

@@ -2,8 +2,6 @@ name: Build and Push Docker Image - Staging
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
branches: [main]
@@ -19,10 +17,10 @@ jobs:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
@@ -30,7 +28,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.1.1
uses: docker/build-push-action@v3.2.0
with:
context: ./server
file: ./server/Dockerfile
@@ -38,6 +36,7 @@ jobs:
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-server:staging
altran1502/immich-server:${{ github.event.pull_request.number }}
build_and_push_machine_learning_staging:
runs-on: ubuntu-latest
@@ -48,10 +47,10 @@ jobs:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
@@ -59,7 +58,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning
uses: docker/build-push-action@v3.1.1
uses: docker/build-push-action@v3.2.0
with:
context: ./machine-learning
file: ./machine-learning/Dockerfile
@@ -67,6 +66,7 @@ jobs:
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-machine-learning:staging
altran1502/immich-machine-learning:${{ github.event.pull_request.number }}
build_and_push_web_staging:
runs-on: ubuntu-latest
@@ -76,10 +76,10 @@ jobs:
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
@@ -87,7 +87,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Web
uses: docker/build-push-action@v3.1.1
uses: docker/build-push-action@v3.2.0
with:
context: ./web
file: ./web/Dockerfile
@@ -96,6 +96,7 @@ jobs:
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-web:staging
altran1502/immich-web:${{ github.event.pull_request.number }}
build_and_push_nginx_staging:
runs-on: ubuntu-latest
@@ -105,10 +106,10 @@ jobs:
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
@@ -116,7 +117,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Proxy
uses: docker/build-push-action@v3.1.1
uses: docker/build-push-action@v3.2.0
with:
context: ./nginx
file: ./nginx/Dockerfile
@@ -124,3 +125,4 @@ jobs:
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-proxy:staging
altran1502/immich-proxy:${{ github.event.pull_request.number }}

View File

@@ -22,11 +22,11 @@ jobs:
fallback: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
@@ -35,7 +35,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-server release
uses: docker/build-push-action@v3.1.1
uses: docker/build-push-action@v3.2.0
with:
context: ./server
file: ./server/Dockerfile
@@ -58,17 +58,17 @@ jobs:
with:
fallback: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning
uses: docker/build-push-action@v3.1.1
uses: docker/build-push-action@v3.2.0
with:
context: ./machine-learning
file: ./machine-learning/Dockerfile
@@ -94,11 +94,11 @@ jobs:
fallback: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
@@ -107,7 +107,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-web release
uses: docker/build-push-action@v3.1.1
uses: docker/build-push-action@v3.2.0
with:
context: ./web
file: ./web/Dockerfile
@@ -134,11 +134,11 @@ jobs:
fallback: latest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2.0.0
uses: docker/setup-buildx-action@v2.2.1
- name: Login to Docker Hub
uses: docker/login-action@v2
@@ -147,7 +147,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-proxy release
uses: docker/build-push-action@v3.1.1
uses: docker/build-push-action@v3.2.0
with:
context: ./nginx
file: ./nginx/Dockerfile

View File

@@ -0,0 +1,21 @@
name: Update Immich SDK
on:
workflow_dispatch:
push:
branches: ["main"]
jobs:
update-sdk-repos:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6
with:
github-token: ${{ secrets.GH_TOKEN }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'immich-app',
repo: 'immich-sdk-typescript-axios',
workflow_id: 'build.yml',
ref: 'main'
})

View File

@@ -15,7 +15,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
- name: Run Immich Server 2E2 Test
- name: Run Immich Server E2E Test
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
server-unit-tests:

View File

@@ -53,7 +53,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
# Features
> ⚠️ WARNING: **NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS**. This project is under heavy development, there will be continuous functions, features and api changes.
> ⚠️ WARNING: **NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS**. This project is under heavy development. There will be continuous functions, features and api changes.
| Features | Mobile | Web |
| - | - | - |
@@ -116,13 +116,12 @@ There are several services that compose Immich:
# Installation
NOTE: When using a reverse proxy in front of Immich (such as NGINX), the reverse proxy might require extra configuration to allow large files to be uploaded (such as client_max_body_size in the case of NGINX).
NOTE: When using a reverse proxy in front of Immich (such as NGINX), the reverse proxy might require extra configuration to allow large files to be uploaded (such as `client_max_body_size` in the case of NGINX).
## Testing one-step installation (not recommended for production)
## Testing One-step installation (not recommended for production)
> ⚠️ *This installation method is for evaluating Immich before further customization to meet the users' needs.*
> ⚠️ *This installation method is for evaluating Immich before futher customization to meet the users' needs.*
*Applicable system: Ubuntu, Debian, MacOS*
*Applicable operating systems: Ubuntu, Debian, MacOS*
- In the shell, from the directory of your choice, run the following command:
@@ -197,6 +196,10 @@ If you have installed, you can update the application by navigate to the directo
```bash
docker-compose pull && docker-compose up -d
```
# Unraid Installation
Please follow this [article](https://mfaz.dev/posts/immich-unraid/) for a tutorial on how to install Immich on Unraid
# Mobile app
@@ -204,7 +207,7 @@ docker-compose pull && docker-compose up -d
| - | - | - |
| <a href="https://f-droid.org/packages/app.alextran.immich"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" height="80"></a> | <p align="left"> <a href="https://play.google.com/store/apps/details?id=app.alextran.immich"><img src="design/google-play-qr-code.png" width="200" title="Google Play Store"></a> <p/> | <p align="left"> <a href="https://apps.apple.com/us/app/immich/id1613945652"><img src="design/ios-qr-code.png" width="200" title="Apple App Store"></a> <p/> |
> *The Play/App Store version might be lagging behind the latest release due to the review process.*
> *The Play/App Store version might be lagging behind the latest release due to their review process.*
# App Beta release channel

View File

@@ -38,7 +38,10 @@ LOG_LEVEL=simple
# JWT SECRET
###################################################################################
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
# This JWT_SECRET is used to sign the authentication keys for user login
# You should set it to a long randomly generated value
# You can use this command to generate one: openssl rand -base64 128
JWT_SECRET=
###################################################################################
# Reverse Geocoding

View File

@@ -18,33 +18,37 @@ get_release_version() {
create_immich_directory() {
echo "Creating Immich directory..."
mkdir -p ./immich-app/immich-data
cd ./immich-app
}
download_docker_compose_file() {
echo "Downloading docker-compose.yml..."
curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/docker-compose.yml -o ./immich-app/docker-compose.yml >/dev/null 2>&1
curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/docker-compose.yml -o ./docker-compose.yml >/dev/null 2>&1
}
download_dot_env_file() {
echo "Downloading .env file..."
curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/.env.example -o ./immich-app/.env >/dev/null 2>&1
curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/.env.example -o ./.env >/dev/null 2>&1
}
replace_env_value() {
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s|$1=.*|$1=$2|" ./.env
else
sed -i "s|$1=.*|$1=$2|" ./.env
fi
}
populate_upload_location() {
echo "Populating default UPLOAD_LOCATION value..."
upload_location=$(pwd)/immich-data
replace_env_value "UPLOAD_LOCATION" $upload_location
}
cd ./immich-app/immich-data
upload_location=$(pwd)
# Replace value of UPLOAD_LOCATION in .env with upload_location path
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s|UPLOAD_LOCATION=.*|UPLOAD_LOCATION=$upload_location|" ../.env
else
sed -i "s|UPLOAD_LOCATION=.*|UPLOAD_LOCATION=$upload_location|" ../.env
fi
cd ..
generate_jwt_secret() {
echo "Generating JWT_SECRET value..."
jwt_secret=$(openssl rand -base64 128)
replace_env_value "JWT_SECRET" $jwt_secret
}
start_docker_compose() {
@@ -88,4 +92,5 @@ create_immich_directory
download_docker_compose_file
download_dot_env_file
populate_upload_location
generate_jwt_secret
start_docker_compose

View File

@@ -7,8 +7,9 @@ WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN apt-get update
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
RUN apt-get update > /dev/null \
&& apt-get install --no-install-recommends -y gcc g++ make cmake python3 python3-pip ffmpeg > /dev/null \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
RUN npm ci
RUN npm rebuild @tensorflow/tfjs-node --build-from-source
@@ -23,6 +24,9 @@ FROM node:16-bullseye-slim
ARG DEBIAN_FRONTEND=noninteractive
COPY LICENSE /licenses/LICENSE.txt
COPY LICENSE /LICENSE
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
@@ -30,13 +34,18 @@ COPY entrypoint.sh ./
RUN mkdir -p /usr/src/app/dist \
&& mkdir -p /usr/src/app/node_modules \
&& apt-get update \
&& apt-get install -y ffmpeg \
&& rm -rf /var/cache/apt/lists
&& mkdir -p /usr/src/app/.reverse-geocoding-dump \
&& apt-get update > /dev/null \
&& apt-get install --no-install-recommends -y ffmpeg > /dev/null \
&& apt-get clean \
&& rm -rf /var/cache/apt/lists/*
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/dist ./dist
RUN npm prune --production
# CMD [ "node", "dist/main" ]
RUN chown -R node:0 /usr/src/app \
&& chmod -R g=u /usr/src/app
RUN addgroup node root

21
machine-learning/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Hau Tran
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich">
<application android:label="Immich" android:name="${applicationName}" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich" xmlns:tools="http://schemas.android.com/tools">
<application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true">
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
@@ -12,12 +12,15 @@
</intent-filter>
</activity>
<service android:name=".AppClearedService" android:stopWithTask="false" />
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data android:name="flutterEmbedding" android:value="2" />
<!-- Disables default WorkManager initialization to use our custom initialization -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

View File

@@ -1,25 +0,0 @@
package app.alextran.immich
import android.app.Service
import android.content.Intent
import android.os.IBinder
/**
* Catches the event when either the system or the user kills the app
* (does not apply on force close!)
*/
class AppClearedService() : Service() {
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
return START_NOT_STICKY;
}
override fun onTaskRemoved(rootIntent: Intent) {
ContentObserverWorker.workManagerAppClearedWorkaround(applicationContext)
stopSelf();
}
}

View File

@@ -10,7 +10,7 @@ import io.flutter.plugin.common.MethodChannel
* Android plugin for Dart `BackgroundService`
*
* Receives messages/method calls from the foreground Dart side to manage
* the background service, e.g. start (enqueue), stop (cancel)
* the background service, e.g. start (enqueue), stop (cancel)
*/
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
@@ -38,14 +38,15 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
val ctx = context!!
when(call.method) {
when (call.method) {
"enable" -> {
val args = call.arguments<ArrayList<*>>()!!
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
.edit()
.putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
.putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
.apply()
.edit()
.putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true)
.putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
.putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
.apply()
ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean)
result.success(true)
}
@@ -54,7 +55,7 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
val requireUnmeteredNetwork = args.get(0) as Boolean
val requireCharging = args.get(1) as Boolean
ContentObserverWorker.configureWork(ctx, requireUnmeteredNetwork, requireCharging)
result.success(true)
result.success(true)
}
"disable" -> {
ContentObserverWorker.disable(ctx)

View File

@@ -1,5 +1,6 @@
package app.alextran.immich
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
@@ -47,6 +48,8 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext)
private var timeBackupStarted: Long = 0L
private var notificationBuilder: NotificationCompat.Builder? = null
private var notificationDetailBuilder: NotificationCompat.Builder? = null
override fun startWork(): ListenableFuture<ListenableWorker.Result> {
@@ -61,16 +64,14 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
// Create a Notification channel if necessary
createChannel()
}
val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
.getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
if (isIgnoringBatteryOptimizations) {
// normal background services can only up to 10 minutes
// foreground services are allowed to run indefinitely
// requires battery optimizations to be disabled (either manually by the user
// or by the system learning that immich is important to the user)
setForegroundAsync(createForegroundInfo(title))
} else {
showBackgroundInfo(title)
val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
.getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
showInfo(getInfoBuilder(title, indeterminate=true).build())
}
engine = FlutterEngine(ctx)
@@ -154,18 +155,21 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
}
"updateNotification" -> {
val args = call.arguments<ArrayList<*>>()!!
val title = args.get(0) as String
val content = args.get(1) as String
if (isIgnoringBatteryOptimizations) {
setForegroundAsync(createForegroundInfo(title, content))
} else {
showBackgroundInfo(title, content)
val title = args.get(0) as String?
val content = args.get(1) as String?
val progress = args.get(2) as Int
val max = args.get(3) as Int
val indeterminate = args.get(4) as Boolean
val isDetail = args.get(5) as Boolean
val onlyIfFG = args.get(6) as Boolean
if (!onlyIfFG || isIgnoringBatteryOptimizations) {
showInfo(getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(), isDetail)
}
}
"showError" -> {
val args = call.arguments<ArrayList<*>>()!!
val title = args.get(0) as String
val content = args.get(1) as String
val content = args.get(1) as String?
val individualTag = args.get(2) as String?
showError(title, content, individualTag)
}
@@ -182,13 +186,12 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
}
}
private fun showError(title: String, content: String, individualTag: String?) {
private fun showError(title: String, content: String?, individualTag: String?) {
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID)
.setContentTitle(title)
.setTicker(title)
.setContentText(content)
.setSmallIcon(R.mipmap.ic_launcher)
.setOnlyAlertOnce(true)
.build()
notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification)
}
@@ -197,38 +200,54 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
notificationManager.cancel(NOTIFICATION_ERROR_ID)
}
private fun showBackgroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null) {
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
.setContentTitle(title)
.setTicker(title)
.setContentText(content)
.setSmallIcon(R.mipmap.ic_launcher)
.setOnlyAlertOnce(true)
.setOngoing(true)
.build()
notificationManager.notify(NOTIFICATION_ID, notification)
}
private fun clearBackgroundNotification() {
notificationManager.cancel(NOTIFICATION_ID)
notificationManager.cancel(NOTIFICATION_DETAIL_ID)
}
private fun createForegroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null): ForegroundInfo {
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
.setContentTitle(title)
.setTicker(title)
.setContentText(content)
.setSmallIcon(R.mipmap.ic_launcher)
.setOngoing(true)
.build()
return ForegroundInfo(NOTIFICATION_ID, notification)
}
private fun showInfo(notification: Notification, isDetail: Boolean = false) {
val id = if(isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID
if (isIgnoringBatteryOptimizations) {
setForegroundAsync(ForegroundInfo(id, notification))
} else {
notificationManager.notify(id, notification)
}
}
private fun getInfoBuilder(
title: String? = null,
content: String? = null,
isDetail: Boolean = false,
progress: Int = 0,
max: Int = 0,
indeterminate: Boolean = false,
): NotificationCompat.Builder {
var builder = if(isDetail) notificationDetailBuilder else notificationBuilder
if (builder == null) {
builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setOnlyAlertOnce(true)
.setOngoing(true)
if (isDetail) {
notificationDetailBuilder = builder
} else {
notificationBuilder = builder
}
}
if (title != null) {
builder.setTicker(title).setContentTitle(title)
}
if (content != null) {
builder.setContentText(content)
}
return builder.setProgress(max, progress, indeterminate)
}
@RequiresApi(Build.VERSION_CODES.O)
private fun createChannel() {
val foreground = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW)
notificationManager.createNotificationChannel(foreground)
val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_DEFAULT)
val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_HIGH)
notificationManager.createNotificationChannel(error)
}
@@ -244,6 +263,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_ERROR_ID = 2
private const val NOTIFICATION_DETAIL_ID = 3
private const val ONE_MINUTE = 60000L
/**

View File

@@ -46,9 +46,6 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
* @param context Android Context
*/
fun enable(context: Context, immediate: Boolean = false) {
// migration to remove any old active background task
WorkManager.getInstance(context).cancelUniqueWork("immich/photoListener")
enqueueObserverWorker(context, ExistingWorkPolicy.KEEP)
Log.d(TAG, "enabled ContentObserverWorker")
if (immediate) {
@@ -123,8 +120,10 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work)
}
private fun startBackupWorker(context: Context, delayMilliseconds: Long) {
fun startBackupWorker(context: Context, delayMilliseconds: Long) {
val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
if (!sp.getBoolean(SHARED_PREF_SERVICE_ENABLED, false))
return
val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true)
val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false)
BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds)

View File

@@ -0,0 +1,19 @@
package app.alextran.immich
import android.app.Application
import androidx.work.Configuration
import androidx.work.WorkManager
class ImmichApp : Application() {
override fun onCreate() {
super.onCreate()
val config = Configuration.Builder().build()
WorkManager.initialize(this, config)
// always start BackupWorker after WorkManager init; this fixes the following bug:
// After the process is killed (by user or system), the first trigger (taking a new picture) is lost.
// Thus, the BackupWorker is not started. If the system kills the process after each initialization
// (because of low memory etc.), the backup is never performed.
// As a workaround, we also run a backup check when initializing the application
ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0)
}
}

View File

@@ -5,21 +5,11 @@ import io.flutter.embedding.engine.FlutterEngine
import android.os.Bundle
import android.content.Intent
class MainActivity: FlutterActivity() {
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine.getPlugins().add(BackgroundServicePlugin())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
try {
startService(Intent(getBaseContext(), AppClearedService::class.java));
} catch (e: Exception) {
// startService must not be called when app is in background (crashes app)
// there is nothing we can do
}
flutterEngine.plugins.add(BackgroundServicePlugin())
}
}

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 48,
"android.injected.version.name" => "1.30.2",
"android.injected.version.code" => 52,
"android.injected.version.name" => "1.33.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -0,0 +1,2 @@
* Fixed run background service after being killed
* Added background backup progress notifications

View File

@@ -0,0 +1,2 @@
* Integrate new grid system to the main timeline.
* Minor UI update.

View File

@@ -0,0 +1 @@
* Improved slow initial loading for large amount of asset.

View File

@@ -0,0 +1,3 @@
* Fixed back button navigation
* Added duplicated asset upload handling mechanism
* Fixed cannot signout completely when "Save Logged In" is checked

View File

@@ -24,6 +24,7 @@
"backup_controller_page_backup_selected": "Ausgewählt: ",
"backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
"backup_controller_page_cancel": "Abbrechen",
"backup_background_service_default_notification": "Suche nach neuen assets…",
"backup_controller_page_created": "Erstellt: {}",
"backup_controller_page_desc_backup": "Aktiviere die Sicherung um Elemente automatisch auf den Server zu laden.",
"backup_controller_page_excluded": "Ausgeschlossen: ",
@@ -123,4 +124,4 @@
"version_announcement_overlay_text_2": "Bitte nehm dir die Zeit und lese das ",
"version_announcement_overlay_text_3": " und achte darauf, dass deine docker-compose und .env Dateien aktuell sind, vor allem wenn du ein System für automatische Updates benutzt (z.B. Watchtower).",
"version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89"
}
}

View File

@@ -46,7 +46,7 @@
"backup_controller_page_backup_sub": "Backed up photos and videos",
"backup_controller_page_cancel": "Cancel",
"backup_controller_page_created": "Created on: {}",
"backup_controller_page_desc_backup": "Turn on backup to automatically upload new assets to the server.",
"backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.",
"backup_controller_page_excluded": "Excluded: ",
"backup_controller_page_failed": "Failed ({})",
"backup_controller_page_filename": "File name: {} [{}]",
@@ -58,14 +58,14 @@
"backup_controller_page_select": "Select",
"backup_controller_page_server_storage": "Server Storage",
"backup_controller_page_start_backup": "Start Backup",
"backup_controller_page_status_off": "Backup is off",
"backup_controller_page_status_on": "Backup is on",
"backup_controller_page_status_off": "Automatic foreground backup is off",
"backup_controller_page_status_on": "Automatic foreground backup is on",
"backup_controller_page_storage_format": "{} of {} used",
"backup_controller_page_to_backup": "Albums to be backup",
"backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "All unique photos and videos from selected albums",
"backup_controller_page_turn_off": "Turn off Backup",
"backup_controller_page_turn_on": "Turn on Backup",
"backup_controller_page_turn_off": "Turn off foreground backup",
"backup_controller_page_turn_on": "Turn on foreground backup",
"backup_controller_page_uploading_file_info": "Uploading file info",
"backup_err_only_album": "Cannot remove the only album",
"backup_info_card_assets": "assets",
@@ -134,6 +134,10 @@
"setting_notifications_notify_never": "never",
"setting_notifications_subtitle": "Adjust your notification preferences",
"setting_notifications_title": "Notifications",
"setting_notifications_total_progress_title": "Show background backup total progress",
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
"setting_notifications_single_progress_title": "Show background backup detail progress",
"setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset",
"setting_pages_app_bar_settings": "Settings",
"share_add": "Add",
"share_add_photos": "Add photos",
@@ -167,8 +171,5 @@
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"experimental_settings_title": "Experimental",
"experimental_settings_subtitle": "Use at your own risk!",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_new_asset_list_subtitle": "Work in progress",
"settings_require_restart": "Please restart Immich to apply this setting"
}
"experimental_settings_subtitle": "Use at your own risk!"
}

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.30.2"
version_number: "1.33.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -25,3 +25,7 @@ const String backgroundBackupInfoBox = "immichBackgroundBackupInfoBox"; // Box
const String backupFailedSince = "immichBackupFailedSince"; // Key 1
const String backupRequireWifi = "immichBackupRequireWifi"; // Key 2
const String backupRequireCharging = "immichBackupRequireCharging"; // Key 3
// Duplicate asset
const String duplicatedAssetsBox = "immichDuplicatedAssetsBox"; // Box
const String duplicatedAssetsKey = "immichDuplicatedAssetsKey"; // Key 1

View File

@@ -7,10 +7,10 @@ import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@@ -31,12 +31,14 @@ void main() async {
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
Hive.registerAdapter(HiveBackupAlbumsAdapter());
Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
await Hive.openBox(userInfoBox);
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
await Hive.openBox(hiveGithubReleaseInfoBox);
await Hive.openBox(userSettingInfoBox);
await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(

View File

@@ -1,22 +1,35 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
import 'package:openapi/api.dart';
class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
AlbumNotifier(this._albumService) : super([]);
AlbumNotifier(this._albumService, this._albumCacheService) : super([]);
final AlbumService _albumService;
final AlbumCacheService _albumCacheService;
_cacheState() {
_albumCacheService.put(state);
}
getAllAlbums() async {
if (await _albumCacheService.isValid() && state.isEmpty) {
state = await _albumCacheService.get();
}
List<AlbumResponseDto>? albums =
await _albumService.getAlbums(isShared: false);
if (albums != null) {
state = albums;
_cacheState();
}
}
deleteAlbum(String albumId) {
state = state.where((album) => album.id != albumId).toList();
_cacheState();
}
Future<AlbumResponseDto?> createAlbum(
@@ -28,6 +41,8 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
if (album != null) {
state = [...state, album];
_cacheState();
return album;
}
return null;
@@ -36,5 +51,8 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
final albumProvider =
StateNotifierProvider<AlbumNotifier, List<AlbumResponseDto>>((ref) {
return AlbumNotifier(ref.watch(albumServiceProvider));
return AlbumNotifier(
ref.watch(albumServiceProvider),
ref.watch(albumCacheServiceProvider),
);
});

View File

@@ -1,12 +1,18 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
import 'package:openapi/api.dart';
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
SharedAlbumNotifier(this._sharedAlbumService) : super([]);
SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService) : super([]);
final AlbumService _sharedAlbumService;
final SharedAlbumCacheService _sharedAlbumCacheService;
_cacheState() {
_sharedAlbumCacheService.put(state);
}
Future<AlbumResponseDto?> createSharedAlbum(
String albumName,
@@ -22,6 +28,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
if (newAlbum != null) {
state = [...state, newAlbum];
_cacheState();
}
return newAlbum;
@@ -33,16 +40,22 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
}
getAllSharedAlbums() async {
if (await _sharedAlbumCacheService.isValid() && state.isEmpty) {
state = await _sharedAlbumCacheService.get();
}
List<AlbumResponseDto>? sharedAlbums =
await _sharedAlbumService.getAlbums(isShared: true);
if (sharedAlbums != null) {
state = sharedAlbums;
_cacheState();
}
}
deleteAlbum(String albumId) async {
state = state.where((album) => album.id != albumId).toList();
_cacheState();
}
Future<bool> leaveAlbum(String albumId) async {
@@ -50,6 +63,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
if (res) {
state = state.where((album) => album.id != albumId).toList();
_cacheState();
return true;
} else {
return false;
@@ -72,7 +86,10 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
final sharedAlbumProvider =
StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
return SharedAlbumNotifier(ref.watch(albumServiceProvider));
return SharedAlbumNotifier(
ref.watch(albumServiceProvider),
ref.watch(sharedAlbumCacheServiceProvider),
);
});
final sharedAlbumDetailProvider = FutureProvider.autoDispose

View File

@@ -0,0 +1,49 @@
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/services/json_cache.dart';
import 'package:openapi/api.dart';
class BaseAlbumCacheService extends JsonCache<List<AlbumResponseDto>> {
BaseAlbumCacheService(super.cacheFileName);
@override
void put(List<AlbumResponseDto> data) {
putRawData(data.map((e) => e.toJson()).toList());
}
@override
Future<List<AlbumResponseDto>> get() async {
try {
final mapList = await readRawData() as List<dynamic>;
final responseData = mapList
.map((e) => AlbumResponseDto.fromJson(e))
.whereNotNull()
.toList();
return responseData;
} catch (e) {
debugPrint(e.toString());
return [];
}
}
}
class AlbumCacheService extends BaseAlbumCacheService {
AlbumCacheService() : super("album_cache");
}
class SharedAlbumCacheService extends BaseAlbumCacheService {
SharedAlbumCacheService() : super("shared_album_cache");
}
final albumCacheServiceProvider = Provider(
(ref) => AlbumCacheService(),
);
final sharedAlbumCacheServiceProvider = Provider(
(ref) => SharedAlbumCacheService(),
);

View File

@@ -3,7 +3,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';

View File

@@ -151,7 +151,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
actions: [
TextButton(
style: TextButton.styleFrom(
primary: Theme.of(context).primaryColor,
foregroundColor: Theme.of(context).primaryColor,
),
onPressed:
sharedUsersList.value.isEmpty ? null : _createSharedAlbum,

View File

@@ -1,5 +1,6 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_swipe_detector/flutter_swipe_detector.dart';
import 'package:hive/hive.dart';
@@ -111,6 +112,9 @@ class GalleryViewerPage extends HookConsumerWidget {
: const BouncingScrollPhysics(),
itemCount: assetList.length,
scrollDirection: Axis.horizontal,
onPageChanged: (value) {
HapticFeedback.selectionClick();
},
itemBuilder: (context, index) {
initState(index);

View File

@@ -14,6 +14,7 @@ import 'package:immich_mobile/modules/backup/background_service/localization.dar
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart';
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
@@ -27,11 +28,11 @@ final backgroundServiceProvider = Provider(
/// Background backup service
class BackgroundService {
static const String _portNameLock = "immichLock";
BackgroundService();
static const MethodChannel _foregroundChannel =
MethodChannel('immich/foregroundChannel');
static const MethodChannel _backgroundChannel =
MethodChannel('immich/backgroundChannel');
static final NumberFormat numberFormat = NumberFormat("###0.##");
bool _isBackgroundInitialized = false;
CancellationToken? _cancellationToken;
bool _canceledBySystem = false;
@@ -40,6 +41,10 @@ class BackgroundService {
SendPort? _waitingIsolate;
ReceivePort? _rp;
bool _errorGracePeriodExceeded = true;
int _uploadedAssetsCount = 0;
int _assetsToUploadCount = 0;
int _lastDetailProgressUpdate = 0;
String _lastPrintedProgress = "";
bool get isBackgroundInitialized {
return _isBackgroundInitialized;
@@ -125,22 +130,29 @@ class BackgroundService {
}
/// Updates the notification shown by the background service
Future<bool> _updateNotification({
required String title,
Future<bool?> _updateNotification({
String? title,
String? content,
int progress = 0,
int max = 0,
bool indeterminate = false,
bool isDetail = false,
bool onlyIfFG = false,
}) async {
if (!Platform.isAndroid) {
return true;
}
try {
if (_isBackgroundInitialized) {
return await _backgroundChannel
.invokeMethod('updateNotification', [title, content]);
return _backgroundChannel.invokeMethod<bool>(
'updateNotification',
[title, content, progress, max, indeterminate, isDetail, onlyIfFG],
);
}
} catch (error) {
debugPrint("[_updateNotification] failed to communicate with plugin");
}
return Future.value(false);
return false;
}
/// Shows a new priority notification
@@ -274,6 +286,7 @@ class BackgroundService {
case "onAssetsChanged":
final Future<bool> translationsLoaded = loadTranslations();
try {
_clearErrorNotifications();
final bool hasAccess = await acquireLock();
if (!hasAccess) {
debugPrint("[_callHandler] could not acquire lock, exiting");
@@ -304,28 +317,35 @@ class BackgroundService {
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
Hive.registerAdapter(HiveBackupAlbumsAdapter());
Hive.registerAdapter(HiveDuplicatedAssetsAdapter());
await Hive.openBox(userInfoBox);
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
await Hive.openBox(userSettingInfoBox);
await Hive.openBox(backgroundBackupInfoBox);
await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox);
ApiService apiService = ApiService();
apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey));
BackupService backupService = BackupService(apiService);
AppSettingsService settingsService = AppSettingsService();
final Box<HiveBackupAlbums> box =
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
if (backupAlbumInfo == null) {
_clearErrorNotifications();
return true;
}
await PhotoManager.setIgnorePermissionCheck(true);
do {
final bool backupOk = await _runBackup(backupService, backupAlbumInfo);
final bool backupOk = await _runBackup(
backupService,
settingsService,
backupAlbumInfo,
);
if (backupOk) {
await Hive.box(backgroundBackupInfoBox).delete(backupFailedSince);
await box.put(
@@ -346,9 +366,14 @@ class BackgroundService {
Future<bool> _runBackup(
BackupService backupService,
AppSettingsService settingsService,
HiveBackupAlbums backupAlbumInfo,
) async {
_errorGracePeriodExceeded = _isErrorGracePeriodExceeded();
_errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService);
final bool notifyTotalProgress = settingsService
.getSetting<bool>(AppSettingsEnum.backgroundBackupTotalProgress);
final bool notifySingleProgress = settingsService
.getSetting<bool>(AppSettingsEnum.backgroundBackupSingleProgress);
if (_canceledBySystem) {
return false;
@@ -372,22 +397,29 @@ class BackgroundService {
}
if (toUpload.isEmpty) {
_clearErrorNotifications();
return true;
}
_assetsToUploadCount = toUpload.length;
_uploadedAssetsCount = 0;
_updateNotification(
title: "backup_background_service_in_progress_notification".tr(),
content: notifyTotalProgress ? _formatAssetBackupProgress() : null,
progress: 0,
max: notifyTotalProgress ? _assetsToUploadCount : 0,
indeterminate: !notifyTotalProgress,
onlyIfFG: !notifyTotalProgress,
);
_cancellationToken = CancellationToken();
final bool ok = await backupService.backupAsset(
toUpload,
_cancellationToken!,
_onAssetUploaded,
_onProgress,
_onSetCurrentBackupAsset,
notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId, isDup) {},
notifySingleProgress ? _onProgress : (sent, total) {},
notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {},
_onBackupError,
);
if (ok) {
_clearErrorNotifications();
} else {
if (!ok && !_cancellationToken!.isCancelled) {
_showErrorNotification(
title: "backup_background_service_error_title".tr(),
content: "backup_background_service_backup_failed_message".tr(),
@@ -396,16 +428,43 @@ class BackgroundService {
return ok;
}
void _onAssetUploaded(String deviceAssetId, String deviceId) {
debugPrint("Uploaded $deviceAssetId from $deviceId");
String _formatAssetBackupProgress() {
final int percent = (_uploadedAssetsCount * 100) ~/ _assetsToUploadCount;
return "$percent% ($_uploadedAssetsCount/$_assetsToUploadCount)";
}
void _onProgress(int sent, int total) {}
void _onAssetUploaded(String deviceAssetId, String deviceId, bool isDup) {
debugPrint("Uploaded $deviceAssetId from $deviceId");
_uploadedAssetsCount++;
_updateNotification(
progress: _uploadedAssetsCount,
max: _assetsToUploadCount,
content: _formatAssetBackupProgress(),
);
}
void _onProgress(int sent, int total) {
final int now = Timeline.now;
// limit updates to 10 per second (or Android drops important notifications)
if (now > _lastDetailProgressUpdate + 100000) {
final String msg = _humanReadableBytesProgress(sent, total);
// only update if message actually differs (to stop many useless notification updates on large assets or slow connections)
if (msg != _lastPrintedProgress) {
_lastDetailProgressUpdate = now;
_lastPrintedProgress = msg;
_updateNotification(
progress: sent,
max: total,
isDetail: true,
content: msg,
);
}
}
}
void _onBackupError(ErrorUploadAsset errorAssetInfo) {
_showErrorNotification(
title: "Upload failed",
content: "backup_background_service_upload_failure_notification"
title: "backup_background_service_upload_failure_notification"
.tr(args: [errorAssetInfo.fileName]),
individualTag: errorAssetInfo.id,
);
@@ -413,14 +472,17 @@ class BackgroundService {
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
_updateNotification(
title: "backup_background_service_in_progress_notification".tr(),
content: "backup_background_service_current_upload_notification"
title: "backup_background_service_current_upload_notification"
.tr(args: [currentUploadAsset.fileName]),
content: "",
isDetail: true,
progress: 0,
max: 0,
);
}
bool _isErrorGracePeriodExceeded() {
final int value = AppSettingsService()
bool _isErrorGracePeriodExceeded(AppSettingsService appSettingsService) {
final int value = appSettingsService
.getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod);
if (value == 0) {
return true;
@@ -445,6 +507,26 @@ class BackgroundService {
assert(false, "Invalid value");
return true;
}
/// prints percentage and absolute progress in useful (kilo/mega/giga)bytes
static String _humanReadableBytesProgress(int bytes, int bytesTotal) {
String unit = "KB"; // Kilobyte
if (bytesTotal >= 0x40000000) {
unit = "GB"; // Gigabyte
bytes >>= 20;
bytesTotal >>= 20;
} else if (bytesTotal >= 0x100000) {
unit = "MB"; // Megabyte
bytes >>= 10;
bytesTotal >>= 10;
} else if (bytesTotal < 0x400) {
return "$bytes / $bytesTotal B";
}
final int percent = (bytes * 100) ~/ bytesTotal;
final String done = numberFormat.format(bytes / 1024.0);
final String total = numberFormat.format(bytesTotal / 1024.0);
return "$percent% ($done/$total$unit)";
}
}
/// entry point called by Kotlin/Java code; needs to be a top-level function

View File

@@ -21,7 +21,9 @@ Future<bool> loadTranslations() async {
await controller.loadTranslations();
return Localization.load(controller.locale,
translations: controller.translations,
fallbackTranslations: controller.fallbackTranslations);
return Localization.load(
controller.locale,
translations: controller.translations,
fallbackTranslations: controller.fallbackTranslations,
);
}

View File

@@ -45,8 +45,10 @@ class ErrorUploadAsset extends Equatable {
List<Object> get props {
return [
id,
createdAt,
fileName,
fileType,
asset,
errorMessage,
];
}

View File

@@ -0,0 +1,57 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:hive/hive.dart';
part 'hive_duplicated_assets.model.g.dart';
@HiveType(typeId: 2)
class HiveDuplicatedAssets {
@HiveField(0, defaultValue: [])
List<String> duplicatedAssetIds;
HiveDuplicatedAssets({
required this.duplicatedAssetIds,
});
HiveDuplicatedAssets copyWith({
List<String>? duplicatedAssetIds,
}) {
return HiveDuplicatedAssets(
duplicatedAssetIds: duplicatedAssetIds ?? this.duplicatedAssetIds,
);
}
Map<String, dynamic> toMap() {
return {
'duplicatedAssetIds': duplicatedAssetIds,
};
}
factory HiveDuplicatedAssets.fromMap(Map<String, dynamic> map) {
return HiveDuplicatedAssets(
duplicatedAssetIds: List<String>.from(map['duplicatedAssetIds']),
);
}
String toJson() => json.encode(toMap());
factory HiveDuplicatedAssets.fromJson(String source) =>
HiveDuplicatedAssets.fromMap(json.decode(source));
@override
String toString() =>
'HiveDuplicatedAssets(duplicatedAssetIds: $duplicatedAssetIds)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
final listEquals = const DeepCollectionEquality().equals;
return other is HiveDuplicatedAssets &&
listEquals(other.duplicatedAssetIds, duplicatedAssetIds);
}
@override
int get hashCode => duplicatedAssetIds.hashCode;
}

View File

@@ -0,0 +1,42 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'hive_duplicated_assets.model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class HiveDuplicatedAssetsAdapter extends TypeAdapter<HiveDuplicatedAssets> {
@override
final int typeId = 2;
@override
HiveDuplicatedAssets read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return HiveDuplicatedAssets(
duplicatedAssetIds:
fields[0] == null ? [] : (fields[0] as List).cast<String>(),
);
}
@override
void write(BinaryWriter writer, HiveDuplicatedAssets obj) {
writer
..writeByte(1)
..writeByte(0)
..write(obj.duplicatedAssetIds);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is HiveDuplicatedAssetsAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -10,6 +10,7 @@ import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
@@ -296,6 +297,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// Those assets are unique and are used as the total assets
///
Future<void> _updateBackupAssetCount() async {
Set<String> duplicatedAssetIds = _backupService.getDuplicatedAssetIds();
Set<AssetEntity> assetsFromSelectedAlbums = {};
Set<AssetEntity> assetsFromExcludedAlbums = {};
@@ -326,9 +328,15 @@ class BackupNotifier extends StateNotifier<BackUpState> {
// Find asset that were backup from selected albums
Set<String> selectedAlbumsBackupAssets =
Set.from(allUniqueAssets.map((e) => e.id));
selectedAlbumsBackupAssets
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
// Remove duplicated asset from all unique assets
allUniqueAssets.removeWhere(
(asset) => duplicatedAssetIds.contains(asset.id),
);
if (allUniqueAssets.isEmpty) {
debugPrint("No Asset On Device");
state = state.copyWith(
@@ -455,14 +463,26 @@ class BackupNotifier extends StateNotifier<BackUpState> {
);
}
void _onAssetUploaded(String deviceAssetId, String deviceId) {
state = state.copyWith(
selectedAlbumsBackupAssetsIds: {
...state.selectedAlbumsBackupAssetsIds,
deviceAssetId
},
allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId],
);
void _onAssetUploaded(
String deviceAssetId,
String deviceId,
bool isDuplicated,
) {
if (isDuplicated) {
state = state.copyWith(
allUniqueAssets: state.allUniqueAssets
.where((asset) => asset.id != deviceAssetId)
.toSet(),
);
} else {
state = state.copyWith(
selectedAlbumsBackupAssetsIds: {
...state.selectedAlbumsBackupAssetsIds,
deviceAssetId
},
allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId],
);
}
if (state.allUniqueAssets.length -
state.selectedAlbumsBackupAssetsIds.length ==
@@ -564,6 +584,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
albums.lastExcludedBackupTime,
);
}
await Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox);
final Box backgroundBox = await Hive.openBox(backgroundBackupInfoBox);
state = state.copyWith(
backupProgress: previous,
@@ -608,6 +629,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
} catch (error) {
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
}
try {
if (Hive.isBoxOpen(duplicatedAssetsBox)) {
await Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox).close();
}
} catch (error) {
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
}
try {
if (Hive.isBoxOpen(backgroundBackupInfoBox)) {
await Hive.box(backgroundBackupInfoBox).close();

View File

@@ -19,6 +19,8 @@ import 'package:http_parser/http_parser.dart';
import 'package:path/path.dart' as p;
import 'package:cancellation_token_http/http.dart' as http;
import '../models/hive_duplicated_assets.model.dart';
final backupServiceProvider = Provider(
(ref) => BackupService(
ref.watch(apiServiceProvider),
@@ -41,6 +43,29 @@ class BackupService {
}
}
void _saveDuplicatedAssetIdToLocalStorage(List<String> deviceAssetIds) {
HiveDuplicatedAssets duplicatedAssets =
Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox)
.get(duplicatedAssetsKey) ??
HiveDuplicatedAssets(duplicatedAssetIds: []);
duplicatedAssets.duplicatedAssetIds =
{...duplicatedAssets.duplicatedAssetIds, ...deviceAssetIds}.toList();
Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox)
.put(duplicatedAssetsKey, duplicatedAssets);
}
/// Get duplicated asset id from Hive storage
Set<String> getDuplicatedAssetIds() {
HiveDuplicatedAssets duplicatedAssets =
Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox)
.get(duplicatedAssetsKey) ??
HiveDuplicatedAssets(duplicatedAssetIds: []);
return duplicatedAssets.duplicatedAssetIds.toSet();
}
/// Returns all assets newer than the last successful backup per album
Future<List<AssetEntity>> buildUploadCandidates(
HiveBackupAlbums backupAlbums,
@@ -140,34 +165,47 @@ class BackupService {
Future<List<AssetEntity>> removeAlreadyUploadedAssets(
List<AssetEntity> candidates,
) async {
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
if (candidates.length < 10) {
final List<CheckDuplicateAssetResponseDto?> duplicateResponse =
await Future.wait(
candidates.map(
(e) => _apiService.assetApi.checkDuplicateAsset(
CheckDuplicateAssetDto(deviceAssetId: e.id, deviceId: deviceId),
),
if (candidates.isEmpty) {
return candidates;
}
final Set<String> duplicatedAssetIds = getDuplicatedAssetIds();
candidates = duplicatedAssetIds.isEmpty
? candidates
: candidates
.whereNot((asset) => duplicatedAssetIds.contains(asset.id))
.toList();
if (candidates.isEmpty) {
return candidates;
}
final Set<String> existing = {};
try {
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
final CheckExistingAssetsResponseDto? duplicates =
await _apiService.assetApi.checkExistingAssets(
CheckExistingAssetsDto(
deviceAssetIds: candidates.map((e) => e.id).toList(),
deviceId: deviceId,
),
);
return candidates
.whereIndexed((i, e) => duplicateResponse[i]?.isExist == false)
.toList();
} else {
final List<String>? allAssetsInDatabase = await getDeviceBackupAsset();
if (allAssetsInDatabase == null) {
return candidates;
if (duplicates != null) {
existing.addAll(duplicates.existingIds);
}
} on ApiException {
// workaround for older server versions or when checking for too many assets at once
final List<String>? allAssetsInDatabase = await getDeviceBackupAsset();
if (allAssetsInDatabase != null) {
existing.addAll(allAssetsInDatabase);
}
final Set<String> inDb = allAssetsInDatabase.toSet();
return candidates.whereNot((e) => inDb.contains(e.id)).toList();
}
return existing.isEmpty
? candidates
: candidates.whereNot((e) => existing.contains(e.id)).toList();
}
Future<bool> backupAsset(
Iterable<AssetEntity> assetList,
http.CancellationToken cancelToken,
Function(String, String) singleAssetDoneCb,
Function(String, String, bool) uploadSuccessCb,
Function(int, int) uploadProgressCb,
Function(CurrentUploadAsset) setCurrentUploadAssetCb,
Function(ErrorUploadAsset) errorCb,
@@ -176,6 +214,7 @@ class BackupService {
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
File? file;
bool anyErrors = false;
final List<String> duplicatedAssetIds = [];
for (var entity in assetList) {
try {
@@ -235,8 +274,13 @@ class BackupService {
var response = await req.send(cancellationToken: cancelToken);
if (response.statusCode == 201) {
singleAssetDoneCb(entity.id, deviceId);
if (response.statusCode == 200) {
// asset is a duplicate (already exists on the server)
duplicatedAssetIds.add(entity.id);
uploadSuccessCb(entity.id, deviceId, true);
} else if (response.statusCode == 201) {
// stored a new asset on the server
uploadSuccessCb(entity.id, deviceId, false);
} else {
var data = await response.stream.bytesToString();
var error = jsonDecode(data);
@@ -260,7 +304,8 @@ class BackupService {
}
} on http.CancelledException {
debugPrint("Backup was cancelled by the user");
return false;
anyErrors = true;
break;
} catch (e) {
debugPrint("ERROR backupAsset: ${e.toString()}");
anyErrors = true;
@@ -271,6 +316,9 @@ class BackupService {
}
}
}
if (duplicatedAssetIds.isNotEmpty) {
_saveDuplicatedAssetIdToLocalStorage(duplicatedAssetIds);
}
return !anyErrors;
}

View File

@@ -16,8 +16,8 @@ class AlbumPreviewPage extends HookConsumerWidget {
final assets = useState<List<AssetEntity>>([]);
_getAssetsInAlbum() async {
assets.value =
await album.getAssetListRange(start: 0, end: album.assetCount);
assets.value = await album.getAssetListRange(
start: 0, end: await album.assetCountAsync);
}
useEffect(
@@ -34,7 +34,7 @@ class AlbumPreviewPage extends HookConsumerWidget {
title: Column(
children: [
Text(
"${album.name} (${album.assetCount})",
"${album.name} (${album.assetCountAsync})",
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
Padding(

View File

@@ -158,7 +158,6 @@ class BackupControllerPage extends HookConsumerWidget {
}
void _showBatteryOptimizationInfoToUser() {
final buttonTextColor = Theme.of(context).primaryColor;
showDialog<void>(
context: context,
barrierDismissible: false,
@@ -173,13 +172,14 @@ class BackupControllerPage extends HookConsumerWidget {
).tr(),
),
actions: [
OutlinedButton(
ElevatedButton(
onPressed: () => launchUrl(
Uri.parse('https://dontkillmyapp.com'),
mode: LaunchMode.externalApplication,
),
child: const Text(
"backup_controller_page_background_battery_info_link",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
).tr(),
),
ElevatedButton(
@@ -220,7 +220,12 @@ class BackupControllerPage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isBackgroundEnabled)
const Text("backup_controller_page_background_description").tr(),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child:
const Text("backup_controller_page_background_description")
.tr(),
),
if (isBackgroundEnabled)
SwitchListTile(
title:
@@ -414,7 +419,6 @@ class BackupControllerPage extends HookConsumerWidget {
ActionChip(
avatar: Icon(
Icons.info,
size: 24,
color: Colors.red[400],
),
elevation: 1,

View File

@@ -1,47 +0,0 @@
import 'package:collection/collection.dart';
import 'package:openapi/api.dart';
class HomePageState {
final bool isMultiSelectEnable;
final Set<AssetResponseDto> selectedItems;
final Set<String> selectedDateGroup;
HomePageState({
required this.isMultiSelectEnable,
required this.selectedItems,
required this.selectedDateGroup,
});
HomePageState copyWith({
bool? isMultiSelectEnable,
Set<AssetResponseDto>? selectedItems,
Set<String>? selectedDateGroup,
}) {
return HomePageState(
isMultiSelectEnable: isMultiSelectEnable ?? this.isMultiSelectEnable,
selectedItems: selectedItems ?? this.selectedItems,
selectedDateGroup: selectedDateGroup ?? this.selectedDateGroup,
);
}
@override
String toString() =>
'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
final setEquals = const DeepCollectionEquality().equals;
return other is HomePageState &&
other.isMultiSelectEnable == isMultiSelectEnable &&
setEquals(other.selectedItems, selectedItems) &&
setEquals(other.selectedDateGroup, selectedDateGroup);
}
@override
int get hashCode =>
isMultiSelectEnable.hashCode ^
selectedItems.hashCode ^
selectedDateGroup.hashCode;
}

View File

@@ -1,95 +1,14 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:openapi/api.dart';
enum RenderAssetGridElementType {
assetRow,
dayTitle,
monthTitle;
}
class RenderAssetGridRow {
final List<AssetResponseDto> assets;
RenderAssetGridRow(this.assets);
}
class RenderAssetGridElement {
final RenderAssetGridElementType type;
final RenderAssetGridRow? assetRow;
final String? title;
final DateTime date;
final List<AssetResponseDto>? relatedAssetList;
RenderAssetGridElement(
this.type, {
this.assetRow,
this.title,
required this.date,
this.relatedAssetList,
});
}
final renderListProvider = StateProvider((ref) {
var assetGroups = ref.watch(assetGroupByDateTimeProvider);
var settings = ref.watch(appSettingsServiceProvider);
var settings = ref.watch(appSettingsServiceProvider);
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
List<RenderAssetGridElement> elements = [];
DateTime? lastDate;
assetGroups.forEach((groupName, assets) {
try {
final date = DateTime.parse(groupName);
if (lastDate == null || lastDate!.month != date.month) {
elements.add(
RenderAssetGridElement(
RenderAssetGridElementType.monthTitle,
title: groupName,
date: date,
),
);
}
// Add group title
elements.add(
RenderAssetGridElement(
RenderAssetGridElementType.dayTitle,
title: groupName,
date: date,
relatedAssetList: assets,
),
);
// Add rows
int cursor = 0;
while (cursor < assets.length) {
int rowElements = min(assets.length - cursor, assetsPerRow);
final rowElement = RenderAssetGridElement(
RenderAssetGridElementType.assetRow,
date: date,
assetRow: RenderAssetGridRow(
assets.sublist(cursor, cursor + rowElements),
),
);
elements.add(rowElement);
cursor += rowElements;
}
lastDate = date;
} catch (e) {
debugPrint(e.toString());
}
});
return elements;
return assetGroupsToRenderList(assetGroups, assetsPerRow);
});

View File

@@ -1,92 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/models/home_page_state.model.dart';
import 'package:immich_mobile/shared/services/share.service.dart';
import 'package:immich_mobile/shared/ui/share_dialog.dart';
import 'package:openapi/api.dart';
class HomePageStateNotifier extends StateNotifier<HomePageState> {
final ShareService _shareService;
HomePageStateNotifier(this._shareService)
: super(
HomePageState(
isMultiSelectEnable: false,
selectedItems: {},
selectedDateGroup: {},
),
);
void addSelectedDateGroup(String dateGroupTitle) {
state = state.copyWith(
selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle},
);
}
void removeSelectedDateGroup(String dateGroupTitle) {
var currentDateGroup = state.selectedDateGroup;
currentDateGroup.removeWhere((e) => e == dateGroupTitle);
state = state.copyWith(selectedDateGroup: currentDateGroup);
}
void enableMultiSelect(Set<AssetResponseDto> selectedItems) {
state =
state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems);
}
void disableMultiSelect() {
state = state.copyWith(
isMultiSelectEnable: false,
selectedItems: {},
selectedDateGroup: {},
);
}
void addSingleSelectedItem(AssetResponseDto asset) {
state = state.copyWith(selectedItems: {...state.selectedItems, asset});
}
void addMultipleSelectedItems(List<AssetResponseDto> assets) {
state = state.copyWith(selectedItems: {...state.selectedItems, ...assets});
}
void removeSingleSelectedItem(AssetResponseDto asset) {
Set<AssetResponseDto> currentList = state.selectedItems;
currentList.removeWhere((e) => e.id == asset.id);
state = state.copyWith(selectedItems: currentList);
}
void removeMultipleSelectedItem(List<AssetResponseDto> assets) {
Set<AssetResponseDto> currentList = state.selectedItems;
for (AssetResponseDto asset in assets) {
currentList.removeWhere((e) => e.id == asset.id);
}
state = state.copyWith(selectedItems: currentList);
}
void shareAssets(List<AssetResponseDto> assets, BuildContext context) {
showDialog(
context: context,
builder: (BuildContext buildContext) {
_shareService
.shareAssets(assets)
.then((_) => Navigator.of(buildContext).pop());
return const ShareDialog();
},
barrierDismissible: false,
);
}
}
final homePageStateProvider =
StateNotifierProvider<HomePageStateNotifier, HomePageState>(
((ref) => HomePageStateNotifier(ref.watch(shareServiceProvider))),
);

View File

@@ -0,0 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
final multiselectProvider = StateProvider((ref) {
return false;
});

View File

@@ -0,0 +1,37 @@
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/services/json_cache.dart';
import 'package:openapi/api.dart';
class AssetCacheService extends JsonCache<List<AssetResponseDto>> {
AssetCacheService() : super("asset_cache");
@override
void put(List<AssetResponseDto> data) {
putRawData(data.map((e) => e.toJson()).toList());
}
@override
Future<List<AssetResponseDto>> get() async {
try {
final mapList = await readRawData() as List<dynamic>;
final responseData = mapList
.map((e) => AssetResponseDto.fromJson(e))
.whereNotNull()
.toList();
return responseData;
} catch (e) {
debugPrint(e.toString());
return [];
}
}
}
final assetCacheServiceProvider = Provider(
(ref) => AssetCacheService(),
);

View File

@@ -0,0 +1,103 @@
import 'dart:math';
import 'package:openapi/api.dart';
enum RenderAssetGridElementType {
assetRow,
dayTitle,
monthTitle;
}
class RenderAssetGridRow {
final List<AssetResponseDto> assets;
RenderAssetGridRow(this.assets);
}
class RenderAssetGridElement {
final RenderAssetGridElementType type;
final RenderAssetGridRow? assetRow;
final String? title;
final DateTime date;
final List<AssetResponseDto>? relatedAssetList;
RenderAssetGridElement(
this.type, {
this.assetRow,
this.title,
required this.date,
this.relatedAssetList,
});
}
List<RenderAssetGridElement> assetsToRenderList(
List<AssetResponseDto> assets, int assetsPerRow) {
List<RenderAssetGridElement> elements = [];
int cursor = 0;
while (cursor < assets.length) {
int rowElements = min(assets.length - cursor, assetsPerRow);
final date = DateTime.parse(assets[cursor].createdAt);
final rowElement = RenderAssetGridElement(
RenderAssetGridElementType.assetRow,
date: date,
assetRow: RenderAssetGridRow(
assets.sublist(cursor, cursor + rowElements),
),
);
elements.add(rowElement);
cursor += rowElements;
}
return elements;
}
List<RenderAssetGridElement> assetGroupsToRenderList(
Map<String, List<AssetResponseDto>> assetGroups, int assetsPerRow) {
List<RenderAssetGridElement> elements = [];
DateTime? lastDate;
assetGroups.forEach((groupName, assets) {
final date = DateTime.parse(groupName);
if (lastDate == null || lastDate!.month != date.month) {
elements.add(
RenderAssetGridElement(RenderAssetGridElementType.monthTitle,
title: groupName, date: date),
);
}
// Add group title
elements.add(
RenderAssetGridElement(
RenderAssetGridElementType.dayTitle,
title: groupName,
date: date,
relatedAssetList: assets,
),
);
// Add rows
int cursor = 0;
while (cursor < assets.length) {
int rowElements = min(assets.length - cursor, assetsPerRow);
final rowElement = RenderAssetGridElement(
RenderAssetGridElementType.assetRow,
date: date,
assetRow: RenderAssetGridRow(
assets.sublist(cursor, cursor + rowElements),
),
);
elements.add(rowElement);
cursor += rowElements;
}
lastDate = date;
});
return elements;
}

View File

@@ -0,0 +1,72 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class DailyTitleText extends ConsumerWidget {
const DailyTitleText({
Key? key,
required this.isoDate,
required this.multiselectEnabled,
required this.onSelect,
required this.onDeselect,
required this.selected,
}) : super(key: key);
final String isoDate;
final bool multiselectEnabled;
final Function onSelect;
final Function onDeselect;
final bool selected;
@override
Widget build(BuildContext context, WidgetRef ref) {
var currentYear = DateTime.now().year;
var groupYear = DateTime.parse(isoDate).year;
var formatDateTemplate = currentYear == groupYear
? "daily_title_text_date".tr()
: "daily_title_text_date_year".tr();
var dateText =
DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
void handleTitleIconClick() {
if (selected) {
onDeselect();
} else {
onSelect();
}
}
return Padding(
padding: const EdgeInsets.only(
top: 29.0,
bottom: 29.0,
left: 12.0,
right: 12.0,
),
child: Row(
children: [
Text(
dateText,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
GestureDetector(
onTap: handleTitleIconClick,
child: multiselectEnabled && selected
? Icon(
Icons.check_circle_rounded,
color: Theme.of(context).primaryColor,
)
: const Icon(
Icons.check_circle_outline_rounded,
color: Colors.grey,
),
)
],
),
);
}
}

View File

@@ -1,40 +1,36 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class DisableMultiSelectButton extends ConsumerWidget {
const DisableMultiSelectButton({
Key? key,
required this.onPressed,
required this.selectedItemCount,
}) : super(key: key);
final Function onPressed;
final int selectedItemCount;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Positioned(
top: 10,
left: 0,
child: Padding(
padding: const EdgeInsets.only(left: 16.0, top: 46),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: ElevatedButton.icon(
onPressed: () {
onPressed();
},
icon: const Icon(Icons.close_rounded),
label: Text(
'$selectedItemCount',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
),
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class DisableMultiSelectButton extends ConsumerWidget {
const DisableMultiSelectButton({
Key? key,
required this.onPressed,
required this.selectedItemCount,
}) : super(key: key);
final Function onPressed;
final int selectedItemCount;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 15),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: ElevatedButton.icon(
onPressed: () {
onPressed();
},
icon: const Icon(Icons.close_rounded),
label: Text(
'$selectedItemCount',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
),
),
);
}
}

View File

@@ -0,0 +1,274 @@
import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
import 'package:openapi/api.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'asset_grid_data_structure.dart';
import 'daily_title_text.dart';
import 'disable_multi_select_button.dart';
import 'draggable_scrollbar_custom.dart';
typedef ImmichAssetGridSelectionListener = void Function(
bool,
Set<AssetResponseDto>,
);
class ImmichAssetGridState extends State<ImmichAssetGrid> {
final ItemScrollController _itemScrollController = ItemScrollController();
final ItemPositionsListener _itemPositionsListener =
ItemPositionsListener.create();
bool _scrolling = false;
final Set<String> _selectedAssets = HashSet();
List<AssetResponseDto> get _assets {
return widget.renderList
.map((e) {
if (e.type == RenderAssetGridElementType.assetRow) {
return e.assetRow!.assets;
} else {
return List<AssetResponseDto>.empty();
}
})
.flattened
.toList();
}
Set<AssetResponseDto> _getSelectedAssets() {
return _selectedAssets
.map((e) => _assets.firstWhereOrNull((a) => a.id == e))
.whereNotNull()
.toSet();
}
void _callSelectionListener(bool selectionActive) {
widget.listener?.call(selectionActive, _getSelectedAssets());
}
void _selectAssets(List<AssetResponseDto> assets) {
setState(() {
for (var e in assets) {
_selectedAssets.add(e.id);
}
_callSelectionListener(true);
});
}
void _deselectAssets(List<AssetResponseDto> assets) {
setState(() {
for (var e in assets) {
_selectedAssets.remove(e.id);
}
_callSelectionListener(_selectedAssets.isNotEmpty);
});
}
void _deselectAll() {
setState(() {
_selectedAssets.clear();
});
_callSelectionListener(false);
}
bool _allAssetsSelected(List<AssetResponseDto> assets) {
return widget.selectionActive &&
assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
}
double _getItemSize(BuildContext context) {
return MediaQuery.of(context).size.width / widget.assetsPerRow -
widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
}
Widget _buildThumbnailOrPlaceholder(
AssetResponseDto asset,
bool placeholder,
) {
if (placeholder) {
return const DecoratedBox(
decoration: BoxDecoration(color: Colors.grey),
);
}
return ThumbnailImage(
asset: asset,
assetList: _assets,
multiselectEnabled: widget.selectionActive,
isSelected: _selectedAssets.contains(asset.id),
onSelect: () => _selectAssets([asset]),
onDeselect: () => _deselectAssets([asset]),
useGrayBoxPlaceholder: true,
showStorageIndicator: widget.showStorageIndicator,
);
}
Widget _buildAssetRow(
BuildContext context,
RenderAssetGridRow row,
bool scrolling,
) {
double size = _getItemSize(context);
return Row(
key: Key("asset-row-${row.assets.first.id}"),
children: row.assets.map((AssetResponseDto asset) {
bool last = asset == row.assets.last;
return Container(
key: Key("asset-${asset.id}"),
width: size,
height: size,
margin: EdgeInsets.only(
top: widget.margin,
right: last ? 0.0 : widget.margin,
),
child: _buildThumbnailOrPlaceholder(asset, scrolling),
);
}).toList(),
);
}
Widget _buildTitle(
BuildContext context,
String title,
List<AssetResponseDto> assets,
) {
return DailyTitleText(
isoDate: title,
multiselectEnabled: widget.selectionActive,
onSelect: () => _selectAssets(assets),
onDeselect: () => _deselectAssets(assets),
selected: _allAssetsSelected(assets),
);
}
Widget _buildMonthTitle(BuildContext context, String title) {
var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
.format(DateTime.parse(title));
return Padding(
key: Key("month-$title"),
padding: const EdgeInsets.only(left: 12.0, top: 32),
child: Text(
monthTitleText,
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.headline1?.color,
),
),
);
}
Widget _itemBuilder(BuildContext c, int position) {
final item = widget.renderList[position];
if (item.type == RenderAssetGridElementType.dayTitle) {
return _buildTitle(c, item.title!, item.relatedAssetList!);
} else if (item.type == RenderAssetGridElementType.monthTitle) {
return _buildMonthTitle(c, item.title!);
} else if (item.type == RenderAssetGridElementType.assetRow) {
return _buildAssetRow(c, item.assetRow!, _scrolling);
}
return const Text("Invalid widget type!");
}
Text _labelBuilder(int pos) {
final date = widget.renderList[pos].date;
return Text(
DateFormat.yMMMd().format(date),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
);
}
Widget _buildMultiSelectIndicator() {
return DisableMultiSelectButton(
onPressed: () => _deselectAll(),
selectedItemCount: _selectedAssets.length,
);
}
Widget _buildAssetGrid() {
final useDragScrolling = _assets.length >= 20;
void dragScrolling(bool active) {
setState(() {
_scrolling = active;
});
}
final listWidget = ScrollablePositionedList.builder(
itemBuilder: _itemBuilder,
itemPositionsListener: _itemPositionsListener,
itemScrollController: _itemScrollController,
itemCount: widget.renderList.length,
);
if (!useDragScrolling) {
return listWidget;
}
return DraggableScrollbar.semicircle(
scrollStateListener: dragScrolling,
itemPositionsListener: _itemPositionsListener,
controller: _itemScrollController,
backgroundColor: Theme.of(context).hintColor,
labelTextBuilder: _labelBuilder,
labelConstraints: const BoxConstraints(maxHeight: 28),
scrollbarAnimationDuration: const Duration(seconds: 1),
scrollbarTimeToFade: const Duration(seconds: 4),
child: listWidget,
);
}
@override
void didUpdateWidget(ImmichAssetGrid oldWidget) {
super.didUpdateWidget(oldWidget);
if (!widget.selectionActive) {
setState(() {
_selectedAssets.clear();
});
}
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
_buildAssetGrid(),
if (widget.selectionActive) _buildMultiSelectIndicator(),
],
);
}
}
class ImmichAssetGrid extends StatefulWidget {
final List<RenderAssetGridElement> renderList;
final int assetsPerRow;
final double margin;
final bool showStorageIndicator;
final ImmichAssetGridSelectionListener? listener;
final bool selectionActive;
const ImmichAssetGrid({
super.key,
required this.renderList,
required this.assetsPerRow,
required this.showStorageIndicator,
this.listener,
this.margin = 5.0,
this.selectionActive = false,
});
@override
State<StatefulWidget> createState() {
return ImmichAssetGridState();
}
}

View File

@@ -1,176 +1,172 @@
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class ThumbnailImage extends HookConsumerWidget {
final AssetResponseDto asset;
final List<AssetResponseDto> assetList;
final bool showStorageIndicator;
final bool useGrayBoxPlaceholder;
const ThumbnailImage({
Key? key,
required this.asset,
required this.assetList,
this.showStorageIndicator = true,
this.useGrayBoxPlaceholder = false,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = getThumbnailUrl(asset);
var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable;
var deviceId = ref.watch(authenticationProvider).deviceId;
Widget buildSelectionIcon(AssetResponseDto asset) {
if (selectedAsset.contains(asset)) {
return Icon(
Icons.check_circle,
color: Theme.of(context).primaryColor,
);
} else {
return const Icon(
Icons.circle_outlined,
color: Colors.white,
);
}
}
return GestureDetector(
onTap: () {
if (isMultiSelectEnable &&
selectedAsset.contains(asset) &&
selectedAsset.length == 1) {
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
} else if (isMultiSelectEnable &&
selectedAsset.contains(asset) &&
selectedAsset.length > 1) {
ref
.watch(homePageStateProvider.notifier)
.removeSingleSelectedItem(asset);
} else if (isMultiSelectEnable && !selectedAsset.contains(asset)) {
ref
.watch(homePageStateProvider.notifier)
.addSingleSelectedItem(asset);
} else {
AutoRouter.of(context).push(
GalleryViewerRoute(
assetList: assetList,
asset: asset,
),
);
}
},
onLongPress: () {
// Enable multi select function
ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset});
HapticFeedback.heavyImpact();
},
child: Hero(
tag: asset.id,
child: Stack(
children: [
Container(
decoration: BoxDecoration(
border: isMultiSelectEnable && selectedAsset.contains(asset)
? Border.all(
color: Theme.of(context).primaryColorLight,
width: 10,
)
: const Border(),
),
child: CachedNetworkImage(
cacheKey: 'thumbnail-image-${asset.id}',
width: 300,
height: 300,
memCacheHeight: 200,
maxWidthDiskCache: 200,
maxHeightDiskCache: 200,
fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl,
httpHeaders: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
fadeInDuration: const Duration(milliseconds: 250),
progressIndicatorBuilder: (context, url, downloadProgress) {
if (useGrayBoxPlaceholder) {
return const DecoratedBox(
decoration: BoxDecoration(color: Colors.grey),
);
}
return Transform.scale(
scale: 0.2,
child: CircularProgressIndicator(
value: downloadProgress.progress,
),
);
},
errorWidget: (context, url, error) {
debugPrint("Error getting thumbnail $url = $error");
CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
return Icon(
Icons.image_not_supported_outlined,
color: Theme.of(context).primaryColor,
);
},
),
),
if (isMultiSelectEnable)
Padding(
padding: const EdgeInsets.all(3.0),
child: Align(
alignment: Alignment.topLeft,
child: buildSelectionIcon(asset),
),
),
if (showStorageIndicator)
Positioned(
right: 10,
bottom: 5,
child: Icon(
(deviceId != asset.deviceId)
? Icons.cloud_done_outlined
: Icons.photo_library_rounded,
color: Colors.white,
size: 18,
),
),
if (asset.type != AssetTypeEnum.IMAGE)
Positioned(
top: 5,
right: 5,
child: Row(
children: [
Text(
asset.duration.toString().substring(0, 7),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
),
),
],
),
),
);
}
}
import 'package:auto_route/auto_route.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class ThumbnailImage extends HookConsumerWidget {
final AssetResponseDto asset;
final List<AssetResponseDto> assetList;
final bool showStorageIndicator;
final bool useGrayBoxPlaceholder;
final bool isSelected;
final bool multiselectEnabled;
final Function? onSelect;
final Function? onDeselect;
const ThumbnailImage({
Key? key,
required this.asset,
required this.assetList,
this.showStorageIndicator = true,
this.useGrayBoxPlaceholder = false,
this.isSelected = false,
this.multiselectEnabled = false,
this.onDeselect,
this.onSelect,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = getThumbnailUrl(asset);
var deviceId = ref.watch(authenticationProvider).deviceId;
Widget buildSelectionIcon(AssetResponseDto asset) {
if (isSelected) {
return Icon(
Icons.check_circle,
color: Theme.of(context).primaryColor,
);
} else {
return const Icon(
Icons.circle_outlined,
color: Colors.white,
);
}
}
return GestureDetector(
onTap: () {
if (multiselectEnabled) {
if (isSelected) {
onDeselect?.call();
} else {
onSelect?.call();
}
} else {
AutoRouter.of(context).push(
GalleryViewerRoute(
assetList: assetList,
asset: asset,
),
);
}
},
onLongPress: () {
onSelect?.call();
HapticFeedback.heavyImpact();
},
child: Hero(
tag: asset.id,
child: Stack(
children: [
Container(
decoration: BoxDecoration(
border: multiselectEnabled && isSelected
? Border.all(
color: Theme.of(context).primaryColorLight,
width: 10,
)
: const Border(),
),
child: CachedNetworkImage(
cacheKey: 'thumbnail-image-${asset.id}',
width: 300,
height: 300,
memCacheHeight: 200,
maxWidthDiskCache: 200,
maxHeightDiskCache: 200,
fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl,
httpHeaders: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
fadeInDuration: const Duration(milliseconds: 250),
progressIndicatorBuilder: (context, url, downloadProgress) {
if (useGrayBoxPlaceholder) {
return const DecoratedBox(
decoration: BoxDecoration(color: Colors.grey),
);
}
return Transform.scale(
scale: 0.2,
child: CircularProgressIndicator(
value: downloadProgress.progress,
),
);
},
errorWidget: (context, url, error) {
debugPrint("Error getting thumbnail $url = $error");
CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
return Icon(
Icons.image_not_supported_outlined,
color: Theme.of(context).primaryColor,
);
},
),
),
if (multiselectEnabled)
Padding(
padding: const EdgeInsets.all(3.0),
child: Align(
alignment: Alignment.topLeft,
child: buildSelectionIcon(asset),
),
),
if (showStorageIndicator)
Positioned(
right: 10,
bottom: 5,
child: Icon(
(deviceId != asset.deviceId)
? Icons.cloud_done_outlined
: Icons.photo_library_rounded,
color: Colors.white,
size: 18,
),
),
if (asset.type != AssetTypeEnum.IMAGE)
Positioned(
top: 5,
right: 5,
child: Row(
children: [
Text(
asset.duration.toString().substring(0, 7),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
),
),
],
),
),
);
}
}

View File

@@ -1,107 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
import 'package:openapi/api.dart';
class DailyTitleText extends ConsumerWidget {
const DailyTitleText({
Key? key,
required this.isoDate,
required this.assetGroup,
}) : super(key: key);
final String isoDate;
final List<AssetResponseDto> assetGroup;
@override
Widget build(BuildContext context, WidgetRef ref) {
var currentYear = DateTime.now().year;
var groupYear = DateTime.parse(isoDate).year;
var formatDateTemplate = currentYear == groupYear
? "daily_title_text_date".tr()
: "daily_title_text_date_year".tr();
var dateText =
DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable;
var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
var selectedItems = ref.watch(homePageStateProvider).selectedItems;
void _handleTitleIconClick() {
if (isMultiSelectEnable &&
selectedDateGroup.contains(dateText) &&
selectedDateGroup.length == 1 &&
selectedItems.length <= assetGroup.length) {
// Multi select is active - click again on the icon while it is the only active group -> disable multi select
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
} else if (isMultiSelectEnable &&
selectedDateGroup.contains(dateText) &&
selectedItems.length != assetGroup.length) {
// Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items
ref
.watch(homePageStateProvider.notifier)
.removeSelectedDateGroup(dateText);
ref
.watch(homePageStateProvider.notifier)
.removeMultipleSelectedItem(assetGroup);
} else if (isMultiSelectEnable &&
selectedDateGroup.contains(dateText) &&
selectedDateGroup.length > 1) {
ref
.watch(homePageStateProvider.notifier)
.removeSelectedDateGroup(dateText);
ref
.watch(homePageStateProvider.notifier)
.removeMultipleSelectedItem(assetGroup);
} else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
ref
.watch(homePageStateProvider.notifier)
.addSelectedDateGroup(dateText);
ref
.watch(homePageStateProvider.notifier)
.addMultipleSelectedItems(assetGroup);
} else {
ref
.watch(homePageStateProvider.notifier)
.enableMultiSelect(assetGroup.toSet());
ref
.watch(homePageStateProvider.notifier)
.addSelectedDateGroup(dateText);
}
}
return Padding(
padding: const EdgeInsets.only(
top: 29.0,
bottom: 29.0,
left: 12.0,
right: 12.0,
),
child: Row(
children: [
Text(
dateText,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
GestureDetector(
onTap: _handleTitleIconClick,
child: isMultiSelectEnable && selectedDateGroup.contains(dateText)
? Icon(
Icons.check_circle_rounded,
color: Theme.of(context).primaryColor,
)
: const Icon(
Icons.check_circle_outline_rounded,
color: Colors.grey,
),
)
],
),
);
}
}

View File

@@ -1,167 +0,0 @@
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_list_v2/daily_title_text.dart';
import 'package:immich_mobile/modules/home/ui/asset_list_v2/draggable_scrollbar_custom.dart';
import 'package:openapi/api.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import '../thumbnail_image.dart';
class ImmichAssetGrid extends HookConsumerWidget {
final ItemScrollController _itemScrollController = ItemScrollController();
final ItemPositionsListener _itemPositionsListener =
ItemPositionsListener.create();
final List<RenderAssetGridElement> renderList;
final int assetsPerRow;
final double margin;
final bool showStorageIndicator;
ImmichAssetGrid({
super.key,
required this.renderList,
required this.assetsPerRow,
required this.showStorageIndicator,
this.margin = 5.0,
});
List<AssetResponseDto> get _assets {
return renderList
.map((e) {
if (e.type == RenderAssetGridElementType.assetRow) {
return e.assetRow!.assets;
} else {
return List<AssetResponseDto>.empty();
}
})
.flattened
.toList();
}
double _getItemSize(BuildContext context) {
return MediaQuery.of(context).size.width / assetsPerRow -
margin * (assetsPerRow - 1) / assetsPerRow;
}
Widget _buildThumbnailOrPlaceholder(
AssetResponseDto asset, bool placeholder) {
if (placeholder) {
return const DecoratedBox(
decoration: BoxDecoration(color: Colors.grey),
);
}
return ThumbnailImage(
asset: asset,
assetList: _assets,
showStorageIndicator: showStorageIndicator,
useGrayBoxPlaceholder: true,
);
}
Widget _buildAssetRow(
BuildContext context, RenderAssetGridRow row, bool scrolling) {
double size = _getItemSize(context);
return Row(
key: Key("asset-row-${row.assets.first.id}"),
children: row.assets.map((AssetResponseDto asset) {
bool last = asset == row.assets.last;
return Container(
key: Key("asset-${asset.id}"),
width: size,
height: size,
margin: EdgeInsets.only(top: margin, right: last ? 0.0 : margin),
child: _buildThumbnailOrPlaceholder(asset, scrolling),
);
}).toList(),
);
}
Widget _buildTitle(
BuildContext context, String title, List<AssetResponseDto> assets) {
return DailyTitleText(
isoDate: title,
assetGroup: assets,
);
}
Widget _buildMonthTitle(BuildContext context, String title) {
var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
.format(DateTime.parse(title));
return Padding(
key: Key("month-$title"),
padding: const EdgeInsets.only(left: 12.0, top: 32),
child: Text(
monthTitleText,
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.headline1?.color,
),
),
);
}
Widget _itemBuilder(BuildContext c, int position, bool scrolling) {
final item = renderList[position];
if (item.type == RenderAssetGridElementType.dayTitle) {
return _buildTitle(c, item.title!, item.relatedAssetList!);
} else if (item.type == RenderAssetGridElementType.monthTitle) {
return _buildMonthTitle(c, item.title!);
} else if (item.type == RenderAssetGridElementType.assetRow) {
return _buildAssetRow(c, item.assetRow!, scrolling);
}
return const Text("Invalid widget type!");
}
Text _labelBuilder(int pos) {
final date = renderList[pos].date;
return Text(DateFormat.yMMMd().format(date),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final scrolling = useState(false);
void dragScrolling(bool active) {
scrolling.value = active;
}
Widget itemBuilder(BuildContext c, int position) {
return _itemBuilder(c, position, scrolling.value);
}
return DraggableScrollbar.semicircle(
scrollStateListener: dragScrolling,
itemPositionsListener: _itemPositionsListener,
controller: _itemScrollController,
backgroundColor: Theme.of(context).hintColor,
labelTextBuilder: _labelBuilder,
labelConstraints: const BoxConstraints(maxHeight: 28),
scrollbarAnimationDuration: const Duration(seconds: 1),
scrollbarTimeToFade: const Duration(seconds: 4),
child: ScrollablePositionedList.builder(
itemBuilder: itemBuilder,
itemPositionsListener: _itemPositionsListener,
itemScrollController: _itemScrollController,
itemCount: renderList.length,
));
}
}

View File

@@ -1,11 +1,15 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
class ControlBottomAppBar extends ConsumerWidget {
const ControlBottomAppBar({Key? key}) : super(key: key);
final Function onShare;
final Function onDelete;
const ControlBottomAppBar(
{Key? key, required this.onShare, required this.onDelete})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -36,7 +40,9 @@ class ControlBottomAppBar extends ConsumerWidget {
showDialog(
context: context,
builder: (BuildContext context) {
return const DeleteDialog();
return DeleteDialog(
onDelete: onDelete,
);
},
);
},
@@ -45,14 +51,7 @@ class ControlBottomAppBar extends ConsumerWidget {
iconData: Icons.share,
label: "control_bottom_app_bar_share".tr(),
onPressed: () {
final homePageState = ref.watch(homePageStateProvider);
ref.watch(homePageStateProvider.notifier).shareAssets(
homePageState.selectedItems.toList(),
context,
);
ref
.watch(homePageStateProvider.notifier)
.disableMultiSelect();
onShare();
},
),
],

View File

@@ -1,109 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
import 'package:openapi/api.dart';
class DailyTitleText extends ConsumerWidget {
const DailyTitleText({
Key? key,
required this.isoDate,
required this.assetGroup,
}) : super(key: key);
final String isoDate;
final List<AssetResponseDto> assetGroup;
@override
Widget build(BuildContext context, WidgetRef ref) {
var currentYear = DateTime.now().year;
var groupYear = DateTime.parse(isoDate).year;
var formatDateTemplate = currentYear == groupYear
? "daily_title_text_date".tr()
: "daily_title_text_date_year".tr();
var dateText = DateFormat(formatDateTemplate)
.format(DateTime.parse(isoDate).toLocal());
var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable;
var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
var selectedItems = ref.watch(homePageStateProvider).selectedItems;
void _handleTitleIconClick() {
if (isMultiSelectEnable &&
selectedDateGroup.contains(dateText) &&
selectedDateGroup.length == 1 &&
selectedItems.length <= assetGroup.length) {
// Multi select is active - click again on the icon while it is the only active group -> disable multi select
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
} else if (isMultiSelectEnable &&
selectedDateGroup.contains(dateText) &&
selectedItems.length != assetGroup.length) {
// Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items
ref
.watch(homePageStateProvider.notifier)
.removeSelectedDateGroup(dateText);
ref
.watch(homePageStateProvider.notifier)
.removeMultipleSelectedItem(assetGroup);
} else if (isMultiSelectEnable &&
selectedDateGroup.contains(dateText) &&
selectedDateGroup.length > 1) {
ref
.watch(homePageStateProvider.notifier)
.removeSelectedDateGroup(dateText);
ref
.watch(homePageStateProvider.notifier)
.removeMultipleSelectedItem(assetGroup);
} else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
ref
.watch(homePageStateProvider.notifier)
.addSelectedDateGroup(dateText);
ref
.watch(homePageStateProvider.notifier)
.addMultipleSelectedItems(assetGroup);
} else {
ref
.watch(homePageStateProvider.notifier)
.enableMultiSelect(assetGroup.toSet());
ref
.watch(homePageStateProvider.notifier)
.addSelectedDateGroup(dateText);
}
}
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(
top: 29.0,
bottom: 29.0,
left: 12.0,
right: 12.0,
),
child: Row(
children: [
Text(
dateText,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
GestureDetector(
onTap: _handleTitleIconClick,
child: isMultiSelectEnable && selectedDateGroup.contains(dateText)
? Icon(
Icons.check_circle_rounded,
color: Theme.of(context).primaryColor,
)
: const Icon(
Icons.check_circle_outline_rounded,
color: Colors.grey,
),
)
],
),
),
);
}
}

View File

@@ -1,18 +1,17 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
class DeleteDialog extends ConsumerWidget {
const DeleteDialog({Key? key}) : super(key: key);
final Function onDelete;
const DeleteDialog({Key? key, required this.onDelete}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final homePageState = ref.watch(homePageStateProvider);
return AlertDialog(
backgroundColor: Colors.grey[200],
// backgroundColor: Colors.grey[200],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
title: const Text("delete_dialog_title").tr(),
content: const Text("delete_dialog_alert").tr(),
@@ -21,23 +20,25 @@ class DeleteDialog extends ConsumerWidget {
onPressed: () {
Navigator.of(context).pop();
},
child: const Text(
child: Text(
"delete_dialog_cancel",
style: TextStyle(color: Colors.blueGrey),
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
).tr(),
),
TextButton(
onPressed: () {
ref
.watch(assetProvider.notifier)
.deleteAssets(homePageState.selectedItems);
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
onDelete();
Navigator.of(context).pop();
},
child: Text(
"delete_dialog_ok",
style: TextStyle(color: Colors.red[400]),
style: TextStyle(
color: Colors.red[400],
fontWeight: FontWeight.bold,
),
).tr(),
),
],

View File

@@ -1,47 +0,0 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
import 'package:openapi/api.dart';
// ignore: must_be_immutable
class ImageGrid extends ConsumerWidget {
final List<AssetResponseDto> assetGroup;
final List<AssetResponseDto> sortedAssetGroup;
final int tilesPerRow;
final bool showStorageIndicator;
ImageGrid({
Key? key,
required this.assetGroup,
required this.sortedAssetGroup,
this.tilesPerRow = 4,
this.showStorageIndicator = true,
}) : super(key: key);
List<AssetResponseDto> imageSortedList = [];
@override
Widget build(BuildContext context, WidgetRef ref) {
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: tilesPerRow,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
var assetType = assetGroup[index].type;
return GestureDetector(
onTap: () {},
child: ThumbnailImage(
asset: assetGroup[index],
assetList: sortedAssetGroup,
showStorageIndicator: showStorageIndicator,
),
);
},
childCount: assetGroup.length,
),
);
}
}

View File

@@ -2,22 +2,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
import 'package:immich_mobile/modules/home/ui/disable_multi_select_button.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
import 'package:immich_mobile/modules/home/ui/asset_list_v2/immich_asset_grid.dart';
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/services/share.service.dart';
import 'package:openapi/api.dart';
class HomePage extends HookConsumerWidget {
@@ -26,22 +21,9 @@ class HomePage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
var renderList = ref.watch(renderListProvider);
ScrollController scrollController = useScrollController();
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
List<Widget> imageGridGroup = [];
var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable;
var homePageState = ref.watch(homePageStateProvider);
List<AssetResponseDto> sortedAssetList = [];
// set sorted List
for (var group in assetGroupByDateTime.values) {
for (var value in group) {
sortedAssetList.add(value);
}
}
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
final selection = useState(<AssetResponseDto>{});
useEffect(
() {
@@ -57,115 +39,61 @@ class HomePage extends HookConsumerWidget {
ref.read(assetProvider.notifier).getAllAsset();
}
_buildSelectedItemCountIndicator() {
return DisableMultiSelectButton(
onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
selectedItemCount: homePageState.selectedItems.length,
);
}
Widget _buildBody() {
if (assetGroupByDateTime.isNotEmpty) {
int? lastMonth;
assetGroupByDateTime.forEach((dateGroup, immichAssetList) {
try {
DateTime parseDateGroup = DateTime.parse(dateGroup);
int currentMonth = parseDateGroup.month;
if (lastMonth != null) {
if (currentMonth - lastMonth! != 0) {
imageGridGroup.add(
MonthlyTitleText(
isoDate: dateGroup,
),
);
}
}
imageGridGroup.add(
DailyTitleText(
key: Key('${dateGroup.toString()}title'),
isoDate: dateGroup,
assetGroup: immichAssetList,
),
);
imageGridGroup.add(
ImageGrid(
assetGroup: immichAssetList,
sortedAssetGroup: sortedAssetList,
tilesPerRow:
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
showStorageIndicator: appSettingService
.getSetting(AppSettingsEnum.storageIndicator),
),
);
lastMonth = currentMonth;
} catch (e) {
debugPrint(
"[ERROR] Cannot parse $dateGroup - Wrong create date format : ${immichAssetList.map((asset) => asset.createdAt).toList()}",
);
}
});
Widget buildBody() {
void selectionListener(
bool multiselect,
Set<AssetResponseDto> selectedAssets,
) {
multiselectEnabled.state = multiselect;
selection.value = selectedAssets;
}
_buildSliverAppBar() {
return isMultiSelectEnable
? const SliverToBoxAdapter(
child: SizedBox(
height: 70,
child: null,
),
)
: ImmichSliverAppBar(
onPopBack: reloadAllAsset,
);
void onShareAssets() {
ref.watch(shareServiceProvider).shareAssets(selection.value.toList());
multiselectEnabled.state = false;
}
_buildAssetGrid() {
if (appSettingService
.getSetting(AppSettingsEnum.useExperimentalAssetGrid)) {
return ImmichAssetGrid(
renderList: renderList,
assetsPerRow:
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
showStorageIndicator: appSettingService
.getSetting(AppSettingsEnum.storageIndicator),
);
} else {
return DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).hintColor,
controller: scrollController,
heightScrollThumb: 48.0,
child: CustomScrollView(
controller: scrollController,
slivers: [
...imageGridGroup,
],
),
);
}
void onDelete() {
ref.watch(assetProvider.notifier).deleteAssets(selection.value);
multiselectEnabled.state = false;
}
return SafeArea(
bottom: !isMultiSelectEnable,
top: !isMultiSelectEnable,
bottom: !multiselectEnabled.state,
top: !multiselectEnabled.state,
child: Stack(
children: [
CustomScrollView(
slivers: [
_buildSliverAppBar(),
multiselectEnabled.state
? const SliverToBoxAdapter(
child: SizedBox(
height: 70,
child: null,
),
)
: ImmichSliverAppBar(
onPopBack: reloadAllAsset,
),
],
),
Padding(
padding: const EdgeInsets.only(top: 60.0, bottom: 0.0),
child: _buildAssetGrid(),
child: ImmichAssetGrid(
renderList: renderList,
assetsPerRow:
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
showStorageIndicator: appSettingService
.getSetting(AppSettingsEnum.storageIndicator),
listener: selectionListener,
selectionActive: multiselectEnabled.state,
),
),
if (isMultiSelectEnable) ...[
_buildSelectedItemCountIndicator(),
const ControlBottomAppBar(),
if (multiselectEnabled.state) ...[
ControlBottomAppBar(
onShare: onShareAssets,
onDelete: onDelete,
),
],
],
),
@@ -174,7 +102,7 @@ class HomePage extends HookConsumerWidget {
return Scaffold(
drawer: const ProfileDrawer(),
body: _buildBody(),
body: buildBody(),
);
}
}

View File

@@ -1,7 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
@@ -15,6 +18,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
this._deviceInfoService,
this._backupService,
this._apiService,
this._assetCacheService,
this._albumCacheService,
this._sharedAlbumCacheService,
) : super(
AuthenticationState(
deviceId: "",
@@ -41,6 +47,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
final DeviceInfoService _deviceInfoService;
final BackupService _backupService;
final ApiService _apiService;
final AssetCacheService _assetCacheService;
final AlbumCacheService _albumCacheService;
final SharedAlbumCacheService _sharedAlbumCacheService;
Future<bool> login(
String email,
@@ -120,6 +129,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
.delete(savedLoginInfoKey);
}
} catch (e) {
HapticFeedback.vibrate();
debugPrint("Error logging in $e");
return false;
}
@@ -151,7 +161,23 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Future<bool> logout() async {
Hive.box(userInfoBox).delete(accessTokenKey);
state = state.copyWith(isAuthenticated: false);
_assetCacheService.invalidate();
_albumCacheService.invalidate();
_sharedAlbumCacheService.invalidate();
// Remove login info from local storage
var loginInfo =
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
if (loginInfo != null) {
loginInfo.email = "";
loginInfo.password = "";
loginInfo.isSaveLogin = false;
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put(
savedLoginInfoKey,
loginInfo,
);
}
return true;
}
@@ -197,5 +223,8 @@ final authenticationProvider =
ref.watch(deviceInfoServiceProvider),
ref.watch(backupServiceProvider),
ref.watch(apiServiceProvider),
ref.watch(assetCacheServiceProvider),
ref.watch(albumCacheServiceProvider),
ref.watch(sharedAlbumCacheServiceProvider),
);
});

View File

@@ -142,8 +142,8 @@ class ChangePasswordButton extends ConsumerWidget {
return ElevatedButton(
style: ElevatedButton.styleFrom(
visualDensity: VisualDensity.standard,
primary: Theme.of(context).primaryColor,
onPrimary: Colors.grey[50],
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.grey[50],
elevation: 2,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
),

View File

@@ -203,8 +203,8 @@ class LoginButton extends ConsumerWidget {
return ElevatedButton(
style: ElevatedButton.styleFrom(
visualDensity: VisualDensity.standard,
primary: Theme.of(context).primaryColor,
onPrimary: Colors.grey[50],
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.grey[50],
elevation: 2,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
),
@@ -228,7 +228,7 @@ class LoginButton extends ConsumerWidget {
AutoRouter.of(context).push(const ChangePasswordRoute());
} else {
ref.watch(backupProvider.notifier).resumeBackup();
AutoRouter.of(context).pushNamed("/tab-controller-page");
AutoRouter.of(context).replace(const TabControllerRoute());
}
} else {
ImmichToast.show(

View File

@@ -1,8 +1,11 @@
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
import 'package:immich_mobile/modules/search/services/search.service.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:intl/intl.dart';
import 'package:openapi/api.dart';
@@ -66,3 +69,12 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) {
.format(DateTime.parse(element.createdAt).toLocal()),
);
});
final searchRenderListProvider = StateProvider((ref) {
var assetGroups = ref.watch(searchResultGroupByDateTimeProvider);
var settings = ref.watch(appSettingsServiceProvider);
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
return assetGroupsToRenderList(assetGroups, assetsPerRow);
});

View File

@@ -4,14 +4,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart';
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
class SearchResultPage extends HookConsumerWidget {
const SearchResultPage({Key? key, required this.searchTerm})
@@ -21,17 +19,12 @@ class SearchResultPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
ScrollController scrollController = useScrollController();
final searchTermController = useTextEditingController(text: "");
final isNewSearch = useState(false);
final currentSearchTerm = useState(searchTerm);
final List<Widget> imageGridGroup = [];
FocusNode? searchFocusNode;
List<AssetResponseDto> sortedAssetList = [];
useEffect(
() {
searchFocusNode = FocusNode();
@@ -117,7 +110,12 @@ class SearchResultPage extends HookConsumerWidget {
_buildSearchResult() {
var searchResultPageState = ref.watch(searchResultPageProvider);
var assetGroupByDateTime = ref.watch(searchResultGroupByDateTimeProvider);
var searchResultRenderList = ref.watch(searchRenderListProvider);
var settings = ref.watch(appSettingsServiceProvider);
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
final showStorageIndicator =
settings.getSetting(AppSettingsEnum.storageIndicator);
if (searchResultPageState.isError) {
return const Text("Error");
@@ -132,57 +130,11 @@ class SearchResultPage extends HookConsumerWidget {
}
if (searchResultPageState.isSuccess) {
if (searchResultPageState.searchResult.isNotEmpty) {
int? lastMonth;
// set sorted List
for (var group in assetGroupByDateTime.values) {
for (var value in group) {
sortedAssetList.add(value);
}
}
assetGroupByDateTime.forEach((dateGroup, immichAssetList) {
DateTime parseDateGroup = DateTime.parse(dateGroup);
int currentMonth = parseDateGroup.month;
if (lastMonth != null) {
if (currentMonth - lastMonth! != 0) {
imageGridGroup.add(
MonthlyTitleText(
isoDate: dateGroup,
),
);
}
}
imageGridGroup.add(
DailyTitleText(
isoDate: dateGroup,
assetGroup: immichAssetList,
),
);
imageGridGroup.add(
ImageGrid(
assetGroup: immichAssetList,
sortedAssetGroup: sortedAssetList,
),
);
lastMonth = currentMonth;
});
return DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).hintColor,
controller: scrollController,
heightScrollThumb: 48.0,
child: CustomScrollView(
controller: scrollController,
slivers: [...imageGridGroup],
),
);
} else {
return const Text("No assets found");
}
return ImmichAssetGrid(
renderList: searchResultRenderList,
assetsPerRow: assetsPerRow,
showStorageIndicator: showStorageIndicator,
);
}
return const SizedBox();

View File

@@ -6,7 +6,11 @@ enum AppSettingsEnum<T> {
themeMode<String>("themeMode", "system"), // "light","dark","system"
tilesPerRow<int>("tilesPerRow", 4),
uploadErrorNotificationGracePeriod<int>(
"uploadErrorNotificationGracePeriod", 2),
"uploadErrorNotificationGracePeriod",
2,
),
backgroundBackupTotalProgress<bool>("backgroundBackupTotalProgress", true),
backgroundBackupSingleProgress<bool>("backgroundBackupSingleProgress", false),
storageIndicator<bool>("storageIndicator", true),
thumbnailCacheSize<int>("thumbnailCacheSize", 10000),
imageCacheSize<int>("imageCacheSize", 350),

View File

@@ -1,11 +1,6 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class ExperimentalSettings extends HookConsumerWidget {
const ExperimentalSettings({
@@ -14,33 +9,6 @@ class ExperimentalSettings extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final useExperimentalAssetGrid = useState(false);
useEffect(
() {
useExperimentalAssetGrid.value = appSettingService
.getSetting(AppSettingsEnum.useExperimentalAssetGrid);
return null;
},
[],
);
void changeUseExperimentalAssetGrid(bool status) {
useExperimentalAssetGrid.value = status;
appSettingService.setSetting(
AppSettingsEnum.useExperimentalAssetGrid,
status,
);
ImmichToast.show(
context: context,
msg: "settings_require_restart".tr(),
gravity: ToastGravity.BOTTOM,
);
}
return ExpansionTile(
textColor: Theme.of(context).primaryColor,
title: const Text(
@@ -55,25 +23,25 @@ class ExperimentalSettings extends HookConsumerWidget {
fontSize: 13,
),
).tr(),
children: [
SwitchListTile.adaptive(
activeColor: Theme.of(context).primaryColor,
title: const Text(
"experimental_settings_new_asset_list_title",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
).tr(),
subtitle: const Text(
"experimental_settings_new_asset_list_subtitle",
style: TextStyle(
fontSize: 12,
),
).tr(),
value: useExperimentalAssetGrid.value,
onChanged: changeUseExperimentalAssetGrid,
),
children: const [
// SwitchListTile.adaptive(
// activeColor: Theme.of(context).primaryColor,
// title: const Text(
// "experimental_settings_new_asset_list_title",
// style: TextStyle(
// fontSize: 12,
// fontWeight: FontWeight.bold,
// ),
// ).tr(),
// subtitle: const Text(
// "experimental_settings_new_asset_list_subtitle",
// style: TextStyle(
// fontSize: 12,
// ),
// ).tr(),
// value: useExperimentalAssetGrid.value,
// onChanged: changeUseExperimentalAssetGrid,
// ),
],
);
}

View File

@@ -15,12 +15,20 @@ class NotificationSetting extends HookConsumerWidget {
final appSettingService = ref.watch(appSettingsServiceProvider);
final sliderValue = useState(0.0);
final totalProgressValue =
useState(AppSettingsEnum.backgroundBackupTotalProgress.defaultValue);
final singleProgressValue =
useState(AppSettingsEnum.backgroundBackupSingleProgress.defaultValue);
useEffect(
() {
sliderValue.value = appSettingService
.getSetting<int>(AppSettingsEnum.uploadErrorNotificationGracePeriod)
.toDouble();
totalProgressValue.value = appSettingService
.getSetting<bool>(AppSettingsEnum.backgroundBackupTotalProgress);
singleProgressValue.value = appSettingService
.getSetting<bool>(AppSettingsEnum.backgroundBackupSingleProgress);
return null;
},
[],
@@ -42,6 +50,22 @@ class NotificationSetting extends HookConsumerWidget {
),
).tr(),
children: [
_buildSwitchListTile(
context,
appSettingService,
totalProgressValue,
AppSettingsEnum.backgroundBackupTotalProgress,
title: 'setting_notifications_total_progress_title'.tr(),
subtitle: 'setting_notifications_total_progress_subtitle'.tr(),
),
_buildSwitchListTile(
context,
appSettingService,
singleProgressValue,
AppSettingsEnum.backgroundBackupSingleProgress,
title: 'setting_notifications_single_progress_title'.tr(),
subtitle: 'setting_notifications_single_progress_subtitle'.tr(),
),
ListTile(
isThreeLine: false,
dense: true,
@@ -53,7 +77,9 @@ class NotificationSetting extends HookConsumerWidget {
value: sliderValue.value,
onChanged: (double v) => sliderValue.value = v,
onChangeEnd: (double v) => appSettingService.setSetting(
AppSettingsEnum.uploadErrorNotificationGracePeriod, v.toInt()),
AppSettingsEnum.uploadErrorNotificationGracePeriod,
v.toInt(),
),
max: 5.0,
divisions: 5,
label: formattedValue,
@@ -65,6 +91,28 @@ class NotificationSetting extends HookConsumerWidget {
}
}
SwitchListTile _buildSwitchListTile(
BuildContext context,
AppSettingsService appSettingService,
ValueNotifier<bool> valueNotifier,
AppSettingsEnum settingsEnum, {
required String title,
String? subtitle,
}) {
return SwitchListTile(
key: Key(settingsEnum.name),
value: valueNotifier.value,
onChanged: (value) {
valueNotifier.value = value;
appSettingService.setSetting(settingsEnum, value);
},
activeColor: Theme.of(context).primaryColor,
dense: true,
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: subtitle != null ? Text(subtitle) : null,
);
}
String _formatSliderValue(double v) {
if (v == 0.0) {
return 'setting_notifications_notify_immediately'.tr();

View File

@@ -43,7 +43,7 @@ class SettingsPage extends HookConsumerWidget {
const ThemeSetting(),
const AssetListSettings(),
if (Platform.isAndroid) const NotificationSetting(),
const ExperimentalSettings(),
//const ExperimentalSettings(),
],
).toList(),
],

View File

@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
import 'package:immich_mobile/shared/services/device_info.service.dart';
import 'package:collection/collection.dart';
import 'package:intl/intl.dart';
@@ -9,24 +10,50 @@ import 'package:photo_manager/photo_manager.dart';
class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
final AssetService _assetService;
final AssetCacheService _assetCacheService;
final DeviceInfoService _deviceInfoService = DeviceInfoService();
AssetNotifier(this._assetService) : super([]);
AssetNotifier(this._assetService, this._assetCacheService) : super([]);
_cacheState() {
_assetCacheService.put(state);
}
getAllAsset() async {
final stopwatch = Stopwatch();
if (await _assetCacheService.isValid() && state.isEmpty) {
stopwatch.start();
state = await _assetCacheService.get();
debugPrint("Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms");
stopwatch.reset();
}
stopwatch.start();
var allAssets = await _assetService.getAllAsset();
debugPrint("Query assets from API: ${stopwatch.elapsedMilliseconds}ms");
stopwatch.reset();
if (allAssets != null) {
state = allAssets;
stopwatch.start();
_cacheState();
debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
stopwatch.reset();
}
}
clearAllAsset() {
state = [];
_cacheState();
}
onNewAssetUploaded(AssetResponseDto newAsset) {
state = [...state, newAsset];
_cacheState();
}
deleteAssets(Set<AssetResponseDto> deleteAssets) async {
@@ -65,12 +92,15 @@ class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
state.where((immichAsset) => immichAsset.id != asset.id).toList();
}
}
_cacheState();
}
}
final assetProvider =
StateNotifierProvider<AssetNotifier, List<AssetResponseDto>>((ref) {
return AssetNotifier(ref.watch(assetServiceProvider));
return AssetNotifier(
ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider));
});
final assetGroupByDateTimeProvider = StateProvider((ref) {

View File

@@ -0,0 +1,49 @@
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
abstract class JsonCache<T> {
final String cacheFileName;
JsonCache(this.cacheFileName);
Future<File> _getCacheFile() async {
final basePath = await getTemporaryDirectory();
final basePathName = basePath.path;
final file = File("$basePathName/$cacheFileName.bin");
return file;
}
Future<bool> isValid() async {
final file = await _getCacheFile();
return await file.exists();
}
Future<void> invalidate() async {
final file = await _getCacheFile();
await file.delete();
}
Future<void> putRawData(dynamic data) async {
final jsonString = json.encode(data);
final file = await _getCacheFile();
if (!await file.exists()) {
await file.create();
}
await file.writeAsString(jsonString);
}
dynamic readRawData() async {
final file = await _getCacheFile();
final data = await file.readAsString();
return json.decode(data);
}
void put(T data);
Future<T> get();
}

View File

@@ -29,9 +29,9 @@ class SplashScreenPage extends HookConsumerWidget {
if (isAuthenticated) {
// Resume backup (if enable) then navigate
ref.watch(backupProvider.notifier).resumeBackup();
AutoRouter.of(context).pushNamed("/tab-controller-page");
AutoRouter.of(context).replace(const TabControllerRoute());
} else {
AutoRouter.of(context).push(const LoginRoute());
AutoRouter.of(context).replace(const LoginRoute());
}
}
@@ -40,7 +40,7 @@ class SplashScreenPage extends HookConsumerWidget {
if (loginInfo?.isSaveLogin == true) {
performLoggingIn();
} else {
AutoRouter.of(context).push(const LoginRoute());
AutoRouter.of(context).replace(const LoginRoute());
}
return null;
},

View File

@@ -1,8 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
import 'package:immich_mobile/routing/router.dart';
class TabControllerPage extends ConsumerWidget {
@@ -10,9 +11,7 @@ class TabControllerPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable;
final multiselectEnabled = ref.watch(multiselectProvider);
return AutoTabsRouter(
routes: [
const HomeRoute(),
@@ -22,17 +21,23 @@ class TabControllerPage extends ConsumerWidget {
],
builder: (context, child, animation) {
final tabsRouter = AutoTabsRouter.of(context);
final appRouter = AutoRouter.of(context);
return WillPopScope(
onWillPop: () async {
tabsRouter.setActiveIndex(0);
return false;
bool atHomeTab = tabsRouter.activeIndex == 0;
if (!atHomeTab) {
tabsRouter.setActiveIndex(0);
} else {
appRouter.navigateBack();
}
return atHomeTab;
},
child: Scaffold(
body: FadeTransition(
opacity: animation,
child: child,
),
bottomNavigationBar: isMultiSelectEnable
bottomNavigationBar: multiselectEnabled
? null
: BottomNavigationBar(
selectedLabelStyle: const TextStyle(
@@ -45,6 +50,7 @@ class TabControllerPage extends ConsumerWidget {
),
currentIndex: tabsRouter.activeIndex,
onTap: (index) {
HapticFeedback.selectionClick();
tabsRouter.setActiveIndex(index);
},
items: [

View File

@@ -103,8 +103,8 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
visualDensity: VisualDensity.standard,
primary: Colors.indigo,
onPrimary: Colors.grey[50],
backgroundColor: Colors.indigo,
foregroundColor: Colors.grey[50],
elevation: 2,
padding: const EdgeInsets.symmetric(
vertical: 10,

View File

@@ -25,9 +25,11 @@ String getImageUrl(final AssetResponseDto asset) {
return '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false';
}
String _getThumbnailUrl(final String id,
{ThumbnailFormat type = ThumbnailFormat.WEBP}) {
String _getThumbnailUrl(
final String id, {
ThumbnailFormat type = ThumbnailFormat.WEBP,
}) {
final box = Hive.box(userInfoBox);
return '${box.get(serverEndpointKey)}/asset/thumbnail/${id}?format=${type.value}';
return '${box.get(serverEndpointKey)}/asset/thumbnail/$id?format=${type.value}';
}

View File

@@ -72,8 +72,8 @@ ThemeData immichDarkTheme = ThemeData(
cardColor: Colors.grey[900],
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
onPrimary: Colors.black87,
primary: immichDarkThemePrimaryColor,
foregroundColor: Colors.black87,
backgroundColor: immichDarkThemePrimaryColor,
),
),
);
@@ -126,8 +126,8 @@ ThemeData immichLightTheme = ThemeData(
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
primary: Colors.indigo,
onPrimary: Colors.white,
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
),
),
);

View File

@@ -112,8 +112,10 @@ class ImmichCacheInfoRepository extends ImmichCacheRepository {
}
@override
Future<CacheObject> insert(CacheObject cacheObject,
{bool setTouchedToNow = true}) async {
Future<CacheObject> insert(
CacheObject cacheObject, {
bool setTouchedToNow = true,
}) async {
int newId = keyLookupHiveBox.length == 0
? 0
: keyLookupHiveBox.values.reduce(max) + 1;
@@ -144,8 +146,10 @@ class ImmichCacheInfoRepository extends ImmichCacheRepository {
}
@override
Future<int> update(CacheObject cacheObject,
{bool setTouchedToNow = true}) async {
Future<int> update(
CacheObject cacheObject, {
bool setTouchedToNow = true,
}) async {
if (cacheObject.id != null) {
cacheObjectLookupBox.put(cacheObject.id, cacheObject.toMap());
return 1;

View File

@@ -8,6 +8,7 @@ doc/AdminSignupResponseDto.md
doc/AlbumApi.md
doc/AlbumCountResponseDto.md
doc/AlbumResponseDto.md
doc/AllJobStatusResponseDto.md
doc/AssetApi.md
doc/AssetCountByTimeBucket.md
doc/AssetCountByTimeBucketResponseDto.md
@@ -18,6 +19,8 @@ doc/AssetTypeEnum.md
doc/AuthenticationApi.md
doc/CheckDuplicateAssetDto.md
doc/CheckDuplicateAssetResponseDto.md
doc/CheckExistingAssetsDto.md
doc/CheckExistingAssetsResponseDto.md
doc/CreateAlbumDto.md
doc/CreateDeviceInfoDto.md
doc/CreateProfileImageResponseDto.md
@@ -33,6 +36,12 @@ doc/DeviceTypeEnum.md
doc/ExifResponseDto.md
doc/GetAssetByTimeBucketDto.md
doc/GetAssetCountByTimeBucketDto.md
doc/JobApi.md
doc/JobCommand.md
doc/JobCommandDto.md
doc/JobCounts.md
doc/JobId.md
doc/JobStatusResponseDto.md
doc/LoginCredentialDto.md
doc/LoginResponseDto.md
doc/LogoutResponseDto.md
@@ -41,6 +50,7 @@ doc/SearchAssetDto.md
doc/ServerInfoApi.md
doc/ServerInfoResponseDto.md
doc/ServerPingResponse.md
doc/ServerStatsResponseDto.md
doc/ServerVersionReponseDto.md
doc/SignUpDto.md
doc/SmartInfoResponseDto.md
@@ -49,6 +59,7 @@ doc/TimeGroupEnum.md
doc/UpdateAlbumDto.md
doc/UpdateDeviceInfoDto.md
doc/UpdateUserDto.md
doc/UsageByUserDto.md
doc/UserApi.md
doc/UserCountResponseDto.md
doc/UserResponseDto.md
@@ -59,6 +70,7 @@ lib/api/album_api.dart
lib/api/asset_api.dart
lib/api/authentication_api.dart
lib/api/device_info_api.dart
lib/api/job_api.dart
lib/api/server_info_api.dart
lib/api/user_api.dart
lib/api_client.dart
@@ -74,6 +86,7 @@ lib/model/add_users_dto.dart
lib/model/admin_signup_response_dto.dart
lib/model/album_count_response_dto.dart
lib/model/album_response_dto.dart
lib/model/all_job_status_response_dto.dart
lib/model/asset_count_by_time_bucket.dart
lib/model/asset_count_by_time_bucket_response_dto.dart
lib/model/asset_count_by_user_id_response_dto.dart
@@ -82,6 +95,8 @@ lib/model/asset_response_dto.dart
lib/model/asset_type_enum.dart
lib/model/check_duplicate_asset_dto.dart
lib/model/check_duplicate_asset_response_dto.dart
lib/model/check_existing_assets_dto.dart
lib/model/check_existing_assets_response_dto.dart
lib/model/create_album_dto.dart
lib/model/create_device_info_dto.dart
lib/model/create_profile_image_response_dto.dart
@@ -96,6 +111,11 @@ lib/model/device_type_enum.dart
lib/model/exif_response_dto.dart
lib/model/get_asset_by_time_bucket_dto.dart
lib/model/get_asset_count_by_time_bucket_dto.dart
lib/model/job_command.dart
lib/model/job_command_dto.dart
lib/model/job_counts.dart
lib/model/job_id.dart
lib/model/job_status_response_dto.dart
lib/model/login_credential_dto.dart
lib/model/login_response_dto.dart
lib/model/logout_response_dto.dart
@@ -103,6 +123,7 @@ lib/model/remove_assets_dto.dart
lib/model/search_asset_dto.dart
lib/model/server_info_response_dto.dart
lib/model/server_ping_response.dart
lib/model/server_stats_response_dto.dart
lib/model/server_version_reponse_dto.dart
lib/model/sign_up_dto.dart
lib/model/smart_info_response_dto.dart
@@ -111,7 +132,10 @@ lib/model/time_group_enum.dart
lib/model/update_album_dto.dart
lib/model/update_device_info_dto.dart
lib/model/update_user_dto.dart
lib/model/usage_by_user_dto.dart
lib/model/user_count_response_dto.dart
lib/model/user_response_dto.dart
lib/model/validate_access_token_response_dto.dart
pubspec.yaml
test/check_existing_assets_dto_test.dart
test/check_existing_assets_response_dto_test.dart

View File

@@ -76,6 +76,7 @@ Class | Method | HTTP request | Description
*AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{albumId}/user/{userId} |
*AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{albumId} |
*AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check |
*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |
*AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset |
*AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **GET** /asset/download |
*AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset |
@@ -97,8 +98,12 @@ Class | Method | HTTP request | Description
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
*DeviceInfoApi* | [**createDeviceInfo**](doc//DeviceInfoApi.md#createdeviceinfo) | **POST** /device-info |
*DeviceInfoApi* | [**updateDeviceInfo**](doc//DeviceInfoApi.md#updatedeviceinfo) | **PATCH** /device-info |
*JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs |
*JobApi* | [**getJobStatus**](doc//JobApi.md#getjobstatus) | **GET** /jobs/{jobId} |
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{jobId} |
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |
*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
*ServerInfoApi* | [**getStats**](doc//ServerInfoApi.md#getstats) | **GET** /server-info/stats |
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
*UserApi* | [**createProfileImage**](doc//UserApi.md#createprofileimage) | **POST** /user/profile-image |
*UserApi* | [**createUser**](doc//UserApi.md#createuser) | **POST** /user |
@@ -117,6 +122,7 @@ Class | Method | HTTP request | Description
- [AdminSignupResponseDto](doc//AdminSignupResponseDto.md)
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
- [AlbumResponseDto](doc//AlbumResponseDto.md)
- [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
- [AssetCountByTimeBucket](doc//AssetCountByTimeBucket.md)
- [AssetCountByTimeBucketResponseDto](doc//AssetCountByTimeBucketResponseDto.md)
- [AssetCountByUserIdResponseDto](doc//AssetCountByUserIdResponseDto.md)
@@ -125,6 +131,8 @@ Class | Method | HTTP request | Description
- [AssetTypeEnum](doc//AssetTypeEnum.md)
- [CheckDuplicateAssetDto](doc//CheckDuplicateAssetDto.md)
- [CheckDuplicateAssetResponseDto](doc//CheckDuplicateAssetResponseDto.md)
- [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md)
- [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md)
- [CreateAlbumDto](doc//CreateAlbumDto.md)
- [CreateDeviceInfoDto](doc//CreateDeviceInfoDto.md)
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
@@ -139,6 +147,11 @@ Class | Method | HTTP request | Description
- [ExifResponseDto](doc//ExifResponseDto.md)
- [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md)
- [GetAssetCountByTimeBucketDto](doc//GetAssetCountByTimeBucketDto.md)
- [JobCommand](doc//JobCommand.md)
- [JobCommandDto](doc//JobCommandDto.md)
- [JobCounts](doc//JobCounts.md)
- [JobId](doc//JobId.md)
- [JobStatusResponseDto](doc//JobStatusResponseDto.md)
- [LoginCredentialDto](doc//LoginCredentialDto.md)
- [LoginResponseDto](doc//LoginResponseDto.md)
- [LogoutResponseDto](doc//LogoutResponseDto.md)
@@ -146,6 +159,7 @@ Class | Method | HTTP request | Description
- [SearchAssetDto](doc//SearchAssetDto.md)
- [ServerInfoResponseDto](doc//ServerInfoResponseDto.md)
- [ServerPingResponse](doc//ServerPingResponse.md)
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
- [ServerVersionReponseDto](doc//ServerVersionReponseDto.md)
- [SignUpDto](doc//SignUpDto.md)
- [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
@@ -154,6 +168,7 @@ Class | Method | HTTP request | Description
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
- [UpdateDeviceInfoDto](doc//UpdateDeviceInfoDto.md)
- [UpdateUserDto](doc//UpdateUserDto.md)
- [UsageByUserDto](doc//UsageByUserDto.md)
- [UserCountResponseDto](doc//UserCountResponseDto.md)
- [UserResponseDto](doc//UserResponseDto.md)
- [ValidateAccessTokenResponseDto](doc//ValidateAccessTokenResponseDto.md)

View File

@@ -0,0 +1,22 @@
# openapi.model.AllJobStatusResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**thumbnailGenerationQueueCount** | [**JobCounts**](JobCounts.md) | |
**metadataExtractionQueueCount** | [**JobCounts**](JobCounts.md) | |
**videoConversionQueueCount** | [**JobCounts**](JobCounts.md) | |
**machineLearningQueueCount** | [**JobCounts**](JobCounts.md) | |
**isThumbnailGenerationActive** | **bool** | |
**isMetadataExtractionActive** | **bool** | |
**isVideoConversionActive** | **bool** | |
**isMachineLearningActive** | **bool** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -10,6 +10,7 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check |
[**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist |
[**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset |
[**downloadFile**](AssetApi.md#downloadfile) | **GET** /asset/download |
[**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset |
@@ -76,6 +77,55 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **checkExistingAssets**
> CheckExistingAssetsResponseDto checkExistingAssets(checkExistingAssetsDto)
Checks if multiple assets exist on the server and returns all existing - used by background backup
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = AssetApi();
final checkExistingAssetsDto = CheckExistingAssetsDto(); // CheckExistingAssetsDto |
try {
final result = api_instance.checkExistingAssets(checkExistingAssetsDto);
print(result);
} catch (e) {
print('Exception when calling AssetApi->checkExistingAssets: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**checkExistingAssetsDto** | [**CheckExistingAssetsDto**](CheckExistingAssetsDto.md)| |
### Return type
[**CheckExistingAssetsResponseDto**](CheckExistingAssetsResponseDto.md)
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **deleteAsset**
> List<DeleteAssetResponseDto> deleteAsset(deleteAssetDto)

View File

@@ -0,0 +1,16 @@
# openapi.model.AssetCountResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**photos** | **int** | |
**videos** | **int** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,16 @@
# openapi.model.CheckExistingAssetsDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**deviceAssetIds** | **List<String>** | | [default to const []]
**deviceId** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,15 @@
# openapi.model.CheckExistingAssetsResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**existingIds** | **List<String>** | | [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,15 @@
# openapi.model.CreateJobDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**jobType** | [**JobType**](JobType.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -8,13 +8,13 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**id** | **String** | | [optional]
**id** | **int** | | [optional]
**fileSizeInByte** | **int** | | [optional]
**make** | **String** | | [optional]
**model** | **String** | | [optional]
**imageName** | **String** | | [optional]
**exifImageWidth** | **num** | | [optional]
**exifImageHeight** | **num** | | [optional]
**fileSizeInByte** | **num** | | [optional]
**orientation** | **String** | | [optional]
**dateTimeOriginal** | [**DateTime**](DateTime.md) | | [optional]
**modifyDate** | [**DateTime**](DateTime.md) | | [optional]

View File

@@ -0,0 +1,155 @@
# openapi.api.JobApi
## Load the API package
```dart
import 'package:openapi/api.dart';
```
All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**getAllJobsStatus**](JobApi.md#getalljobsstatus) | **GET** /jobs |
[**getJobStatus**](JobApi.md#getjobstatus) | **GET** /jobs/{jobId} |
[**sendJobCommand**](JobApi.md#sendjobcommand) | **PUT** /jobs/{jobId} |
# **getAllJobsStatus**
> AllJobStatusResponseDto getAllJobsStatus()
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = JobApi();
try {
final result = api_instance.getAllJobsStatus();
print(result);
} catch (e) {
print('Exception when calling JobApi->getAllJobsStatus: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**AllJobStatusResponseDto**](AllJobStatusResponseDto.md)
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getJobStatus**
> JobStatusResponseDto getJobStatus(jobId)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = JobApi();
final jobId = ; // JobId |
try {
final result = api_instance.getJobStatus(jobId);
print(result);
} catch (e) {
print('Exception when calling JobApi->getJobStatus: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**jobId** | [**JobId**](.md)| |
### Return type
[**JobStatusResponseDto**](JobStatusResponseDto.md)
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **sendJobCommand**
> num sendJobCommand(jobId, jobCommandDto)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = JobApi();
final jobId = ; // JobId |
final jobCommandDto = JobCommandDto(); // JobCommandDto |
try {
final result = api_instance.sendJobCommand(jobId, jobCommandDto);
print(result);
} catch (e) {
print('Exception when calling JobApi->sendJobCommand: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**jobId** | [**JobId**](.md)| |
**jobCommandDto** | [**JobCommandDto**](JobCommandDto.md)| |
### Return type
**num**
### Authorization
[bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

View File

@@ -0,0 +1,14 @@
# openapi.model.JobCommand
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,15 @@
# openapi.model.JobCommandDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**command** | [**JobCommand**](JobCommand.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,19 @@
# openapi.model.JobCounts
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**active** | **int** | |
**completed** | **int** | |
**failed** | **int** | |
**delayed** | **int** | |
**waiting** | **int** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,14 @@
# openapi.model.JobId
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,16 @@
# openapi.model.JobStatusResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**isActive** | **bool** | |
**queueCount** | [**Object**](.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,14 @@
# openapi.model.JobType
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -11,6 +11,7 @@ Method | HTTP request | Description
------------- | ------------- | -------------
[**getServerInfo**](ServerInfoApi.md#getserverinfo) | **GET** /server-info |
[**getServerVersion**](ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
[**getStats**](ServerInfoApi.md#getstats) | **GET** /server-info/stats |
[**pingServer**](ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
@@ -88,6 +89,43 @@ No authorization required
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **getStats**
> ServerStatsResponseDto getStats()
### Example
```dart
import 'package:openapi/api.dart';
final api_instance = ServerInfoApi();
try {
final result = api_instance.getStats();
print(result);
} catch (e) {
print('Exception when calling ServerInfoApi->getStats: $e\n');
}
```
### Parameters
This endpoint does not need any parameter.
### Return type
[**ServerStatsResponseDto**](ServerStatsResponseDto.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **pingServer**
> ServerPingResponse pingServer()

View File

@@ -0,0 +1,20 @@
# openapi.model.ServerStatsResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**photos** | **int** | |
**videos** | **int** | |
**objects** | **int** | |
**usageRaw** | **int** | |
**usage** | **String** | |
**usageByUser** | [**List<UsageByUserDto>**](UsageByUserDto.md) | | [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -0,0 +1,20 @@
# openapi.model.UsageByUserDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**userId** | **String** | |
**objects** | **int** | |
**videos** | **int** | |
**photos** | **int** | |
**usageRaw** | **int** | |
**usage** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -31,6 +31,7 @@ part 'api/album_api.dart';
part 'api/asset_api.dart';
part 'api/authentication_api.dart';
part 'api/device_info_api.dart';
part 'api/job_api.dart';
part 'api/server_info_api.dart';
part 'api/user_api.dart';
@@ -39,6 +40,7 @@ part 'model/add_users_dto.dart';
part 'model/admin_signup_response_dto.dart';
part 'model/album_count_response_dto.dart';
part 'model/album_response_dto.dart';
part 'model/all_job_status_response_dto.dart';
part 'model/asset_count_by_time_bucket.dart';
part 'model/asset_count_by_time_bucket_response_dto.dart';
part 'model/asset_count_by_user_id_response_dto.dart';
@@ -47,6 +49,8 @@ part 'model/asset_response_dto.dart';
part 'model/asset_type_enum.dart';
part 'model/check_duplicate_asset_dto.dart';
part 'model/check_duplicate_asset_response_dto.dart';
part 'model/check_existing_assets_dto.dart';
part 'model/check_existing_assets_response_dto.dart';
part 'model/create_album_dto.dart';
part 'model/create_device_info_dto.dart';
part 'model/create_profile_image_response_dto.dart';
@@ -61,6 +65,11 @@ part 'model/device_type_enum.dart';
part 'model/exif_response_dto.dart';
part 'model/get_asset_by_time_bucket_dto.dart';
part 'model/get_asset_count_by_time_bucket_dto.dart';
part 'model/job_command.dart';
part 'model/job_command_dto.dart';
part 'model/job_counts.dart';
part 'model/job_id.dart';
part 'model/job_status_response_dto.dart';
part 'model/login_credential_dto.dart';
part 'model/login_response_dto.dart';
part 'model/logout_response_dto.dart';
@@ -68,6 +77,7 @@ part 'model/remove_assets_dto.dart';
part 'model/search_asset_dto.dart';
part 'model/server_info_response_dto.dart';
part 'model/server_ping_response.dart';
part 'model/server_stats_response_dto.dart';
part 'model/server_version_reponse_dto.dart';
part 'model/sign_up_dto.dart';
part 'model/smart_info_response_dto.dart';
@@ -76,6 +86,7 @@ part 'model/time_group_enum.dart';
part 'model/update_album_dto.dart';
part 'model/update_device_info_dto.dart';
part 'model/update_user_dto.dart';
part 'model/usage_by_user_dto.dart';
part 'model/user_count_response_dto.dart';
part 'model/user_response_dto.dart';
part 'model/validate_access_token_response_dto.dart';

View File

@@ -72,6 +72,62 @@ class AssetApi {
return null;
}
///
///
/// Checks if multiple assets exist on the server and returns all existing - used by background backup
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [CheckExistingAssetsDto] checkExistingAssetsDto (required):
Future<Response> checkExistingAssetsWithHttpInfo(CheckExistingAssetsDto checkExistingAssetsDto,) async {
// ignore: prefer_const_declarations
final path = r'/asset/exist';
// ignore: prefer_final_locals
Object? postBody = checkExistingAssetsDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
///
///
/// Checks if multiple assets exist on the server and returns all existing - used by background backup
///
/// Parameters:
///
/// * [CheckExistingAssetsDto] checkExistingAssetsDto (required):
Future<CheckExistingAssetsResponseDto?> checkExistingAssets(CheckExistingAssetsDto checkExistingAssetsDto,) async {
final response = await checkExistingAssetsWithHttpInfo(checkExistingAssetsDto,);
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), 'CheckExistingAssetsResponseDto',) as CheckExistingAssetsResponseDto;
}
return null;
}
/// Performs an HTTP 'DELETE /asset' operation and returns the [Response].
/// Parameters:
///

View File

@@ -0,0 +1,159 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// 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 JobApi {
JobApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// Performs an HTTP 'GET /jobs' operation and returns the [Response].
Future<Response> getAllJobsStatusWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/jobs';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
Future<AllJobStatusResponseDto?> getAllJobsStatus() async {
final response = await getAllJobsStatusWithHttpInfo();
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), 'AllJobStatusResponseDto',) as AllJobStatusResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /jobs/{jobId}' operation and returns the [Response].
/// Parameters:
///
/// * [JobId] jobId (required):
Future<Response> getJobStatusWithHttpInfo(JobId jobId,) async {
// ignore: prefer_const_declarations
final path = r'/jobs/{jobId}'
.replaceAll('{jobId}', jobId.toString());
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [JobId] jobId (required):
Future<JobStatusResponseDto?> getJobStatus(JobId jobId,) async {
final response = await getJobStatusWithHttpInfo(jobId,);
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), 'JobStatusResponseDto',) as JobStatusResponseDto;
}
return null;
}
/// Performs an HTTP 'PUT /jobs/{jobId}' operation and returns the [Response].
/// Parameters:
///
/// * [JobId] jobId (required):
///
/// * [JobCommandDto] jobCommandDto (required):
Future<Response> sendJobCommandWithHttpInfo(JobId jobId, JobCommandDto jobCommandDto,) async {
// ignore: prefer_const_declarations
final path = r'/jobs/{jobId}'
.replaceAll('{jobId}', jobId.toString());
// ignore: prefer_final_locals
Object? postBody = jobCommandDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [JobId] jobId (required):
///
/// * [JobCommandDto] jobCommandDto (required):
Future<num?> sendJobCommand(JobId jobId, JobCommandDto jobCommandDto,) async {
final response = await sendJobCommandWithHttpInfo(jobId, jobCommandDto,);
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), 'num',) as num;
}
return null;
}
}

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