Compare commits

...

34 Commits

Author SHA1 Message Date
Alex Tran
1390d01763 Up version 2022-08-15 19:13:51 -05:00
Alex
86f780871c Fixed different lettercases in email create different user (#470)
* Fixed different lettercases in email create different user

* Fixed test
2022-08-15 19:11:08 -05:00
Alex
c1b22125fd Add mobile dark mode and user setting (#468)
* styling light and dark theme

* Icon topbar

* Fixed app bar title dark theme

* Fixed issue with getting thumbnail for things

* Refactor sharing page

* Refactor scroll thumb

* Refactor chip in auto  backup indiation button

* Refactor sharing page

* Added theme toggle

* Up version for testflight build

* Refactor backup controller page

* Refactor album selection page

* refactor album pages

* Refactor gradient color profile header

* Added theme switcher

* Register app theme correctly

* Added locale to the app

* Added translation key

* Styling for bottomsheet colors

* up server version

* Fixed font size

* Fixed overlapsed sliverappbar on photos screen
2022-08-15 18:53:30 -05:00
Alex
30f069a5db Add settings screen on mobile (#463)
* Refactor profile drawer to sub component

* Added setting page, routing with some options

* Added setting service

* Implement three stage settings

* get app setting for three stage loading
2022-08-13 15:51:09 -05:00
bo0tzz
2bf6cd9241 Fix redirect to login page after password change (#461)
* Fix redirect to login page after password change

Copied from the similar fix in #414

* Fix typo in change-password form

* Remove misplaced text from user management page
2022-08-13 09:54:29 -05:00
Alex Tran
87d2a954a3 Fixed error handling with catch block 2022-08-12 22:29:24 -05:00
Alex
a388c5a642 Fixed webp upload on web (#460) 2022-08-12 21:52:30 -05:00
Alex Tran
4b34f017ca cosmetic change 2022-08-12 21:19:54 -05:00
Alex Tran
5c1d1dd5a1 Added version note for f-droid 2022-08-12 20:10:00 -05:00
Alex Tran
1580d27c23 Up version 2022-08-12 20:06:45 -05:00
Alex
4b9187928c Edit user on the web (#458)
* Added dispatch event for edit user

* Fixed import location

* solve merge conflict

* Fixed issue not admin user can access admin page

* Implemented edit user and password reset
2022-08-12 14:25:19 -05:00
Alex Tran
5b7236f6ad Temporary remove bug tests 2022-08-11 23:17:09 -05:00
Alex Tran
6fb439b580 Fixed merge conflict 2022-08-11 13:46:42 -05:00
Alex Tran
a8334b5c27 Fixed test again 2022-08-11 13:46:11 -05:00
Alex Tran
e1cac93945 Fixed test 2022-08-11 09:29:53 -05:00
R0GGER
081f9f5bce typo (#456) 2022-08-11 08:33:44 -05:00
Alex Tran
25ccc5660d Merge branch 'main' of github.com:immich-app/immich 2022-08-11 08:27:48 -05:00
Alex Tran
b6d3e578f2 Added test and github action for unit tests 2022-08-11 08:27:44 -05:00
Matthias Rupp
52377c2dcf Fix sharing on iPad (#453) 2022-08-11 08:13:33 -05:00
Alex
5c78f707fe Modify Album API endpoint to return a count attribute instead of a full assets array (#454)
* Change API to return assets count and change web behavior accordingly

* Refactor assets.length

* Explicitly declare type of assetCount so Dart SDK understand it

* Finished refactoring on mobile
2022-08-10 22:48:25 -05:00
Alex Tran
bd5ed1b684 Merge branch 'main' of github.com:immich-app/immich 2022-08-09 19:12:32 -05:00
Alex Tran
e89339b813 Up server version 2022-08-09 19:12:21 -05:00
Alex
0b69feda40 Fixed checkbox render performance (#448) 2022-08-09 19:10:55 -05:00
Alex
339f7f776f Fixed setting high refresh rate crash ios release build 2022-08-08 23:43:48 -05:00
Alex Tran
7e6ccbad21 Up server version 2022-08-08 22:55:35 -05:00
Alex Tran
aac53e5cdc Up version for release 2022-08-08 22:39:32 -05:00
Alex Tran
cbec75a175 Rewording delete caution message 2022-08-08 22:13:36 -05:00
Alex
bf04d9eb39 Feature - Delete asset on the web (#436)
* Added selection mechanism to photos page

* Added control app bar

* Refactor AlbumAppBar into ControlAppBar

* Added addtional micro interactions when in multi selection mode

* Implemented delete selected asset and rerender
2022-08-08 22:06:11 -05:00
Malte Kiefer
3058c894b1 updated German translation (#444) 2022-08-08 21:21:02 -05:00
Matthias Rupp
e57e279fe1 Share assets from mobile to other apps (#435)
* Share unique assets

* Style share preparing dialog

* Share assets from multiselect

* Fix i18n

* Use navigator like in delete dialog

* Center bottom-bar buttons
2022-08-08 10:46:12 -05:00
dependabot[bot]
f43c58fc6d Bump docker/build-push-action from 3.1.0 to 3.1.1 (#441)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3.1.0 to 3.1.1.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v3.1.0...v3.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-08 08:22:14 -05:00
Matthias Rupp
dea304ac39 Fix/album title (#440)
* Fix album title overflow

* i18n

* More i18n
2022-08-08 08:11:56 -05:00
Matthias Rupp
b46e834220 Mobile performance improvements (#417)
* First performance tweaks (caching and rendering improvemetns)

* Revert asset response caching

* 3-step image loading in asset viewer

* Prevent panning and zooming until full-scale version is loaded

* Loading indicator

* Adapt to gallery PR

* Cleanup

* Dart format

* Fix exif sheet

* Disable three stage loading until settings are available
2022-08-07 19:43:09 -05:00
Alex Tran
46f4905259 Up server version 2022-08-07 18:42:21 -05:00
110 changed files with 2574 additions and 1288 deletions

View File

@@ -27,7 +27,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich Mono Repo - name: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./server context: ./server
file: ./server/Dockerfile file: ./server/Dockerfile
@@ -55,7 +55,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning - name: Build and Push Machine Learning
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./machine-learning context: ./machine-learning
file: ./machine-learning/Dockerfile file: ./machine-learning/Dockerfile
@@ -82,7 +82,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Web - name: Build and Push Web
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./web context: ./web
file: ./web/Dockerfile file: ./web/Dockerfile
@@ -110,7 +110,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Proxy - name: Build and Push Proxy
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./nginx context: ./nginx
file: ./nginx/Dockerfile file: ./nginx/Dockerfile

View File

@@ -30,7 +30,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich Mono Repo - name: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./server context: ./server
file: ./server/Dockerfile file: ./server/Dockerfile
@@ -59,7 +59,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning - name: Build and Push Machine Learning
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./machine-learning context: ./machine-learning
file: ./machine-learning/Dockerfile file: ./machine-learning/Dockerfile
@@ -87,7 +87,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Web - name: Build and Push Web
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./web context: ./web
file: ./web/Dockerfile file: ./web/Dockerfile
@@ -116,7 +116,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Proxy - name: Build and Push Proxy
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./nginx context: ./nginx
file: ./nginx/Dockerfile file: ./nginx/Dockerfile

View File

@@ -35,7 +35,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-server release - name: Build and push immich-server release
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./server context: ./server
file: ./server/Dockerfile file: ./server/Dockerfile
@@ -68,7 +68,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning - name: Build and Push Machine Learning
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./machine-learning context: ./machine-learning
file: ./machine-learning/Dockerfile file: ./machine-learning/Dockerfile
@@ -107,7 +107,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-web release - name: Build and push immich-web release
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./web context: ./web
file: ./web/Dockerfile file: ./web/Dockerfile
@@ -147,7 +147,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-proxy release - name: Build and push immich-proxy release
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./nginx context: ./nginx
file: ./nginx/Dockerfile file: ./nginx/Dockerfile

View File

@@ -2,11 +2,12 @@ name: Test
on: on:
workflow_dispatch: workflow_dispatch:
pull_request: pull_request:
push: { branches: master } push:
branches: [main]
jobs: jobs:
test-server-e2e: e2e-tests:
name: Run test suite name: Run end-to-end test suites
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -16,3 +17,14 @@ jobs:
- name: Run Immich Server 2E2 Test - name: Run Immich Server 2E2 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 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
unit-tests:
name: Run unit test suites
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests
run: cd server && npm install && npm run test

View File

@@ -1,6 +1,9 @@
dev: dev:
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
dev-new:
rm -rf ./server/dist && docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
dev-update: dev-update:
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans

View File

@@ -129,7 +129,7 @@ wget https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-comp
Get `.env` Get `.env`
```bash ```bash
wget -O .env wget https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example wget -O .env https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example
``` ```
### Step 2 - Populate .env file with customed information ### Step 2 - Populate .env file with customed information

View File

@@ -30,8 +30,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 30, "android.injected.version.code" => 33,
"android.injected.version.name" => "1.20.0", "android.injected.version.name" => "1.23.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') 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

@@ -15,13 +15,21 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
## Android ## Android
### android build
```sh
[bundle exec] fastlane android build
```
Build Android
### android release ### android release
```sh ```sh
[bundle exec] fastlane android release [bundle exec] fastlane android release
``` ```
Update AAB to PlayStore Build and Release Android
---- ----

View File

@@ -0,0 +1,3 @@
* Improve performance
* Fix album title overflow
* New feature - Share asset from mobile to other apps

View File

@@ -0,0 +1 @@
* Modify Album API endpoint to return count attribute instead of all assets to reduce network consumption and CPU processing.

View File

@@ -0,0 +1,2 @@
* Added setting screen
* Implemented dark mode

View File

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000204"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000221">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="11.673502"> <testcase classname="fastlane.lanes" name="1: bundleRelease" time="55.750133">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="37.162935"> <testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="35.558064">
</testcase> </testcase>

View File

@@ -47,6 +47,7 @@
"backup_info_card_assets": "Elemente", "backup_info_card_assets": "Elemente",
"control_bottom_app_bar_delete": "Löschen", "control_bottom_app_bar_delete": "Löschen",
"create_shared_album_page_share": "Teilen", "create_shared_album_page_share": "Teilen",
"create_shared_album_page_create": "Erstellen",
"create_shared_album_page_share_add_assets": "ELEMENTE HINZUFÜGEN", "create_shared_album_page_share_add_assets": "ELEMENTE HINZUFÜGEN",
"create_shared_album_page_share_select_photos": "Fotos auswählen", "create_shared_album_page_share_select_photos": "Fotos auswählen",
"daily_title_text_date": "E, dd MMM", "daily_title_text_date": "E, dd MMM",
@@ -97,10 +98,19 @@
"tab_controller_nav_photos": "Fotos", "tab_controller_nav_photos": "Fotos",
"tab_controller_nav_search": "Suche", "tab_controller_nav_search": "Suche",
"tab_controller_nav_sharing": "Teilen", "tab_controller_nav_sharing": "Teilen",
"tab_controller_nav_library": "Bibliothek",
"version_announcement_overlay_ack": "Ich habe verstanden", "version_announcement_overlay_ack": "Ich habe verstanden",
"version_announcement_overlay_release_notes": "Änderungsprotokoll", "version_announcement_overlay_release_notes": "Änderungsprotokoll",
"version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von", "version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von",
"version_announcement_overlay_text_2": "Bitte nehm dir die Zeit und lese das ", "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_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" "version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89",
"album_thumbnail_card_item": "1 Element",
"album_thumbnail_card_items": "{} Elemente",
"album_thumbnail_card_shared": " · Geteilt",
"library_page_albums": "Alben",
"library_page_new_album": "Neues Album",
"create_album_page_untitled": "Unbenannt",
"share_dialog_preparing": "Vorbereiten...",
"control_bottom_app_bar_share": "Teilen"
} }

View File

@@ -48,7 +48,7 @@
"control_bottom_app_bar_delete": "Delete", "control_bottom_app_bar_delete": "Delete",
"create_shared_album_page_share": "Share", "create_shared_album_page_share": "Share",
"create_shared_album_page_create": "Create", "create_shared_album_page_create": "Create",
"create_shared_album_page_share_add_assets": "ADD ASSETS", "create_shared_album_page_share_add_assets": "ADD PHOTOS",
"create_shared_album_page_share_select_photos": "Select Photos", "create_shared_album_page_share_select_photos": "Select Photos",
"daily_title_text_date": "E, MMM dd", "daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy", "daily_title_text_date_year": "E, MMM dd, yyyy",
@@ -75,7 +75,8 @@
"login_form_save_login": "Stay logged in", "login_form_save_login": "Stay logged in",
"monthly_title_text_date_format": "MMMM y", "monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date", "profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
"profile_drawer_sign_out": "Sign Out", "profile_drawer_sign_out": "Sign out",
"profile_drawer_settings": "Settings",
"search_bar_hint": "Search your photos", "search_bar_hint": "Search your photos",
"search_page_no_objects": "No Objects Info Available", "search_page_no_objects": "No Objects Info Available",
"search_page_no_places": "No Places Info Available", "search_page_no_places": "No Places Info Available",
@@ -104,5 +105,22 @@
"version_announcement_overlay_text_1": "Hi friend, there is a new release of", "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
"version_announcement_overlay_text_2": "please take your time to visit the ", "version_announcement_overlay_text_2": "please take your time to visit the ",
"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_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" "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
"album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · Shared",
"library_page_albums": "Albums",
"library_page_new_album": "New album",
"create_album_page_untitled": "Untitled",
"share_dialog_preparing": "Preparing...",
"control_bottom_app_bar_share": "Share",
"setting_pages_app_bar_settings": "Settings",
"theme_setting_theme_title": "Theme",
"theme_setting_theme_subtitle": "Choose the app's theme setting",
"theme_setting_system_theme_switch": "Automatic (Follow system setting)",
"theme_setting_dark_mode_switch": "Dark mode",
"theme_setting_image_viewer_quality_title": "Image viewer quality",
"theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer",
"theme_setting_three_stage_loading_title": "Enable three-stage loading",
"theme_setting_three_stage_loading_subtitle": "The three-stage loading delivers the best quality image in exchange for a slower loading speed"
} }

View File

@@ -19,6 +19,8 @@ PODS:
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- SAMKeychain (1.5.3) - SAMKeychain (1.5.3)
- share_plus (0.0.1):
- Flutter
- shared_preferences_ios (0.0.1): - shared_preferences_ios (0.0.1):
- Flutter - Flutter
- sqflite (0.0.2): - sqflite (0.0.2):
@@ -40,6 +42,7 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`) - sqflite (from `.symlinks/plugins/sqflite/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@@ -67,6 +70,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_ios/ios" :path: ".symlinks/plugins/path_provider_ios/ios"
photo_manager: photo_manager:
:path: ".symlinks/plugins/photo_manager/ios" :path: ".symlinks/plugins/photo_manager/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_ios: shared_preferences_ios:
:path: ".symlinks/plugins/shared_preferences_ios/ios" :path: ".symlinks/plugins/shared_preferences_ios/ios"
sqflite: sqflite:
@@ -88,6 +93,7 @@ SPEC CHECKSUMS:
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196

View File

@@ -360,7 +360,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38; CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -495,7 +495,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38; CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -522,7 +522,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 38; CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -17,11 +17,11 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.20.0</string> <string>1.21.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>38</string> <string>40</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true /> <true />
<key>MGLMapboxMetricsEnabledSettingShownInApp</key> <key>MGLMapboxMetricsEnabledSettingShownInApp</key>

View File

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

View File

@@ -5,34 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000213"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000205">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.088407"> <testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.360401">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="22.635867"> <testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.012696">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.376681"> <testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.378836">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="91.762747"> <testcase classname="fastlane.lanes" name="4: build_app" time="80.023705">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="49.149884"> <testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="98.18403">
<failure message="/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:229:in `chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing&apos;&#10;Fastfile:30:in `block (2 levels) in parsing_binding&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/lane.rb:33:in `call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:45:in `chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:45:in `execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/commands_generator.rb:110:in `block (2 levels) in run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/commands_generator.rb:354:in `run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/commands_generator.rb:43:in `start&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/bin/fastlane:23:in `&lt;top (required)&gt;&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/bin/fastlane:25:in `load&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/bin/fastlane:25:in `&lt;main&gt;&apos;&#10;&#10;Error uploading ipa file: &#10; [Transporter Error Output]: ERROR ITMS-90186: Invalid Pre-Release Train. The train version &apos;1.19.0&apos; is closed for new build submissions
</testcase> </testcase>

View File

@@ -16,3 +16,6 @@ const String backupInfoKey = "immichBackupAlbumInfoKey"; // Key 1
// Github Release Info // Github Release Info
const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; // Box const String hiveGithubReleaseInfoBox = "immichGithubReleaseInfoBox"; // Box
const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1 const String githubReleaseInfoKey = "immichGithubReleaseInfoKey"; // Key 1
// User Setting Info
const String userSettingInfoBox = "immichUserSettingInfoBox";

View File

@@ -1,3 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
const immichBackgroundColor = Color(0xFFf6f8fe); Color immichBackgroundColor = const Color(0xFFf6f8fe);
Color immichDarkBackgroundColor = const Color.fromARGB(255, 0, 0, 0);
Color immichDarkThemePrimaryColor = const Color.fromARGB(255, 173, 203, 250);

View File

@@ -1,9 +1,12 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart'; import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.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/models/hive_saved_login_info.model.dart';
@@ -15,8 +18,10 @@ import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/release_info.provider.dart'; import 'package:immich_mobile/shared/providers/release_info.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.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/providers/websocket.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart'; import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
import 'package:immich_mobile/utils/immich_app_theme.dart';
import 'constants/hive_box.dart'; import 'constants/hive_box.dart';
void main() async { void main() async {
@@ -29,6 +34,7 @@ void main() async {
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox); await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox); await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
await Hive.openBox(hiveGithubReleaseInfoBox); await Hive.openBox(hiveGithubReleaseInfoBox);
await Hive.openBox(userSettingInfoBox);
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle( const SystemUiOverlayStyle(
@@ -45,10 +51,21 @@ void main() async {
Locale('da', 'DK'), Locale('da', 'DK'),
Locale('de', 'DE'), Locale('de', 'DE'),
Locale('es', 'ES'), Locale('es', 'ES'),
Locale('fi', 'FI'),
Locale('fr', 'FR'), Locale('fr', 'FR'),
Locale('it', 'IT'), Locale('it', 'IT'),
Locale('ja', 'JP'),
Locale('pl', 'PL')
]; ];
if (kReleaseMode && Platform.isAndroid) {
try {
await FlutterDisplayMode.setHighRefreshRate();
} catch (e) {
debugPrint("Error setting high refresh rate: $e");
}
}
runApp( runApp(
EasyLocalization( EasyLocalization(
supportedLocales: locales, supportedLocales: locales,
@@ -117,7 +134,6 @@ class ImmichAppState extends ConsumerState<ImmichApp>
@override @override
initState() { initState() {
super.initState(); super.initState();
initApp().then((_) => debugPrint("App Init Completed")); initApp().then((_) => debugPrint("App Init Completed"));
} }
@@ -142,23 +158,9 @@ class ImmichAppState extends ConsumerState<ImmichApp>
MaterialApp.router( MaterialApp.router(
title: 'Immich', title: 'Immich',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( themeMode: ref.watch(immichThemeProvider),
useMaterial3: true, darkTheme: immichDarkTheme,
brightness: Brightness.light, theme: immichLightTheme,
primarySwatch: Colors.indigo,
fontFamily: 'WorkSans',
snackBarTheme: const SnackBarThemeData(
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
),
scaffoldBackgroundColor: immichBackgroundColor,
appBarTheme: const AppBarTheme(
backgroundColor: immichBackgroundColor,
foregroundColor: Colors.indigo,
elevation: 1,
centerTitle: true,
systemOverlayStyle: SystemUiOverlayStyle.dark,
),
),
routeInformationParser: router.defaultRouteParser(), routeInformationParser: router.defaultRouteParser(),
routerDelegate: router.delegate( routerDelegate: router.delegate(
navigatorObservers: () => [TabNavigationObserver(ref: ref)], navigatorObservers: () => [TabNavigationObserver(ref: ref)],

View File

@@ -14,6 +14,8 @@ class AlbumActionOutlinedButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
return Padding( return Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: OutlinedButton.icon( child: OutlinedButton.icon(
@@ -22,19 +24,23 @@ class AlbumActionOutlinedButton extends StatelessWidget {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
), ),
side: const BorderSide( side: BorderSide(
width: 1, width: 1,
color: Color.fromARGB(255, 215, 215, 215), color: isDarkTheme
? const Color.fromARGB(255, 63, 63, 63)
: const Color.fromARGB(255, 206, 206, 206),
), ),
), ),
icon: Icon(iconData, size: 15), icon: Icon(
iconData,
size: 15,
color: Theme.of(context).primaryColor,
),
label: Text( label: Text(
labelText, labelText,
style: const TextStyle( style: Theme.of(context).textTheme.labelSmall?.copyWith(
fontSize: 12, fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold, ),
color: Colors.black87,
),
), ),
onPressed: onPressed, onPressed: onPressed,
), ),

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
@@ -15,6 +16,8 @@ class AlbumThumbnailCard extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var box = Hive.box(userInfoBox); var box = Hive.box(userInfoBox);
final cardSize = MediaQuery.of(context).size.width / 2 - 18;
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id)); AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id));
@@ -27,8 +30,8 @@ class AlbumThumbnailCard extends StatelessWidget {
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: FadeInImage( child: FadeInImage(
width: MediaQuery.of(context).size.width / 2 - 18, width: cardSize,
height: MediaQuery.of(context).size.width / 2 - 18, height: cardSize,
fit: BoxFit.cover, fit: BoxFit.cover,
placeholder: MemoryImage(kTransparentImage), placeholder: MemoryImage(kTransparentImage),
image: NetworkImage( image: NetworkImage(
@@ -43,11 +46,13 @@ class AlbumThumbnailCard extends StatelessWidget {
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: Text( child: SizedBox(
album.albumName, width: cardSize,
style: const TextStyle( child: Text(
fontWeight: FontWeight.bold, album.albumName,
fontSize: 12, style: const TextStyle(
fontWeight: FontWeight.bold,
),
), ),
), ),
), ),
@@ -55,18 +60,20 @@ class AlbumThumbnailCard extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
'${album.assets.length} item${album.assets.length > 1 ? 's' : ''}', album.assetCount == 1
? 'album_thumbnail_card_item'
: 'album_thumbnail_card_items',
style: const TextStyle( style: const TextStyle(
fontSize: 10, fontSize: 12,
), ),
), ).tr(args: ['${album.assetCount}']),
if (album.shared) if (album.shared)
const Text( const Text(
' · Shared', 'album_thumbnail_card_shared',
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 12,
), ),
) ).tr()
], ],
) )
], ],

View File

@@ -19,6 +19,8 @@ class AlbumTitleTextField extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
return TextField( return TextField(
onChanged: (v) { onChanged: (v) {
if (v.isEmpty) { if (v.isEmpty) {
@@ -51,7 +53,10 @@ class AlbumTitleTextField extends ConsumerWidget {
albumTitleController.clear(); albumTitleController.clear();
isAlbumTitleEmpty.value = true; isAlbumTitleEmpty.value = true;
}, },
icon: const Icon(Icons.cancel_rounded), icon: Icon(
Icons.cancel_rounded,
color: Theme.of(context).primaryColor,
),
splashRadius: 10, splashRadius: 10,
) )
: null, : null,
@@ -65,7 +70,9 @@ class AlbumTitleTextField extends ConsumerWidget {
), ),
hintText: 'share_add_title'.tr(), hintText: 'share_add_title'.tr(),
focusColor: Colors.grey[300], focusColor: Colors.grey[300],
fillColor: Colors.grey[200], fillColor: isDarkTheme
? const Color.fromARGB(255, 32, 33, 35)
: Colors.grey[200],
filled: isAlbumTitleTextFieldFocus.value, filled: isAlbumTitleTextFieldFocus.value,
), ),
); );

View File

@@ -150,7 +150,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
void _buildBottomSheet() { void _buildBottomSheet() {
showModalBottomSheet( showModalBottomSheet(
backgroundColor: immichBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
isScrollControlled: false, isScrollControlled: false,
context: context, context: context,
builder: (context) { builder: (context) {

View File

@@ -18,6 +18,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final titleTextEditController = final titleTextEditController =
useTextEditingController(text: albumInfo.albumName); useTextEditingController(text: albumInfo.albumName);
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
void onFocusModeChange() { void onFocusModeChange() {
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) { if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
@@ -65,7 +66,10 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
onPressed: () { onPressed: () {
titleTextEditController.clear(); titleTextEditController.clear();
}, },
icon: const Icon(Icons.cancel_rounded), icon: Icon(
Icons.cancel_rounded,
color: Theme.of(context).primaryColor,
),
splashRadius: 10, splashRadius: 10,
) )
: null, : null,
@@ -78,7 +82,9 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
focusColor: Colors.grey[300], focusColor: Colors.grey[300],
fillColor: Colors.grey[200], fillColor: isDarkTheme
? const Color.fromARGB(255, 32, 33, 35)
: Colors.grey[200],
filled: titleFocusNode.hasFocus, filled: titleFocusNode.hasFocus,
hintText: 'share_add_title'.tr(), hintText: 'share_add_title'.tr(),
), ),

View File

@@ -8,6 +8,7 @@ import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart'; import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class AlbumViewerThumbnail extends HookConsumerWidget { class AlbumViewerThumbnail extends HookConsumerWidget {
@@ -24,8 +25,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1); final cacheKey = useState(1);
var box = Hive.box(userInfoBox); var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = var thumbnailRequestUrl = getThumbnailUrl(asset);
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
var deviceId = ref.watch(authenticationProvider).deviceId; var deviceId = ref.watch(authenticationProvider).deviceId;
final selectedAssetsInAlbumViewer = final selectedAssetsInAlbumViewer =
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer; ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
@@ -37,7 +37,6 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
GalleryViewerRoute( GalleryViewerRoute(
asset: asset, asset: asset,
assetList: assetList, assetList: assetList,
thumbnailRequestUrl: thumbnailRequestUrl,
), ),
); );
} }

View File

@@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class SharedAlbumThumbnailImage extends HookConsumerWidget { class SharedAlbumThumbnailImage extends HookConsumerWidget {
@@ -17,8 +18,6 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
final cacheKey = useState(1); final cacheKey = useState(1);
var box = Hive.box(userInfoBox); var box = Hive.box(userInfoBox);
var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
@@ -32,7 +31,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
height: 500, height: 500,
memCacheHeight: 500, memCacheHeight: 500,
fit: BoxFit.cover, fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl, imageUrl: getThumbnailUrl(asset),
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
fadeInDuration: const Duration(milliseconds: 250), fadeInDuration: const Duration(milliseconds: 250),
progressIndicatorBuilder: (context, url, downloadProgress) => progressIndicatorBuilder: (context, url, downloadProgress) =>

View File

@@ -35,13 +35,7 @@ class SharingSliverAppBar extends StatelessWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 4.0), padding: const EdgeInsets.only(right: 4.0),
child: TextButton.icon( child: ElevatedButton.icon(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Theme.of(context).primaryColor.withAlpha(20),
),
// foregroundColor: MaterialStateProperty.all(Colors.white),
),
onPressed: () { onPressed: () {
AutoRouter.of(context) AutoRouter.of(context)
.push(CreateAlbumRoute(isSharedAlbum: true)); .push(CreateAlbumRoute(isSharedAlbum: true));
@@ -52,8 +46,12 @@ class SharingSliverAppBar extends StatelessWidget {
), ),
label: const Text( label: const Text(
"sharing_silver_appbar_create_shared_album", "sharing_silver_appbar_create_shared_album",
style: maxLines: 1,
TextStyle(fontWeight: FontWeight.bold, fontSize: 12), style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 11,
// color: Theme.of(context).primaryColor,
),
).tr(), ).tr(),
), ),
), ),
@@ -61,13 +59,7 @@ class SharingSliverAppBar extends StatelessWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 4.0), padding: const EdgeInsets.only(left: 4.0),
child: TextButton.icon( child: ElevatedButton.icon(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Theme.of(context).primaryColor.withAlpha(20),
),
// foregroundColor: MaterialStateProperty.all(Colors.white),
),
onPressed: null, onPressed: null,
icon: const Icon( icon: const Icon(
Icons.swap_horizontal_circle_outlined, Icons.swap_horizontal_circle_outlined,
@@ -75,8 +67,11 @@ class SharingSliverAppBar extends StatelessWidget {
), ),
label: const Text( label: const Text(
"sharing_silver_appbar_share_partner", "sharing_silver_appbar_share_partner",
style: style: TextStyle(
TextStyle(fontWeight: FontWeight.bold, fontSize: 12), fontWeight: FontWeight.bold,
fontSize: 11,
),
maxLines: 1,
).tr(), ).tr(),
), ),
), ),

View File

@@ -3,7 +3,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
@@ -203,7 +202,7 @@ class AlbumViewerPage extends HookConsumerWidget {
assetList: albumInfo.assets, assetList: albumInfo.assets,
); );
}, },
childCount: albumInfo.assets.length, childCount: albumInfo.assetCount,
), ),
), ),
); );
@@ -242,7 +241,7 @@ class AlbumViewerPage extends HookConsumerWidget {
titleFocusNode.unfocus(); titleFocusNode.unfocus();
}, },
child: DraggableScrollbar.semicircle( child: DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).hintColor,
controller: scrollController, controller: scrollController,
heightScrollThumb: 48.0, heightScrollThumb: 48.0,
child: CustomScrollView( child: CustomScrollView(
@@ -255,7 +254,7 @@ class AlbumViewerPage extends HookConsumerWidget {
minHeight: 50, minHeight: 50,
maxHeight: 50, maxHeight: 50,
child: Container( child: Container(
color: immichBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,
child: _buildControlButton(albumInfo), child: _buildControlButton(albumInfo),
), ),
), ),

View File

@@ -43,7 +43,7 @@ class AssetSelectionPage extends HookConsumerWidget {
return Stack( return Stack(
children: [ children: [
DraggableScrollbar.semicircle( DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).hintColor,
controller: scrollController, controller: scrollController,
heightScrollThumb: 48.0, heightScrollThumb: 48.0,
child: CustomScrollView( child: CustomScrollView(

View File

@@ -27,6 +27,7 @@ class CreateAlbumPage extends HookConsumerWidget {
final isAlbumTitleEmpty = useState(true); final isAlbumTitleEmpty = useState(true);
final selectedAssets = final selectedAssets =
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum; ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
_showSelectUserPage() { _showSelectUserPage() {
AutoRouter.of(context).push(const SelectUserForSharingRoute()); AutoRouter.of(context).push(const SelectUserForSharingRoute());
@@ -37,8 +38,10 @@ class CreateAlbumPage extends HookConsumerWidget {
isAlbumTitleTextFieldFocus.value = false; isAlbumTitleTextFieldFocus.value = false;
if (albumTitleController.text.isEmpty) { if (albumTitleController.text.isEmpty) {
albumTitleController.text = 'Untitled'; albumTitleController.text = 'create_album_page_untitled'.tr();
ref.watch(albumTitleProvider.notifier).setAlbumTitle('Untitled'); ref
.watch(albumTitleProvider.notifier)
.setAlbumTitle('create_album_page_untitled'.tr());
} }
} }
@@ -73,9 +76,12 @@ class CreateAlbumPage extends HookConsumerWidget {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 200, left: 18), padding: const EdgeInsets.only(top: 200, left: 18),
child: const Text( child: Text(
'create_shared_album_page_share_add_assets', 'create_shared_album_page_share_add_assets',
style: TextStyle(fontSize: 12), style: Theme.of(context).textTheme.headline2?.copyWith(
fontSize: 12,
fontWeight: FontWeight.normal,
),
).tr(), ).tr(),
), ),
); );
@@ -94,24 +100,28 @@ class CreateAlbumPage extends HookConsumerWidget {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
padding: padding:
const EdgeInsets.symmetric(vertical: 22, horizontal: 16), const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
side: const BorderSide( side: BorderSide(
color: Color.fromARGB(255, 206, 206, 206), color: isDarkTheme
? const Color.fromARGB(255, 63, 63, 63)
: const Color.fromARGB(255, 206, 206, 206),
), ),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
), ),
), ),
onPressed: _onSelectPhotosButtonPressed, onPressed: _onSelectPhotosButtonPressed,
icon: const Icon(Icons.add_rounded), icon: Icon(
Icons.add_rounded,
color: Theme.of(context).primaryColor,
),
label: Padding( label: Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0),
child: Text( child: Text(
'create_shared_album_page_share_select_photos', 'create_shared_album_page_share_select_photos',
style: TextStyle( style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontSize: 16, fontSize: 16,
color: Colors.grey[700], fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold, ),
),
).tr(), ).tr(),
), ),
), ),
@@ -188,6 +198,7 @@ class CreateAlbumPage extends HookConsumerWidget {
appBar: AppBar( appBar: AppBar(
elevation: 0, elevation: 0,
centerTitle: false, centerTitle: false,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
leading: IconButton( leading: IconButton(
onPressed: () { onPressed: () {
ref.watch(assetSelectionProvider.notifier).removeAll(); ref.watch(assetSelectionProvider.notifier).removeAll();
@@ -195,9 +206,11 @@ class CreateAlbumPage extends HookConsumerWidget {
}, },
icon: const Icon(Icons.close_rounded), icon: const Icon(Icons.close_rounded),
), ),
title: const Text( title: Text(
'share_create_album', 'share_create_album',
style: TextStyle(color: Colors.black), style: Theme.of(context).textTheme.headline2?.copyWith(
color: Theme.of(context).primaryColor,
),
).tr(), ).tr(),
actions: [ actions: [
if (isSharedAlbum) if (isSharedAlbum)
@@ -207,8 +220,9 @@ class CreateAlbumPage extends HookConsumerWidget {
: null, : null,
child: Text( child: Text(
'create_shared_album_page_share'.tr(), 'create_shared_album_page_share'.tr(),
style: const TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
), ),
), ),
), ),
@@ -232,9 +246,9 @@ class CreateAlbumPage extends HookConsumerWidget {
child: CustomScrollView( child: CustomScrollView(
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
elevation: 5, elevation: 5,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
// leading: Container(),
pinned: true, pinned: true,
floating: false, floating: false,
bottom: PreferredSize( bottom: PreferredSize(

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -22,7 +23,7 @@ class LibraryPage extends HookConsumerWidget {
); );
Widget _buildAppBar() { Widget _buildAppBar() {
return SliverAppBar( return const SliverAppBar(
centerTitle: true, centerTitle: true,
floating: true, floating: true,
pinned: false, pinned: false,
@@ -34,7 +35,6 @@ class LibraryPage extends HookConsumerWidget {
fontFamily: 'SnowburstOne', fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 22, fontSize: 22,
color: Theme.of(context).primaryColor,
), ),
), ),
); );
@@ -66,15 +66,14 @@ class LibraryPage extends HookConsumerWidget {
), ),
), ),
), ),
const Padding( Padding(
padding: EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: Text( child: const Text(
"New album", 'library_page_new_album',
style: TextStyle( style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ).tr(),
) )
], ],
), ),
@@ -85,13 +84,13 @@ class LibraryPage extends HookConsumerWidget {
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
_buildAppBar(), _buildAppBar(),
const SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: Text( child: const Text(
"Albums", 'library_page_albums',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ).tr(),
), ),
), ),
SliverPadding( SliverPadding(

View File

@@ -136,9 +136,9 @@ class SelectUserForSharingPage extends HookConsumerWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text( title: Text(
'share_invite', 'share_invite',
style: TextStyle(color: Colors.black), style: TextStyle(color: Theme.of(context).primaryColor),
).tr(), ).tr(),
elevation: 0, elevation: 0,
centerTitle: false, centerTitle: false,
@@ -150,11 +150,18 @@ class SelectUserForSharingPage extends HookConsumerWidget {
), ),
actions: [ actions: [
TextButton( TextButton(
style: TextButton.styleFrom(
primary: Theme.of(context).primaryColor,
),
onPressed: onPressed:
sharedUsersList.value.isEmpty ? null : _createSharedAlbum, sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
child: const Text( child: const Text(
"share_create_album", "share_create_album",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
// color: Theme.of(context).primaryColor,
),
).tr(), ).tr(),
) )
], ],

View File

@@ -61,11 +61,9 @@ class SharingPage extends HookConsumerWidget {
sharedAlbums[index].albumName, sharedAlbums[index].albumName,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontSize: 16, fontWeight: FontWeight.bold,
fontWeight: FontWeight.bold, ),
color: Colors.grey.shade800,
),
), ),
onTap: () { onTap: () {
AutoRouter.of(context) AutoRouter.of(context)
@@ -87,7 +85,7 @@ class SharingPage extends HookConsumerWidget {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), // if you need this borderRadius: BorderRadius.circular(10), // if you need this
side: const BorderSide( side: const BorderSide(
color: Colors.black12, color: Colors.grey,
width: 1, width: 1,
), ),
), ),
@@ -97,30 +95,26 @@ class SharingPage extends HookConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( const Padding(
padding: const EdgeInsets.only(left: 5.0, bottom: 5), padding: EdgeInsets.only(left: 5.0, bottom: 5),
child: Icon( child: Icon(
Icons.offline_share_outlined, Icons.offline_share_outlined,
size: 50, size: 50,
color: Theme.of(context).primaryColor.withAlpha(200), // color: Theme.of(context).primaryColor,
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
'sharing_page_empty_list', 'sharing_page_empty_list',
style: TextStyle( style: Theme.of(context).textTheme.headline3,
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
).tr(), ).tr(),
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
'sharing_page_description', 'sharing_page_description',
style: TextStyle(fontSize: 12, color: Colors.grey[700]), style: Theme.of(context).textTheme.bodyMedium,
).tr(), ).tr(),
), ),
], ],

View File

@@ -1,15 +1,19 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart'; import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
import 'package:immich_mobile/shared/services/share.service.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/shared/ui/share_dialog.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> { class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
final ImageViewerService _imageViewerService; final ImageViewerService _imageViewerService;
final ShareService _shareService;
ImageViewerStateNotifier(this._imageViewerService) ImageViewerStateNotifier(this._imageViewerService, this._shareService)
: super( : super(
ImageViewerPageState( ImageViewerPageState(
downloadAssetStatus: DownloadAssetStatus.idle, downloadAssetStatus: DownloadAssetStatus.idle,
@@ -42,9 +46,23 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle); state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle);
} }
void shareAsset(AssetResponseDto asset, BuildContext context) async {
showDialog(
context: context,
builder: (BuildContext buildContext) {
_shareService
.shareAsset(asset)
.then((_) => Navigator.of(buildContext).pop());
return const ShareDialog();
},
barrierDismissible: false,
);
}
} }
final imageViewerStateProvider = final imageViewerStateProvider =
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>( StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
((ref) => ImageViewerStateNotifier(ref.watch(imageViewerServiceProvider))), ((ref) => ImageViewerStateNotifier(
ref.watch(imageViewerServiceProvider), ref.watch(shareServiceProvider))),
); );

View File

@@ -3,7 +3,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
enum _RemoteImageStatus { empty, thumbnail, full } enum _RemoteImageStatus { empty, thumbnail, preview, full }
class _RemotePhotoViewState extends State<RemotePhotoView> { class _RemotePhotoViewState extends State<RemotePhotoView> {
late CachedNetworkImageProvider _imageProvider; late CachedNetworkImageProvider _imageProvider;
@@ -15,13 +15,16 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool allowMoving = _status == _RemoteImageStatus.full; bool allowMoving = _status == _RemoteImageStatus.full;
return PhotoView(
imageProvider: _imageProvider, return IgnorePointer(
minScale: PhotoViewComputedScale.contained, ignoring: !allowMoving,
maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained, child: PhotoView(
enablePanAlways: true, imageProvider: _imageProvider,
scaleStateChangedCallback: _scaleStateChanged, minScale: PhotoViewComputedScale.contained,
onScaleEnd: _onScaleListener, enablePanAlways: true,
scaleStateChangedCallback: _scaleStateChanged,
onScaleEnd: _onScaleListener,
),
); );
} }
@@ -52,6 +55,14 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
widget.isZoomedFunction(); widget.isZoomedFunction();
} }
void _fireStartLoadingEvent() {
widget.onLoadingStart();
}
void _fireFinishedLoadingEvent() {
widget.onLoadingCompleted();
}
CachedNetworkImageProvider _authorizedImageProvider(String url) { CachedNetworkImageProvider _authorizedImageProvider(String url) {
return CachedNetworkImageProvider( return CachedNetworkImageProvider(
url, url,
@@ -64,14 +75,25 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
_RemoteImageStatus newStatus, _RemoteImageStatus newStatus,
CachedNetworkImageProvider provider, CachedNetworkImageProvider provider,
) { ) {
// Transition to same status is forbidden
if (_status == newStatus) return; if (_status == newStatus) return;
// Transition full -> thumbnail is forbidden
if (_status == _RemoteImageStatus.full && if (_status == _RemoteImageStatus.full &&
newStatus == _RemoteImageStatus.thumbnail) return; newStatus == _RemoteImageStatus.thumbnail) return;
if (_status == _RemoteImageStatus.preview &&
newStatus == _RemoteImageStatus.thumbnail) return;
if (_status == _RemoteImageStatus.full &&
newStatus == _RemoteImageStatus.preview) return;
if (!mounted) return; if (!mounted) return;
if (newStatus != _RemoteImageStatus.full) {
_fireStartLoadingEvent();
} else {
_fireFinishedLoadingEvent();
}
setState(() { setState(() {
_status = newStatus; _status = newStatus;
_imageProvider = provider; _imageProvider = provider;
@@ -92,6 +114,16 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
}), }),
); );
if (widget.previewUrl != null) {
CachedNetworkImageProvider previewProvider =
_authorizedImageProvider(widget.previewUrl!);
previewProvider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo imageInfo, _) {
_performStateTransition(_RemoteImageStatus.preview, previewProvider);
}),
);
}
CachedNetworkImageProvider fullProvider = CachedNetworkImageProvider fullProvider =
_authorizedImageProvider(widget.imageUrl); _authorizedImageProvider(widget.imageUrl);
fullProvider.resolve(const ImageConfiguration()).addListener( fullProvider.resolve(const ImageConfiguration()).addListener(
@@ -118,11 +150,17 @@ class RemotePhotoView extends StatefulWidget {
required this.isZoomedListener, required this.isZoomedListener,
required this.onSwipeDown, required this.onSwipeDown,
required this.onSwipeUp, required this.onSwipeUp,
this.previewUrl,
required this.onLoadingCompleted,
required this.onLoadingStart,
}) : super(key: key); }) : super(key: key);
final String thumbnailUrl; final String thumbnailUrl;
final String imageUrl; final String imageUrl;
final String authToken; final String authToken;
final String? previewUrl;
final Function onLoadingCompleted;
final Function onLoadingStart;
final void Function() onSwipeDown; final void Function() onSwipeDown;
final void Function() onSwipeUp; final void Function() onSwipeUp;

View File

@@ -11,11 +11,15 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
required this.asset, required this.asset,
required this.onMoreInfoPressed, required this.onMoreInfoPressed,
required this.onDownloadPressed, required this.onDownloadPressed,
required this.onSharePressed,
this.loading = false
}) : super(key: key); }) : super(key: key);
final AssetResponseDto asset; final AssetResponseDto asset;
final Function onMoreInfoPressed; final Function onMoreInfoPressed;
final Function onDownloadPressed; final Function onDownloadPressed;
final Function onSharePressed;
final bool loading;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -35,6 +39,14 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
), ),
), ),
actions: [ actions: [
if (loading) Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 15.0),
width: iconSize,
height: iconSize,
child: const CircularProgressIndicator(strokeWidth: 2.0),
),
) ,
IconButton( IconButton(
iconSize: iconSize, iconSize: iconSize,
splashRadius: iconSize, splashRadius: iconSize,
@@ -53,6 +65,14 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
? const Icon(Icons.favorite_rounded) ? const Icon(Icons.favorite_rounded)
: const Icon(Icons.favorite_border_rounded), : const Icon(Icons.favorite_border_rounded),
), ),
IconButton(
iconSize: iconSize,
splashRadius: iconSize,
onPressed: () {
onSharePressed();
},
icon: const Icon(Icons.share),
),
IconButton( IconButton(
iconSize: iconSize, iconSize: iconSize,
splashRadius: iconSize, splashRadius: iconSize,

View File

@@ -11,36 +11,51 @@ import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart'; import 'package:immich_mobile/modules/home/services/asset.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:openapi/api.dart'; import 'package:openapi/api.dart';
// ignore: must_be_immutable // ignore: must_be_immutable
class GalleryViewerPage extends HookConsumerWidget { class GalleryViewerPage extends HookConsumerWidget {
late List<AssetResponseDto> assetList; late List<AssetResponseDto> assetList;
final AssetResponseDto asset; final AssetResponseDto asset;
final String thumbnailRequestUrl;
GalleryViewerPage({ GalleryViewerPage({
Key? key, Key? key,
required this.assetList, required this.assetList,
required this.asset, required this.asset,
required this.thumbnailRequestUrl,
}) : super(key: key); }) : super(key: key);
AssetResponseDto? assetDetail; AssetResponseDto? assetDetail;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final Box<dynamic> box = Hive.box(userInfoBox); final Box<dynamic> box = Hive.box(userInfoBox);
final appSettingService = ref.watch(appSettingsServiceProvider);
final threeStageLoading = useState(false);
final loading = useState(false);
final isZoomed = useState<bool>(false);
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
int indexOfAsset = assetList.indexOf(asset); int indexOfAsset = assetList.indexOf(asset);
@override
void initState(int index) {
indexOfAsset = index;
}
PageController controller = PageController controller =
PageController(initialPage: assetList.indexOf(asset)); PageController(initialPage: assetList.indexOf(asset));
useEffect(
() {
threeStageLoading.value = appSettingService
.getSetting<bool>(AppSettingsEnum.threeStageLoading);
return null;
},
[],
);
@override
initState(int index) {
indexOfAsset = index;
}
getAssetExif() async { getAssetExif() async {
assetDetail = await ref assetDetail = await ref
.watch(assetServiceProvider) .watch(assetServiceProvider)
@@ -59,9 +74,6 @@ class GalleryViewerPage extends HookConsumerWidget {
); );
} }
final isZoomed = useState<bool>(false);
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
//make isZoomed listener call instead //make isZoomed listener call instead
void isZoomedMethod() { void isZoomedMethod() {
if (isZoomedListener.value) { if (isZoomedListener.value) {
@@ -74,6 +86,7 @@ class GalleryViewerPage extends HookConsumerWidget {
return Scaffold( return Scaffold(
backgroundColor: Colors.black, backgroundColor: Colors.black,
appBar: TopControlAppBar( appBar: TopControlAppBar(
loading: loading.value,
asset: assetList[indexOfAsset], asset: assetList[indexOfAsset],
onMoreInfoPressed: () { onMoreInfoPressed: () {
showInfo(); showInfo();
@@ -83,6 +96,11 @@ class GalleryViewerPage extends HookConsumerWidget {
.watch(imageViewerStateProvider.notifier) .watch(imageViewerStateProvider.notifier)
.downloadAsset(assetList[indexOfAsset], context); .downloadAsset(assetList[indexOfAsset], context);
}, },
onSharePressed: () {
ref
.watch(imageViewerStateProvider.notifier)
.shareAsset(assetList[indexOfAsset], context);
},
), ),
body: SafeArea( body: SafeArea(
child: PageView.builder( child: PageView.builder(
@@ -95,18 +113,19 @@ class GalleryViewerPage extends HookConsumerWidget {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemBuilder: (context, index) { itemBuilder: (context, index) {
initState(index); initState(index);
getAssetExif(); getAssetExif();
if (assetList[index].type == AssetTypeEnum.IMAGE) { if (assetList[index].type == AssetTypeEnum.IMAGE) {
return ImageViewerPage( return ImageViewerPage(
thumbnailUrl:
'${box.get(serverEndpointKey)}/asset/thumbnail/${assetList[index].id}',
imageUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${assetList[index].deviceAssetId}&did=${assetList[index].deviceId}&isThumb=false',
authToken: 'Bearer ${box.get(accessTokenKey)}', authToken: 'Bearer ${box.get(accessTokenKey)}',
isZoomedFunction: isZoomedMethod, isZoomedFunction: isZoomedMethod,
isZoomedListener: isZoomedListener, isZoomedListener: isZoomedListener,
onLoadingCompleted: () => {},
onLoadingStart: () => {},
asset: assetList[index], asset: assetList[index],
heroTag: assetList[index].id, heroTag: assetList[index].id,
threeStageLoading: threeStageLoading.value,
); );
} else { } else {
return SwipeDetector( return SwipeDetector(

View File

@@ -8,30 +8,34 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart'; import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
// ignore: must_be_immutable // ignore: must_be_immutable
class ImageViewerPage extends HookConsumerWidget { class ImageViewerPage extends HookConsumerWidget {
final String imageUrl;
final String heroTag; final String heroTag;
final String thumbnailUrl;
final AssetResponseDto asset; final AssetResponseDto asset;
final String authToken; final String authToken;
final ValueNotifier<bool> isZoomedListener; final ValueNotifier<bool> isZoomedListener;
final void Function() isZoomedFunction; final void Function() isZoomedFunction;
final void Function() onLoadingCompleted;
final void Function() onLoadingStart;
final bool threeStageLoading;
ImageViewerPage({ ImageViewerPage({
Key? key, Key? key,
required this.imageUrl,
required this.heroTag, required this.heroTag,
required this.thumbnailUrl,
required this.asset, required this.asset,
required this.authToken, required this.authToken,
required this.isZoomedFunction, required this.isZoomedFunction,
required this.isZoomedListener, required this.isZoomedListener,
required this.onLoadingCompleted,
required this.onLoadingStart,
required this.threeStageLoading,
}) : super(key: key); }) : super(key: key);
AssetResponseDto? assetDetail; AssetResponseDto? assetDetail;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final downloadAssetStatus = final downloadAssetStatus =
@@ -68,13 +72,18 @@ class ImageViewerPage extends HookConsumerWidget {
child: Hero( child: Hero(
tag: heroTag, tag: heroTag,
child: RemotePhotoView( child: RemotePhotoView(
thumbnailUrl: thumbnailUrl, thumbnailUrl: getThumbnailUrl(asset),
imageUrl: imageUrl, imageUrl: getImageUrl(asset),
previewUrl: threeStageLoading
? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG)
: null,
authToken: authToken, authToken: authToken,
isZoomedFunction: isZoomedFunction, isZoomedFunction: isZoomedFunction,
isZoomedListener: isZoomedListener, isZoomedListener: isZoomedListener,
onSwipeDown: () => AutoRouter.of(context).pop(), onSwipeDown: () => AutoRouter.of(context).pop(),
onSwipeUp: () => showInfo(), onSwipeUp: () => showInfo(),
onLoadingCompleted: onLoadingCompleted,
onLoadingStart: onLoadingStart,
), ),
), ),
), ),

View File

@@ -24,6 +24,7 @@ class AlbumInfoCard extends HookConsumerWidget {
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo); ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
final bool isExcluded = final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo); ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
ColorFilter selectedFilter = ColorFilter.mode( ColorFilter selectedFilter = ColorFilter.mode(
Theme.of(context).primaryColor.withAlpha(100), Theme.of(context).primaryColor.withAlpha(100),
@@ -39,11 +40,11 @@ class AlbumInfoCard extends HookConsumerWidget {
return Chip( return Chip(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
label: const Text( label: Text(
"album_info_card_backup_album_included", "album_info_card_backup_album_included",
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.white, color: isDarkTheme ? Colors.black : Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
).tr(), ).tr(),
@@ -53,11 +54,11 @@ class AlbumInfoCard extends HookConsumerWidget {
return Chip( return Chip(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
label: const Text( label: Text(
"album_info_card_backup_album_excluded", "album_info_card_backup_album_excluded",
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.white, color: isDarkTheme ? Colors.black : Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
).tr(), ).tr(),
@@ -141,8 +142,10 @@ class AlbumInfoCard extends HookConsumerWidget {
margin: const EdgeInsets.all(1), margin: const EdgeInsets.all(1),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), // if you need this borderRadius: BorderRadius.circular(12), // if you need this
side: const BorderSide( side: BorderSide(
color: Color(0xFFC9C9C9), color: isDarkTheme
? const Color.fromARGB(255, 37, 35, 35)
: const Color(0xFFC9C9C9),
width: 1, width: 1,
), ),
), ),
@@ -219,8 +222,9 @@ class AlbumInfoCard extends HookConsumerWidget {
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
AutoRouter.of(context) AutoRouter.of(context).push(
.push(AlbumPreviewRoute(album: albumInfo)); AlbumPreviewRoute(album: albumInfo),
);
}, },
icon: Icon( icon: Icon(
Icons.image_outlined, Icons.image_outlined,

View File

@@ -35,7 +35,7 @@ class BackupInfoCard extends StatelessWidget {
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: Text( child: Text(
subtitle, subtitle,
style: const TextStyle(color: Color(0xFF808080), fontSize: 12), style: const TextStyle(fontSize: 12),
), ),
), ),
trailing: Column( trailing: Column(

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart'; import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/ui/album_info_card.dart'; import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
@@ -16,6 +17,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
final availableAlbums = ref.watch(backupProvider).availableAlbums; final availableAlbums = ref.watch(backupProvider).availableAlbums;
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
useEffect( useEffect(
() { () {
@@ -81,14 +83,16 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
), ),
label: Text( label: Text(
album.name, album.name,
style: const TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.white, color: Theme.of(context).brightness == Brightness.dark
? Colors.black
: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
deleteIconColor: Colors.white, deleteIconColor: isDarkTheme ? Colors.black : Colors.white,
deleteIcon: const Icon( deleteIcon: const Icon(
Icons.cancel_rounded, Icons.cancel_rounded,
size: 15, size: 15,
@@ -119,14 +123,15 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
), ),
label: Text( label: Text(
album.name, album.name,
style: const TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.white, color: isDarkTheme ? Colors.black : immichBackgroundColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
backgroundColor: Colors.red[300], backgroundColor: Colors.red[300],
deleteIconColor: Colors.white, deleteIconColor:
isDarkTheme ? Colors.black : immichBackgroundColor,
deleteIcon: const Icon( deleteIcon: const Icon(
Icons.cancel_rounded, Icons.cancel_rounded,
size: 15, size: 15,
@@ -154,11 +159,16 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
children: [ children: [
Padding( Padding(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), vertical: 8.0,
horizontal: 16.0,
),
child: const Text( child: const Text(
"backup_album_selection_page_selection_info", "backup_album_selection_page_selection_info",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14), style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
).tr(), ).tr(),
), ),
// Selected Album Chips // Selected Album Chips
@@ -178,9 +188,11 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
child: Card( child: Card(
margin: const EdgeInsets.all(0), margin: const EdgeInsets.all(0),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), // if you need this borderRadius: BorderRadius.circular(5),
side: const BorderSide( side: BorderSide(
color: Color.fromARGB(255, 235, 235, 235), color: isDarkTheme
? const Color.fromARGB(255, 0, 0, 0)
: const Color.fromARGB(255, 235, 235, 235),
width: 1, width: 1,
), ),
), ),
@@ -190,12 +202,11 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
children: [ children: [
ListTile( ListTile(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
title: Text( title: const Text(
"backup_album_selection_page_total_assets", "backup_album_selection_page_total_assets",
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
color: Colors.grey[700],
), ),
).tr(), ).tr(),
trailing: Text( trailing: Text(
@@ -257,11 +268,10 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
content: SingleChildScrollView( content: SingleChildScrollView(
child: ListBody( child: ListBody(
children: [ children: [
Text( const Text(
'backup_album_selection_page_assets_scatter', 'backup_album_selection_page_assets_scatter',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.grey[700],
), ),
).tr(), ).tr(),
], ],

View File

@@ -82,7 +82,7 @@ class BackupControllerPage extends HookConsumerWidget {
); );
} }
ListTile _buildBackupController() { ListTile _buildAutoBackupController() {
var backUpOption = authenticationState.deviceInfo.isAutoBackup var backUpOption = authenticationState.deviceInfo.isAutoBackup
? "backup_controller_page_status_on".tr() ? "backup_controller_page_status_on".tr()
: "backup_controller_page_status_off".tr(); : "backup_controller_page_status_off".tr();
@@ -114,13 +114,7 @@ class BackupControllerPage extends HookConsumerWidget {
).tr(), ).tr(),
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: OutlinedButton( child: ElevatedButton(
style: OutlinedButton.styleFrom(
side: const BorderSide(
width: 1,
color: Color.fromARGB(255, 220, 220, 220),
),
),
onPressed: () { onPressed: () {
if (isAutoBackup) { if (isAutoBackup) {
ref ref
@@ -134,7 +128,10 @@ class BackupControllerPage extends HookConsumerWidget {
}, },
child: Text( child: Text(
backupBtnText, backupBtnText,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
), ),
), ),
) )
@@ -232,33 +229,24 @@ class BackupControllerPage extends HookConsumerWidget {
children: [ children: [
const Text( const Text(
"backup_controller_page_to_backup", "backup_controller_page_to_backup",
style: TextStyle(color: Color(0xFF808080), fontSize: 12), style: TextStyle(fontSize: 12),
).tr(), ).tr(),
_buildSelectedAlbumName(), _buildSelectedAlbumName(),
_buildExcludedAlbumName() _buildExcludedAlbumName()
], ],
), ),
), ),
trailing: OutlinedButton( trailing: ElevatedButton(
style: OutlinedButton.styleFrom(
enableFeedback: true,
side: const BorderSide(
width: 1,
color: Color.fromARGB(255, 220, 220, 220),
),
),
onPressed: () { onPressed: () {
AutoRouter.of(context).push(const BackupAlbumSelectionRoute()); AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
}, },
child: Padding( child: const Text(
padding: const EdgeInsets.symmetric( "backup_controller_page_select",
vertical: 16.0, style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
), ),
child: const Text( ).tr(),
"backup_controller_page_select",
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
),
), ),
), ),
); );
@@ -324,14 +312,14 @@ class BackupControllerPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: Table( child: Table(
border: TableBorder.all( border: TableBorder.all(
color: Colors.black12, color: Theme.of(context).primaryColorLight,
width: 1, width: 1,
), ),
children: [ children: [
TableRow( TableRow(
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.grey[100], // color: Colors.grey[100],
), ),
children: [ children: [
TableCell( TableCell(
verticalAlignment: TableCellVerticalAlignment.middle, verticalAlignment: TableCellVerticalAlignment.middle,
@@ -355,9 +343,9 @@ class BackupControllerPage extends HookConsumerWidget {
], ],
), ),
TableRow( TableRow(
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.grey[200], // color: Colors.grey[200],
), ),
children: [ children: [
TableCell( TableCell(
verticalAlignment: TableCellVerticalAlignment.middle, verticalAlignment: TableCellVerticalAlignment.middle,
@@ -384,9 +372,9 @@ class BackupControllerPage extends HookConsumerWidget {
], ],
), ),
TableRow( TableRow(
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.grey[100], // color: Colors.grey[100],
), ),
children: [ children: [
TableCell( TableCell(
child: Padding( child: Padding(
@@ -463,7 +451,7 @@ class BackupControllerPage extends HookConsumerWidget {
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}", "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
), ),
const Divider(), const Divider(),
_buildBackupController(), _buildAutoBackupController(),
const Divider(), const Divider(),
_buildStorageInformation(), _buildStorageInformation(),
const Divider(), const Divider(),
@@ -479,7 +467,7 @@ class BackupControllerPage extends HookConsumerWidget {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
primary: Colors.red[300], primary: Colors.red[300],
onPrimary: Colors.grey[50], onPrimary: Colors.grey[50],
padding: const EdgeInsets.all(14), // padding: const EdgeInsets.all(14),
), ),
onPressed: () { onPressed: () {
ref.read(backupProvider.notifier).cancelBackup(); ref.read(backupProvider.notifier).cancelBackup();
@@ -493,11 +481,6 @@ class BackupControllerPage extends HookConsumerWidget {
).tr(), ).tr(),
) )
: ElevatedButton( : ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).primaryColor,
onPrimary: Colors.grey[50],
padding: const EdgeInsets.all(14),
),
onPressed: shouldBackup ? startBackup : null, onPressed: shouldBackup ? startBackup : null,
child: const Text( child: const Text(
"backup_controller_page_start_backup", "backup_controller_page_start_backup",

View File

@@ -1,9 +1,16 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/models/home_page_state.model.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'; import 'package:openapi/api.dart';
class HomePageStateNotifier extends StateNotifier<HomePageState> { class HomePageStateNotifier extends StateNotifier<HomePageState> {
HomePageStateNotifier()
final ShareService _shareService;
HomePageStateNotifier(this._shareService)
: super( : super(
HomePageState( HomePageState(
isMultiSelectEnable: false, isMultiSelectEnable: false,
@@ -64,9 +71,22 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
state = state.copyWith(selectedItems: currentList); 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 = final homePageStateProvider =
StateNotifierProvider<HomePageStateNotifier, HomePageState>( StateNotifierProvider<HomePageStateNotifier, HomePageState>(
((ref) => HomePageStateNotifier()), ((ref) => HomePageStateNotifier(ref.watch(shareServiceProvider))),
); );

View File

@@ -1,12 +1,14 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package: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'; import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
class ControlBottomAppBar extends StatelessWidget { class ControlBottomAppBar extends ConsumerWidget {
const ControlBottomAppBar({Key? key}) : super(key: key); const ControlBottomAppBar({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
return Positioned( return Positioned(
bottom: 0, bottom: 0,
left: 0, left: 0,
@@ -15,17 +17,17 @@ class ControlBottomAppBar extends StatelessWidget {
height: MediaQuery.of(context).size.height * 0.15, height: MediaQuery.of(context).size.height * 0.15,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(15), topLeft: Radius.circular(8),
topRight: Radius.circular(15), topRight: Radius.circular(8),
), ),
color: Colors.grey[300]?.withOpacity(0.98), color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.95),
), ),
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
ControlBoxButton( ControlBoxButton(
iconData: Icons.delete_forever_rounded, iconData: Icons.delete_forever_rounded,
@@ -39,6 +41,20 @@ class ControlBottomAppBar extends StatelessWidget {
); );
}, },
), ),
ControlBoxButton(
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();
},
),
], ],
), ),
) )
@@ -67,7 +83,7 @@ class ControlBoxButton extends StatelessWidget {
width: 60, width: 60,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
IconButton( IconButton(
onPressed: () { onPressed: () {

View File

@@ -86,7 +86,6 @@ class DailyTitleText extends ConsumerWidget {
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.black87,
), ),
), ),
const Spacer(), const Spacer(),

View File

@@ -14,32 +14,22 @@ class DisableMultiSelectButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Positioned( return Positioned(
top: 0, top: 10,
left: 0, left: 0,
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 16.0, top: 46), padding: const EdgeInsets.only(left: 16.0, top: 46),
child: Material( child: Padding(
elevation: 20, padding: const EdgeInsets.symmetric(horizontal: 4.0),
borderRadius: BorderRadius.circular(35), child: ElevatedButton.icon(
child: Container( onPressed: () {
decoration: BoxDecoration( onPressed();
borderRadius: BorderRadius.circular(35), },
color: Colors.grey[100], icon: const Icon(Icons.close_rounded),
), label: Text(
child: Padding( '$selectedItemCount',
padding: const EdgeInsets.symmetric(horizontal: 4.0), style: const TextStyle(
child: TextButton.icon( fontWeight: FontWeight.w600,
onPressed: () { fontSize: 18,
onPressed();
},
icon: const Icon(Icons.close_rounded),
label: Text(
'$selectedItemCount',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
),
),
), ),
), ),
), ),

View File

@@ -30,6 +30,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
floating: true, floating: true,
pinned: false, pinned: false,
snap: false, snap: false,
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)), borderRadius: BorderRadius.all(Radius.circular(5)),
), ),
@@ -57,7 +58,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
child: GestureDetector( child: GestureDetector(
onTap: () => Scaffold.of(context).openDrawer(), onTap: () => Scaffold.of(context).openDrawer(),
child: Material( child: Material(
color: Colors.grey[200], // color: Colors.grey[200],
elevation: 1, elevation: 1,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50.0), borderRadius: BorderRadius.circular(50.0),
@@ -77,13 +78,12 @@ class ImmichSliverAppBar extends ConsumerWidget {
); );
}, },
), ),
title: Text( title: const Text(
'IMMICH', 'IMMICH',
style: TextStyle( style: TextStyle(
fontFamily: 'SnowburstOne', fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 22, fontSize: 22,
color: Theme.of(context).primaryColor,
), ),
), ),
actions: [ actions: [
@@ -112,12 +112,13 @@ class ImmichSliverAppBar extends ConsumerWidget {
? const Icon(Icons.backup_rounded) ? const Icon(Icons.backup_rounded)
: Badge( : Badge(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
elevation: 2, elevation: 3,
position: BadgePosition.bottomEnd(bottom: -4, end: -4), position: BadgePosition.bottomEnd(bottom: -4, end: -4),
badgeColor: Colors.white, badgeColor: Colors.white,
badgeContent: const Icon( badgeContent: const Icon(
Icons.cloud_off_rounded, Icons.cloud_off_rounded,
size: 8, size: 8,
color: Colors.indigo,
), ),
child: const Icon(Icons.backup_rounded), child: const Icon(Icons.backup_rounded),
), ),

View File

@@ -22,7 +22,7 @@ class MonthlyTitleText extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: 26, fontSize: 26,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor, color: Theme.of(context).textTheme.headline1?.color,
), ),
), ),
), ),

View File

@@ -1,303 +0,0 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.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/ui/immich_loading_indicator.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'dart:math';
class ProfileDrawer extends HookConsumerWidget {
const ProfileDrawer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
AuthenticationState authState = ref.watch(authenticationProvider);
ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
final uploadProfileImageStatus =
ref.watch(uploadProfileImageProvider).status;
final appInfo = useState({});
var dummmy = Random().nextInt(1024);
_getPackageInfo() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
appInfo.value = {
"version": packageInfo.version,
"buildNumber": packageInfo.buildNumber,
};
}
_buildUserProfileImage() {
if (authState.profileImagePath.isEmpty) {
return const CircleAvatar(
radius: 35,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
}
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
if (authState.profileImagePath.isNotEmpty) {
return CircleAvatar(
radius: 35,
backgroundImage: NetworkImage(
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
),
backgroundColor: Colors.transparent,
);
} else {
return const CircleAvatar(
radius: 35,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
}
}
if (uploadProfileImageStatus == UploadProfileStatus.success) {
return CircleAvatar(
radius: 35,
backgroundImage: NetworkImage(
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
),
backgroundColor: Colors.transparent,
);
}
if (uploadProfileImageStatus == UploadProfileStatus.failure) {
return const CircleAvatar(
radius: 35,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
}
if (uploadProfileImageStatus == UploadProfileStatus.loading) {
return const ImmichLoadingIndicator();
}
return const SizedBox();
}
_pickUserProfileImage() async {
final XFile? image = await ImagePicker().pickImage(
source: ImageSource.gallery,
maxHeight: 1024,
maxWidth: 1024,
);
if (image != null) {
var success =
await ref.watch(uploadProfileImageProvider.notifier).upload(image);
if (success) {
ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
ref.read(uploadProfileImageProvider).profileImagePath,
);
}
}
}
useEffect(
() {
_getPackageInfo();
_buildUserProfileImage();
return null;
},
[],
);
return Drawer(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ListView(
shrinkWrap: true,
padding: EdgeInsets.zero,
children: [
DrawerHeader(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color.fromARGB(255, 216, 219, 238),
Color.fromARGB(255, 226, 230, 231)
],
begin: Alignment.centerRight,
end: Alignment.centerLeft,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
clipBehavior: Clip.none,
children: [
_buildUserProfileImage(),
Positioned(
bottom: 0,
right: -5,
child: GestureDetector(
onTap: _pickUserProfileImage,
child: Material(
color: Colors.grey[50],
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50.0),
),
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Icon(
Icons.edit,
color: Theme.of(context).primaryColor,
size: 14,
),
),
),
),
),
],
),
Text(
"${authState.firstName} ${authState.lastName}",
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
fontSize: 24,
),
),
Text(
authState.userEmail,
style: TextStyle(color: Colors.grey[800], fontSize: 12),
)
],
),
),
ListTile(
tileColor: Colors.grey[100],
leading: const Icon(
Icons.logout_rounded,
color: Colors.black54,
),
title: const Text(
"profile_drawer_sign_out",
style: TextStyle(
color: Colors.black54,
fontSize: 14,
fontWeight: FontWeight.bold,
),
).tr(),
onTap: () async {
bool res =
await ref.watch(authenticationProvider.notifier).logout();
if (res) {
ref.watch(backupProvider.notifier).cancelBackup();
ref.watch(assetProvider.notifier).clearAllAsset();
ref.watch(websocketProvider.notifier).disconnect();
// AutoRouter.of(context).popUntilRoot();
AutoRouter.of(context).replace(const LoginRoute());
}
},
)
],
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
elevation: 0,
color: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), // if you need this
side: const BorderSide(
color: Color.fromARGB(101, 201, 201, 201),
width: 1,
),
),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
serverInfoState.isVersionMismatch
? serverInfoState.versionMismatchErrorMessage
: "profile_drawer_client_server_up_to_date".tr(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w600,
),
),
),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"App Version",
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
),
),
Text(
"${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}",
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Server Version",
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
),
),
Text(
"${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch_}",
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
),
)
],
),
);
}
}

View File

@@ -0,0 +1,91 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer_header.dart';
import 'package:immich_mobile/modules/home/ui/profile_drawer/server_info_box.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
class ProfileDrawer extends HookConsumerWidget {
const ProfileDrawer({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
_buildSignoutButton() {
return ListTile(
horizontalTitleGap: 0,
leading: SizedBox(
height: double.infinity,
child: Icon(
Icons.logout_rounded,
color: Theme.of(context).textTheme.labelMedium?.color,
size: 20,
),
),
title: Text(
"profile_drawer_sign_out",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(fontWeight: FontWeight.bold),
).tr(),
onTap: () async {
bool res = await ref.watch(authenticationProvider.notifier).logout();
if (res) {
ref.watch(backupProvider.notifier).cancelBackup();
ref.watch(assetProvider.notifier).clearAllAsset();
ref.watch(websocketProvider.notifier).disconnect();
AutoRouter.of(context).replace(const LoginRoute());
}
},
);
}
_buildSettingButton() {
return ListTile(
horizontalTitleGap: 0,
leading: SizedBox(
height: double.infinity,
child: Icon(
Icons.settings_rounded,
color: Theme.of(context).textTheme.labelMedium?.color,
size: 20,
),
),
title: Text(
"profile_drawer_settings",
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(fontWeight: FontWeight.bold),
).tr(),
onTap: () {
AutoRouter.of(context).push(const SettingsRoute());
},
);
}
return Drawer(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ListView(
shrinkWrap: true,
padding: EdgeInsets.zero,
children: [
const ProfileDrawerHeader(),
_buildSettingButton(),
_buildSignoutButton(),
],
),
const ServerInfoBox()
],
),
);
}
}

View File

@@ -0,0 +1,173 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
class ProfileDrawerHeader extends HookConsumerWidget {
const ProfileDrawerHeader({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
AuthenticationState authState = ref.watch(authenticationProvider);
final uploadProfileImageStatus =
ref.watch(uploadProfileImageProvider).status;
var dummmy = Random().nextInt(1024);
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
_buildUserProfileImage() {
if (authState.profileImagePath.isEmpty) {
return const CircleAvatar(
radius: 35,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
}
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
if (authState.profileImagePath.isNotEmpty) {
return CircleAvatar(
radius: 35,
backgroundImage: NetworkImage(
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
),
backgroundColor: Colors.transparent,
);
} else {
return const CircleAvatar(
radius: 35,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
}
}
if (uploadProfileImageStatus == UploadProfileStatus.success) {
return CircleAvatar(
radius: 35,
backgroundImage: NetworkImage(
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
),
backgroundColor: Colors.transparent,
);
}
if (uploadProfileImageStatus == UploadProfileStatus.failure) {
return const CircleAvatar(
radius: 35,
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
backgroundColor: Colors.transparent,
);
}
if (uploadProfileImageStatus == UploadProfileStatus.loading) {
return const ImmichLoadingIndicator();
}
return const SizedBox();
}
_pickUserProfileImage() async {
final XFile? image = await ImagePicker().pickImage(
source: ImageSource.gallery,
maxHeight: 1024,
maxWidth: 1024,
);
if (image != null) {
var success =
await ref.watch(uploadProfileImageProvider.notifier).upload(image);
if (success) {
ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
ref.read(uploadProfileImageProvider).profileImagePath,
);
}
}
}
useEffect(
() {
_buildUserProfileImage();
return null;
},
[],
);
return DrawerHeader(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: isDarkMode
? [
const Color.fromARGB(255, 22, 25, 48),
const Color.fromARGB(255, 13, 13, 13),
const Color.fromARGB(255, 0, 0, 0),
]
: [
const Color.fromARGB(255, 216, 219, 238),
const Color.fromARGB(255, 242, 242, 242),
Colors.white,
],
begin: Alignment.centerRight,
end: Alignment.centerLeft,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
clipBehavior: Clip.none,
children: [
_buildUserProfileImage(),
Positioned(
bottom: 0,
right: -5,
child: GestureDetector(
onTap: _pickUserProfileImage,
child: Material(
color: Colors.grey[100],
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50.0),
),
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Icon(
Icons.edit,
color: Theme.of(context).primaryColor,
size: 14,
),
),
),
),
),
],
),
Text(
"${authState.firstName} ${authState.lastName}",
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
fontSize: 24,
),
),
Text(
authState.userEmail,
style: Theme.of(context).textTheme.labelMedium,
)
],
),
);
}
}

View File

@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:package_info_plus/package_info_plus.dart';
class ServerInfoBox extends HookConsumerWidget {
const ServerInfoBox({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
final appInfo = useState({});
_getPackageInfo() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
appInfo.value = {
"version": packageInfo.version,
"buildNumber": packageInfo.buildNumber,
};
}
useEffect(
() {
_getPackageInfo();
return null;
},
[],
);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
elevation: 0,
color: Theme.of(context).scaffoldBackgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), // if you need this
side: const BorderSide(
color: Color.fromARGB(101, 201, 201, 201),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
serverInfoState.isVersionMismatch
? serverInfoState.versionMismatchErrorMessage
: "profile_drawer_client_server_up_to_date".tr(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 11,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w600,
),
),
),
const Divider(
color: Color.fromARGB(101, 201, 201, 201),
thickness: 1,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"App Version",
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
),
),
Text(
"${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}",
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(
color: Color.fromARGB(101, 201, 201, 201),
thickness: 1,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Server Version",
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
),
),
Text(
"${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch_}",
style: TextStyle(
fontSize: 11,
color: Colors.grey[500],
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
),
);
}
}

View File

@@ -9,6 +9,7 @@ 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/home/providers/home_page_state.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class ThumbnailImage extends HookConsumerWidget { class ThumbnailImage extends HookConsumerWidget {
@@ -23,8 +24,7 @@ class ThumbnailImage extends HookConsumerWidget {
final cacheKey = useState(1); final cacheKey = useState(1);
var box = Hive.box(userInfoBox); var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = var thumbnailRequestUrl = getThumbnailUrl(asset);
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
var selectedAsset = ref.watch(homePageStateProvider).selectedItems; var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
var isMultiSelectEnable = var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable; ref.watch(homePageStateProvider).isMultiSelectEnable;
@@ -65,7 +65,6 @@ class ThumbnailImage extends HookConsumerWidget {
AutoRouter.of(context).push( AutoRouter.of(context).push(
GalleryViewerRoute( GalleryViewerRoute(
assetList: assetList, assetList: assetList,
thumbnailRequestUrl: thumbnailRequestUrl,
asset: asset, asset: asset,
), ),
); );

View File

@@ -9,7 +9,7 @@ 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/image_grid.dart';
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.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/monthly_title_text.dart';
import 'package:immich_mobile/modules/home/ui/profile_drawer.dart'; import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
import 'package:immich_mobile/shared/providers/asset.provider.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/server_info.provider.dart';
@@ -76,6 +76,7 @@ class HomePage extends HookConsumerWidget {
imageGridGroup.add( imageGridGroup.add(
DailyTitleText( DailyTitleText(
key: Key('${dateGroup.toString()}title'),
isoDate: dateGroup, isoDate: dateGroup,
assetGroup: immichAssetList, assetGroup: immichAssetList,
), ),
@@ -116,9 +117,9 @@ class HomePage extends HookConsumerWidget {
], ],
), ),
Padding( Padding(
padding: const EdgeInsets.only(top: 50.0), padding: const EdgeInsets.only(top: 60.0, bottom: 30.0),
child: DraggableScrollbar.semicircle( child: DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).hintColor,
controller: scrollController, controller: scrollController,
heightScrollThumb: 48.0, heightScrollThumb: 48.0,
child: CustomScrollView( child: CustomScrollView(

View File

@@ -26,7 +26,7 @@ class ThumbnailWithInfo extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: SizedBox( child: SizedBox(
width: MediaQuery.of(context).size.width / 2, width: MediaQuery.of(context).size.width / 3,
child: Stack( child: Stack(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
children: [ children: [
@@ -58,7 +58,7 @@ class ThumbnailWithInfo extends StatelessWidget {
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16, fontSize: 14,
), ),
), ),
), ),

View File

@@ -29,6 +29,8 @@ class SearchPage extends HookConsumerWidget {
AsyncValue<List<CuratedObjectsResponseDto>> curatedObjects = AsyncValue<List<CuratedObjectsResponseDto>> curatedObjects =
ref.watch(getCuratedObjectProvider); ref.watch(getCuratedObjectProvider);
double imageSize = MediaQuery.of(context).size.width / 3;
useEffect( useEffect(
() { () {
searchFocusNode = FocusNode(); searchFocusNode = FocusNode();
@@ -46,15 +48,15 @@ class SearchPage extends HookConsumerWidget {
_buildPlaces() { _buildPlaces() {
return curatedLocation.when( return curatedLocation.when(
loading: () => const SizedBox( loading: () => SizedBox(
height: 200, height: imageSize,
child: Center(child: ImmichLoadingIndicator()), child: const Center(child: ImmichLoadingIndicator()),
), ),
error: (err, stack) => Text('Error: $err'), error: (err, stack) => Text('Error: $err'),
data: (curatedLocations) { data: (curatedLocations) {
return curatedLocations.isNotEmpty return curatedLocations.isNotEmpty
? SizedBox( ? SizedBox(
height: MediaQuery.of(context).size.width / 2, height: imageSize,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(left: 16), padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@@ -76,7 +78,7 @@ class SearchPage extends HookConsumerWidget {
), ),
) )
: SizedBox( : SizedBox(
height: MediaQuery.of(context).size.width / 2, height: imageSize,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(left: 16), padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@@ -105,7 +107,7 @@ class SearchPage extends HookConsumerWidget {
data: (objects) { data: (objects) {
return objects.isNotEmpty return objects.isNotEmpty
? SizedBox( ? SizedBox(
height: MediaQuery.of(context).size.width / 2, height: imageSize,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(left: 16), padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@@ -113,7 +115,7 @@ class SearchPage extends HookConsumerWidget {
itemBuilder: ((context, index) { itemBuilder: ((context, index) {
var curatedObjectInfo = objects[index]; var curatedObjectInfo = objects[index];
var thumbnailRequestUrl = var thumbnailRequestUrl =
'${box.get(serverEndpointKey)}/asset/file?aid=${curatedObjectInfo.deviceAssetId}&did=${curatedObjectInfo.deviceId}&isThumb=true'; '${box.get(serverEndpointKey)}/asset/thumbnail/${curatedObjectInfo.id}';
return ThumbnailWithInfo( return ThumbnailWithInfo(
imageUrl: thumbnailRequestUrl, imageUrl: thumbnailRequestUrl,
@@ -131,7 +133,8 @@ class SearchPage extends HookConsumerWidget {
), ),
) )
: SizedBox( : SizedBox(
height: MediaQuery.of(context).size.width / 2, // height: imageSize,
width: imageSize,
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(left: 16), padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@@ -163,12 +166,13 @@ class SearchPage extends HookConsumerWidget {
child: Stack( child: Stack(
children: [ children: [
ListView( ListView(
shrinkWrap: true,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: const Text( child: const Text(
"search_page_places", "search_page_places",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
).tr(), ).tr(),
), ),
_buildPlaces(), _buildPlaces(),
@@ -176,7 +180,7 @@ class SearchPage extends HookConsumerWidget {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: const Text( child: const Text(
"search_page_things", "search_page_things",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
).tr(), ).tr(),
), ),
_buildThings() _buildThings()

View File

@@ -172,7 +172,7 @@ class SearchResultPage extends HookConsumerWidget {
}); });
return DraggableScrollbar.semicircle( return DraggableScrollbar.semicircle(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).hintColor,
controller: scrollController, controller: scrollController,
heightScrollThumb: 48.0, heightScrollThumb: 48.0,
child: CustomScrollView( child: CustomScrollView(

View File

@@ -0,0 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
final appSettingsServiceProvider = Provider((ref) => AppSettingsService());

View File

@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
enum AppSettingsEnum {
threeStageLoading, // true, false,
themeMode, // "light","dark","system"
}
class AppSettingsService {
late final Box hiveBox;
AppSettingsService() {
hiveBox = Hive.box(userSettingInfoBox);
}
T getSetting<T>(AppSettingsEnum settingType) {
var settingKey = _settingHiveBoxKeyLookup(settingType);
if (!hiveBox.containsKey(settingKey)) {
T defaultSetting = _setDefaultSetting(settingType);
return defaultSetting;
}
var result = hiveBox.get(settingKey);
if (result is T) {
return result;
} else {
debugPrint("Incorrect setting type");
throw TypeError();
}
}
setSetting<T>(AppSettingsEnum settingType, T value) {
var settingKey = _settingHiveBoxKeyLookup(settingType);
if (hiveBox.containsKey(settingKey)) {
var result = hiveBox.get(settingKey);
if (result is! T) {
debugPrint("Incorrect setting type");
throw TypeError();
}
hiveBox.put(settingKey, value);
} else {
hiveBox.put(settingKey, value);
}
}
_setDefaultSetting(AppSettingsEnum settingType) {
var settingKey = _settingHiveBoxKeyLookup(settingType);
// Default value of threeStageLoading is false
if (settingType == AppSettingsEnum.threeStageLoading) {
hiveBox.put(settingKey, false);
return false;
}
// Default value of themeMode is "light"
if (settingType == AppSettingsEnum.themeMode) {
hiveBox.put(settingKey, "system");
return "system";
}
}
String _settingHiveBoxKeyLookup(AppSettingsEnum settingType) {
switch (settingType) {
case AppSettingsEnum.threeStageLoading:
return 'threeStageLoading';
case AppSettingsEnum.themeMode:
return 'themeMode';
}
}
}

View File

@@ -0,0 +1,31 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart';
class ImageViewerQualitySetting extends StatelessWidget {
const ImageViewerQualitySetting({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ExpansionTile(
textColor: Theme.of(context).primaryColor,
title: const Text(
'theme_setting_image_viewer_quality_title',
style: TextStyle(
fontWeight: FontWeight.bold,
),
).tr(),
subtitle: const Text(
'theme_setting_image_viewer_quality_subtitle',
style: TextStyle(
fontSize: 13,
),
).tr(),
children: const [
ThreeStageLoading(),
],
);
}
}

View File

@@ -0,0 +1,56 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
class ThreeStageLoading extends HookConsumerWidget {
const ThreeStageLoading({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
final isEnable = useState(false);
useEffect(
() {
var isThreeStageLoadingEnable =
appSettingService.getSetting(AppSettingsEnum.threeStageLoading);
isEnable.value = isThreeStageLoadingEnable;
return null;
},
[],
);
void onSwitchChanged(bool switchValue) {
appSettingService.setSetting(
AppSettingsEnum.threeStageLoading,
switchValue,
);
isEnable.value = switchValue;
}
return SwitchListTile.adaptive(
title: const Text(
"theme_setting_three_stage_loading_title",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
).tr(),
subtitle: const Text(
"theme_setting_three_stage_loading_subtitle",
style: TextStyle(
fontSize: 12,
),
).tr(),
value: isEnable.value,
onChanged: onSwitchChanged,
);
}
}

View File

@@ -0,0 +1,107 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/three_stage_loading.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/utils/immich_app_theme.dart';
class ThemeSetting extends HookConsumerWidget {
const ThemeSetting({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentTheme = useState<ThemeMode>(ThemeMode.system);
useEffect(
() {
currentTheme.value = ref.read(immichThemeProvider);
return null;
},
[],
);
return ExpansionTile(
textColor: Theme.of(context).primaryColor,
title: const Text(
'theme_setting_theme_title',
style: TextStyle(
fontWeight: FontWeight.bold,
),
).tr(),
subtitle: const Text(
'theme_setting_theme_subtitle',
style: TextStyle(
fontSize: 13,
),
).tr(),
children: [
SwitchListTile.adaptive(
activeColor: Theme.of(context).primaryColor,
title: const Text(
'theme_setting_system_theme_switch',
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
),
).tr(),
value: currentTheme.value == ThemeMode.system,
onChanged: (bool isSystem) {
var currentSystemBrightness =
MediaQuery.of(context).platformBrightness;
if (isSystem) {
currentTheme.value = ThemeMode.system;
ref.watch(immichThemeProvider.notifier).state = ThemeMode.system;
ref
.watch(appSettingsServiceProvider)
.setSetting(AppSettingsEnum.themeMode, "system");
} else {
if (currentSystemBrightness == Brightness.light) {
currentTheme.value = ThemeMode.light;
ref.watch(immichThemeProvider.notifier).state = ThemeMode.light;
ref
.watch(appSettingsServiceProvider)
.setSetting(AppSettingsEnum.themeMode, "light");
} else if (currentSystemBrightness == Brightness.dark) {
currentTheme.value = ThemeMode.dark;
ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark;
ref
.watch(appSettingsServiceProvider)
.setSetting(AppSettingsEnum.themeMode, "dark");
}
}
},
),
if (currentTheme.value != ThemeMode.system)
SwitchListTile.adaptive(
activeColor: Theme.of(context).primaryColor,
title: const Text(
'theme_setting_dark_mode_switch',
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
),
).tr(),
value: ref.watch(immichThemeProvider) == ThemeMode.dark,
onChanged: (bool isDark) {
if (isDark) {
ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark;
ref
.watch(appSettingsServiceProvider)
.setSetting(AppSettingsEnum.themeMode, "dark");
} else {
ref.watch(immichThemeProvider.notifier).state = ThemeMode.light;
ref
.watch(appSettingsServiceProvider)
.setSetting(AppSettingsEnum.themeMode, "light");
}
},
),
],
);
}
}

View File

@@ -0,0 +1,45 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
class SettingsPage extends HookConsumerWidget {
const SettingsPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
iconSize: 20,
splashRadius: 24,
onPressed: () {
Navigator.pop(context);
},
icon: const Icon(Icons.arrow_back_ios_new_rounded),
),
automaticallyImplyLeading: false,
centerTitle: false,
title: const Text(
'setting_pages_app_bar_settings',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
).tr(),
),
body: ListView(
children: [
...ListTile.divideTiles(
context: context,
tiles: [
const ImageViewerQualitySetting(),
const ThemeSetting(),
],
).toList(),
],
),
);
}
}

View File

@@ -18,6 +18,7 @@ import 'package:immich_mobile/modules/album/views/create_album_page.dart';
import 'package:immich_mobile/modules/album/views/select_additional_user_for_sharing_page.dart'; import 'package:immich_mobile/modules/album/views/select_additional_user_for_sharing_page.dart';
import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart'; import 'package:immich_mobile/modules/album/views/select_user_for_sharing_page.dart';
import 'package:immich_mobile/modules/album/views/sharing_page.dart'; import 'package:immich_mobile/modules/album/views/sharing_page.dart';
import 'package:immich_mobile/modules/settings/views/settings_page.dart';
import 'package:immich_mobile/routing/auth_guard.dart'; import 'package:immich_mobile/routing/auth_guard.dart';
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart'; import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart'; import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
@@ -77,6 +78,7 @@ part 'router.gr.dart';
guards: [AuthGuard], guards: [AuthGuard],
transitionsBuilder: TransitionsBuilders.slideBottom, transitionsBuilder: TransitionsBuilders.slideBottom,
), ),
AutoRoute(page: SettingsPage, guards: [AuthGuard]),
], ],
) )
class AppRouter extends _$AppRouter { class AppRouter extends _$AppRouter {

View File

@@ -46,10 +46,7 @@ class _$AppRouter extends RootStackRouter {
return MaterialPageX<dynamic>( return MaterialPageX<dynamic>(
routeData: routeData, routeData: routeData,
child: GalleryViewerPage( child: GalleryViewerPage(
key: args.key, key: args.key, assetList: args.assetList, asset: args.asset));
assetList: args.assetList,
asset: args.asset,
thumbnailRequestUrl: args.thumbnailRequestUrl));
}, },
ImageViewerRoute.name: (routeData) { ImageViewerRoute.name: (routeData) {
final args = routeData.argsAs<ImageViewerRouteArgs>(); final args = routeData.argsAs<ImageViewerRouteArgs>();
@@ -57,13 +54,14 @@ class _$AppRouter extends RootStackRouter {
routeData: routeData, routeData: routeData,
child: ImageViewerPage( child: ImageViewerPage(
key: args.key, key: args.key,
imageUrl: args.imageUrl,
heroTag: args.heroTag, heroTag: args.heroTag,
thumbnailUrl: args.thumbnailUrl,
asset: args.asset, asset: args.asset,
authToken: args.authToken, authToken: args.authToken,
isZoomedFunction: args.isZoomedFunction, isZoomedFunction: args.isZoomedFunction,
isZoomedListener: args.isZoomedListener)); isZoomedListener: args.isZoomedListener,
onLoadingCompleted: args.onLoadingCompleted,
onLoadingStart: args.onLoadingStart,
threeStageLoading: args.threeStageLoading));
}, },
VideoViewerRoute.name: (routeData) { VideoViewerRoute.name: (routeData) {
final args = routeData.argsAs<VideoViewerRouteArgs>(); final args = routeData.argsAs<VideoViewerRouteArgs>();
@@ -139,6 +137,10 @@ class _$AppRouter extends RootStackRouter {
opaque: true, opaque: true,
barrierDismissible: false); barrierDismissible: false);
}, },
SettingsRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData, child: const SettingsPage());
},
HomeRoute.name: (routeData) { HomeRoute.name: (routeData) {
return MaterialPageX<dynamic>( return MaterialPageX<dynamic>(
routeData: routeData, child: const HomePage()); routeData: routeData, child: const HomePage());
@@ -213,7 +215,9 @@ class _$AppRouter extends RootStackRouter {
RouteConfig(AlbumPreviewRoute.name, RouteConfig(AlbumPreviewRoute.name,
path: '/album-preview-page', guards: [authGuard]), path: '/album-preview-page', guards: [authGuard]),
RouteConfig(FailedBackupStatusRoute.name, RouteConfig(FailedBackupStatusRoute.name,
path: '/failed-backup-status-page', guards: [authGuard]) path: '/failed-backup-status-page', guards: [authGuard]),
RouteConfig(SettingsRoute.name,
path: '/settings-page', guards: [authGuard])
]; ];
} }
@@ -258,25 +262,18 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
GalleryViewerRoute( GalleryViewerRoute(
{Key? key, {Key? key,
required List<AssetResponseDto> assetList, required List<AssetResponseDto> assetList,
required AssetResponseDto asset, required AssetResponseDto asset})
required String thumbnailRequestUrl})
: super(GalleryViewerRoute.name, : super(GalleryViewerRoute.name,
path: '/gallery-viewer-page', path: '/gallery-viewer-page',
args: GalleryViewerRouteArgs( args: GalleryViewerRouteArgs(
key: key, key: key, assetList: assetList, asset: asset));
assetList: assetList,
asset: asset,
thumbnailRequestUrl: thumbnailRequestUrl));
static const String name = 'GalleryViewerRoute'; static const String name = 'GalleryViewerRoute';
} }
class GalleryViewerRouteArgs { class GalleryViewerRouteArgs {
const GalleryViewerRouteArgs( const GalleryViewerRouteArgs(
{this.key, {this.key, required this.assetList, required this.asset});
required this.assetList,
required this.asset,
required this.thumbnailRequestUrl});
final Key? key; final Key? key;
@@ -284,11 +281,9 @@ class GalleryViewerRouteArgs {
final AssetResponseDto asset; final AssetResponseDto asset;
final String thumbnailRequestUrl;
@override @override
String toString() { String toString() {
return 'GalleryViewerRouteArgs{key: $key, assetList: $assetList, asset: $asset, thumbnailRequestUrl: $thumbnailRequestUrl}'; return 'GalleryViewerRouteArgs{key: $key, assetList: $assetList, asset: $asset}';
} }
} }
@@ -297,24 +292,26 @@ class GalleryViewerRouteArgs {
class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> { class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
ImageViewerRoute( ImageViewerRoute(
{Key? key, {Key? key,
required String imageUrl,
required String heroTag, required String heroTag,
required String thumbnailUrl,
required AssetResponseDto asset, required AssetResponseDto asset,
required String authToken, required String authToken,
required void Function() isZoomedFunction, required void Function() isZoomedFunction,
required ValueNotifier<bool> isZoomedListener}) required ValueNotifier<bool> isZoomedListener,
required void Function() onLoadingCompleted,
required void Function() onLoadingStart,
required bool threeStageLoading})
: super(ImageViewerRoute.name, : super(ImageViewerRoute.name,
path: '/image-viewer-page', path: '/image-viewer-page',
args: ImageViewerRouteArgs( args: ImageViewerRouteArgs(
key: key, key: key,
imageUrl: imageUrl,
heroTag: heroTag, heroTag: heroTag,
thumbnailUrl: thumbnailUrl,
asset: asset, asset: asset,
authToken: authToken, authToken: authToken,
isZoomedFunction: isZoomedFunction, isZoomedFunction: isZoomedFunction,
isZoomedListener: isZoomedListener)); isZoomedListener: isZoomedListener,
onLoadingCompleted: onLoadingCompleted,
onLoadingStart: onLoadingStart,
threeStageLoading: threeStageLoading));
static const String name = 'ImageViewerRoute'; static const String name = 'ImageViewerRoute';
} }
@@ -322,22 +319,19 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
class ImageViewerRouteArgs { class ImageViewerRouteArgs {
const ImageViewerRouteArgs( const ImageViewerRouteArgs(
{this.key, {this.key,
required this.imageUrl,
required this.heroTag, required this.heroTag,
required this.thumbnailUrl,
required this.asset, required this.asset,
required this.authToken, required this.authToken,
required this.isZoomedFunction, required this.isZoomedFunction,
required this.isZoomedListener}); required this.isZoomedListener,
required this.onLoadingCompleted,
required this.onLoadingStart,
required this.threeStageLoading});
final Key? key; final Key? key;
final String imageUrl;
final String heroTag; final String heroTag;
final String thumbnailUrl;
final AssetResponseDto asset; final AssetResponseDto asset;
final String authToken; final String authToken;
@@ -346,9 +340,15 @@ class ImageViewerRouteArgs {
final ValueNotifier<bool> isZoomedListener; final ValueNotifier<bool> isZoomedListener;
final void Function() onLoadingCompleted;
final void Function() onLoadingStart;
final bool threeStageLoading;
@override @override
String toString() { String toString() {
return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener}'; return 'ImageViewerRouteArgs{key: $key, heroTag: $heroTag, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener, onLoadingCompleted: $onLoadingCompleted, onLoadingStart: $onLoadingStart, threeStageLoading: $threeStageLoading}';
} }
} }
@@ -552,6 +552,14 @@ class FailedBackupStatusRoute extends PageRouteInfo<void> {
static const String name = 'FailedBackupStatusRoute'; static const String name = 'FailedBackupStatusRoute';
} }
/// generated route for
/// [SettingsPage]
class SettingsRoute extends PageRouteInfo<void> {
const SettingsRoute() : super(SettingsRoute.name, path: '/settings-page');
static const String name = 'SettingsRoute';
}
/// generated route for /// generated route for
/// [HomePage] /// [HomePage]
class HomeRoute extends PageRouteInfo<void> { class HomeRoute extends PageRouteInfo<void> {

View File

@@ -0,0 +1,47 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:openapi/api.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:path/path.dart' as p;
import 'api.service.dart';
final shareServiceProvider =
Provider((ref) => ShareService(ref.watch(apiServiceProvider)));
class ShareService {
final ApiService _apiService;
ShareService(this._apiService);
Future<void> shareAsset(AssetResponseDto asset) async {
await shareAssets([asset]);
}
Future<void> shareAssets(List<AssetResponseDto> assets) async {
final downloadedFilePaths = assets.map((asset) async {
final res = await _apiService.assetApi.downloadFileWithHttpInfo(
asset.deviceAssetId,
asset.deviceId,
isThumb: false,
isWeb: false,
);
final fileName = p.basename(asset.originalPath);
final tempDir = await getTemporaryDirectory();
final tempFile = await File('${tempDir.path}/$fileName').create();
tempFile.writeAsBytesSync(res.bodyBytes);
return tempFile.path;
});
Share.shareFiles(
await Future.wait(downloadedFilePaths),
sharePositionOrigin: Rect.zero,
);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class ShareDialog extends StatelessWidget {
const ShareDialog({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
Container(
margin: const EdgeInsets.only(top: 12),
child: const Text('share_dialog_preparing')
.tr(),
)
],
),
);
}
}

View File

@@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.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/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@@ -49,7 +48,6 @@ class SplashScreenPage extends HookConsumerWidget {
); );
return Scaffold( return Scaffold(
backgroundColor: immichBackgroundColor,
body: Center( body: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@@ -2,7 +2,6 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@@ -36,8 +35,6 @@ class TabControllerPage extends ConsumerWidget {
bottomNavigationBar: isMultiSelectEnable bottomNavigationBar: isMultiSelectEnable
? null ? null
: BottomNavigationBar( : BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: immichBackgroundColor,
selectedLabelStyle: const TextStyle( selectedLabelStyle: const TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -53,21 +50,23 @@ class TabControllerPage extends ConsumerWidget {
items: [ items: [
BottomNavigationBarItem( BottomNavigationBarItem(
label: 'tab_controller_nav_photos'.tr(), label: 'tab_controller_nav_photos'.tr(),
icon: const Icon(Icons.photo), icon: const Icon(Icons.photo_outlined),
activeIcon: const Icon(Icons.photo),
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
label: 'tab_controller_nav_search'.tr(), label: 'tab_controller_nav_search'.tr(),
icon: const Icon(Icons.search), icon: const Icon(Icons.search_rounded),
activeIcon: const Icon(Icons.search),
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
label: 'tab_controller_nav_sharing'.tr(), label: 'tab_controller_nav_sharing'.tr(),
icon: const Icon(Icons.group_outlined), icon: const Icon(Icons.group_outlined),
activeIcon: const Icon(Icons.group),
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
label: 'tab_controller_nav_library'.tr(), label: 'tab_controller_nav_library'.tr(),
icon: const Icon( icon: const Icon(Icons.photo_album_outlined),
Icons.photo_album_outlined, activeIcon: const Icon(Icons.photo_album_rounded),
),
) )
], ],
), ),

View File

@@ -0,0 +1,16 @@
import 'package:hive/hive.dart';
import 'package:openapi/api.dart';
import '../constants/hive_box.dart';
String getThumbnailUrl(final AssetResponseDto asset,
{ThumbnailFormat type = ThumbnailFormat.WEBP}) {
final box = Hive.box(userInfoBox);
return '${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}?format=${type.value}';
}
String getImageUrl(final AssetResponseDto asset) {
final box = Hive.box(userInfoBox);
return '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false';
}

View File

@@ -0,0 +1,133 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
final immichThemeProvider = StateProvider<ThemeMode>((ref) {
var themeMode = ref
.watch(appSettingsServiceProvider)
.getSetting(AppSettingsEnum.themeMode);
debugPrint("Current themeMode $themeMode");
if (themeMode == "light") {
return ThemeMode.light;
} else if (themeMode == "dark") {
return ThemeMode.dark;
} else {
return ThemeMode.system;
}
});
ThemeData immichDarkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
primarySwatch: Colors.indigo,
primaryColor: immichDarkThemePrimaryColor,
scaffoldBackgroundColor: immichDarkBackgroundColor,
hintColor: Colors.grey[600],
fontFamily: 'WorkSans',
snackBarTheme: const SnackBarThemeData(
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
),
appBarTheme: AppBarTheme(
titleTextStyle: TextStyle(
fontFamily: 'WorkSans',
color: immichDarkThemePrimaryColor,
),
backgroundColor: const Color.fromARGB(255, 32, 33, 35),
foregroundColor: immichDarkThemePrimaryColor,
elevation: 1,
centerTitle: true,
systemOverlayStyle: SystemUiOverlayStyle.light,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: const Color.fromARGB(255, 35, 36, 37),
selectedItemColor: immichDarkThemePrimaryColor,
),
drawerTheme: DrawerThemeData(
backgroundColor: immichDarkBackgroundColor,
scrimColor: Colors.white.withOpacity(0.1),
),
textTheme: TextTheme(
headline1: const TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 255, 255, 255),
),
headline2: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 148, 151, 155),
),
headline3: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: immichDarkThemePrimaryColor,
),
),
cardColor: Colors.grey[900],
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
onPrimary: Colors.black87,
primary: immichDarkThemePrimaryColor,
),
),
);
ThemeData immichLightTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.light,
primarySwatch: Colors.indigo,
hintColor: Colors.indigo,
fontFamily: 'WorkSans',
scaffoldBackgroundColor: immichBackgroundColor,
snackBarTheme: const SnackBarThemeData(
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
),
appBarTheme: AppBarTheme(
titleTextStyle: const TextStyle(
fontFamily: 'WorkSans',
color: Colors.indigo,
),
backgroundColor: immichBackgroundColor,
foregroundColor: Colors.indigo,
elevation: 1,
centerTitle: true,
systemOverlayStyle: SystemUiOverlayStyle.dark,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: immichBackgroundColor,
selectedItemColor: Colors.indigo,
),
drawerTheme: DrawerThemeData(
backgroundColor: immichBackgroundColor,
),
textTheme: const TextTheme(
headline1: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
headline2: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
headline3: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
primary: Colors.indigo,
onPrimary: Colors.white,
),
),
);

View File

@@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
## Properties ## Properties
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**assetCount** | **int** | |
**id** | **String** | | **id** | **String** | |
**ownerId** | **String** | | **ownerId** | **String** | |
**albumName** | **String** | | **albumName** | **String** | |

View File

@@ -13,6 +13,7 @@ part of openapi.api;
class AlbumResponseDto { class AlbumResponseDto {
/// Returns a new [AlbumResponseDto] instance. /// Returns a new [AlbumResponseDto] instance.
AlbumResponseDto({ AlbumResponseDto({
required this.assetCount,
required this.id, required this.id,
required this.ownerId, required this.ownerId,
required this.albumName, required this.albumName,
@@ -23,6 +24,8 @@ class AlbumResponseDto {
this.assets = const [], this.assets = const [],
}); });
int assetCount;
String id; String id;
String ownerId; String ownerId;
@@ -41,6 +44,7 @@ class AlbumResponseDto {
@override @override
bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto && bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto &&
other.assetCount == assetCount &&
other.id == id && other.id == id &&
other.ownerId == ownerId && other.ownerId == ownerId &&
other.albumName == albumName && other.albumName == albumName &&
@@ -53,6 +57,7 @@ class AlbumResponseDto {
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(assetCount.hashCode) +
(id.hashCode) + (id.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
(albumName.hashCode) + (albumName.hashCode) +
@@ -63,10 +68,11 @@ class AlbumResponseDto {
(assets.hashCode); (assets.hashCode);
@override @override
String toString() => 'AlbumResponseDto[id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]'; String toString() => 'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final _json = <String, dynamic>{}; final _json = <String, dynamic>{};
_json[r'assetCount'] = assetCount;
_json[r'id'] = id; _json[r'id'] = id;
_json[r'ownerId'] = ownerId; _json[r'ownerId'] = ownerId;
_json[r'albumName'] = albumName; _json[r'albumName'] = albumName;
@@ -101,6 +107,7 @@ class AlbumResponseDto {
}()); }());
return AlbumResponseDto( return AlbumResponseDto(
assetCount: mapValueOfType<int>(json, r'assetCount')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
ownerId: mapValueOfType<String>(json, r'ownerId')!, ownerId: mapValueOfType<String>(json, r'ownerId')!,
albumName: mapValueOfType<String>(json, r'albumName')!, albumName: mapValueOfType<String>(json, r'albumName')!,
@@ -158,6 +165,7 @@ class AlbumResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'assetCount',
'id', 'id',
'ownerId', 'ownerId',
'albumName', 'albumName',

View File

@@ -76,72 +76,69 @@ class AssetResponseDto {
SmartInfoResponseDto? smartInfo; SmartInfoResponseDto? smartInfo;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
identical(this, other) || other.type == type &&
other is AssetResponseDto && other.id == id &&
other.type == type && other.deviceAssetId == deviceAssetId &&
other.id == id && other.ownerId == ownerId &&
other.deviceAssetId == deviceAssetId && other.deviceId == deviceId &&
other.ownerId == ownerId && other.originalPath == originalPath &&
other.deviceId == deviceId && other.resizePath == resizePath &&
other.originalPath == originalPath && other.createdAt == createdAt &&
other.resizePath == resizePath && other.modifiedAt == modifiedAt &&
other.createdAt == createdAt && other.isFavorite == isFavorite &&
other.modifiedAt == modifiedAt && other.mimeType == mimeType &&
other.isFavorite == isFavorite && other.duration == duration &&
other.mimeType == mimeType && other.webpPath == webpPath &&
other.duration == duration && other.encodedVideoPath == encodedVideoPath &&
other.webpPath == webpPath && other.exifInfo == exifInfo &&
other.encodedVideoPath == encodedVideoPath && other.smartInfo == smartInfo;
other.exifInfo == exifInfo &&
other.smartInfo == smartInfo;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(type.hashCode) + (type.hashCode) +
(id.hashCode) + (id.hashCode) +
(deviceAssetId.hashCode) + (deviceAssetId.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
(deviceId.hashCode) + (deviceId.hashCode) +
(originalPath.hashCode) + (originalPath.hashCode) +
(resizePath == null ? 0 : resizePath!.hashCode) + (resizePath == null ? 0 : resizePath!.hashCode) +
(createdAt.hashCode) + (createdAt.hashCode) +
(modifiedAt.hashCode) + (modifiedAt.hashCode) +
(isFavorite.hashCode) + (isFavorite.hashCode) +
(mimeType == null ? 0 : mimeType!.hashCode) + (mimeType == null ? 0 : mimeType!.hashCode) +
(duration.hashCode) + (duration.hashCode) +
(webpPath == null ? 0 : webpPath!.hashCode) + (webpPath == null ? 0 : webpPath!.hashCode) +
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) + (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
(exifInfo == null ? 0 : exifInfo!.hashCode) + (exifInfo == null ? 0 : exifInfo!.hashCode) +
(smartInfo == null ? 0 : smartInfo!.hashCode); (smartInfo == null ? 0 : smartInfo!.hashCode);
@override @override
String toString() => String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final _json = <String, dynamic>{}; final _json = <String, dynamic>{};
_json[r'type'] = type; _json[r'type'] = type;
_json[r'id'] = id; _json[r'id'] = id;
_json[r'deviceAssetId'] = deviceAssetId; _json[r'deviceAssetId'] = deviceAssetId;
_json[r'ownerId'] = ownerId; _json[r'ownerId'] = ownerId;
_json[r'deviceId'] = deviceId; _json[r'deviceId'] = deviceId;
_json[r'originalPath'] = originalPath; _json[r'originalPath'] = originalPath;
if (resizePath != null) { if (resizePath != null) {
_json[r'resizePath'] = resizePath; _json[r'resizePath'] = resizePath;
} else { } else {
_json[r'resizePath'] = null; _json[r'resizePath'] = null;
} }
_json[r'createdAt'] = createdAt; _json[r'createdAt'] = createdAt;
_json[r'modifiedAt'] = modifiedAt; _json[r'modifiedAt'] = modifiedAt;
_json[r'isFavorite'] = isFavorite; _json[r'isFavorite'] = isFavorite;
if (mimeType != null) { if (mimeType != null) {
_json[r'mimeType'] = mimeType; _json[r'mimeType'] = mimeType;
} else { } else {
_json[r'mimeType'] = null; _json[r'mimeType'] = null;
} }
_json[r'duration'] = duration; _json[r'duration'] = duration;
if (webpPath != null) { if (webpPath != null) {
_json[r'webpPath'] = webpPath; _json[r'webpPath'] = webpPath;
} else { } else {
@@ -177,10 +174,8 @@ class AssetResponseDto {
// Note 2: this code is stripped in release mode! // Note 2: this code is stripped in release mode!
assert(() { assert(() {
requiredKeys.forEach((key) { requiredKeys.forEach((key) {
assert(json.containsKey(key), assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
'Required key "AssetResponseDto[$key]" is missing from JSON.'); assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
assert(json[key] != null,
'Required key "AssetResponseDto[$key]" has a null value in JSON.');
}); });
return true; return true;
}()); }());
@@ -207,10 +202,7 @@ class AssetResponseDto {
return null; return null;
} }
static List<AssetResponseDto>? listFromJson( static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
dynamic json, {
bool growable = false,
}) {
final result = <AssetResponseDto>[]; final result = <AssetResponseDto>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
@@ -238,18 +230,12 @@ class AssetResponseDto {
} }
// maps a json object with a list of AssetResponseDto-objects as value to a dart map // maps a json object with a list of AssetResponseDto-objects as value to a dart map
static Map<String, List<AssetResponseDto>> mapListFromJson( static Map<String, List<AssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
dynamic json, {
bool growable = false,
}) {
final map = <String, List<AssetResponseDto>>{}; final map = <String, List<AssetResponseDto>>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) { for (final entry in json.entries) {
final value = AssetResponseDto.listFromJson( final value = AssetResponseDto.listFromJson(entry.value, growable: growable,);
entry.value,
growable: growable,
);
if (value != null) { if (value != null) {
map[entry.key] = value; map[entry.key] = value;
} }
@@ -276,3 +262,4 @@ class AssetResponseDto {
'encodedVideoPath', 'encodedVideoPath',
}; };
} }

View File

@@ -328,6 +328,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.3.0" version: "3.3.0"
flutter_displaymode:
dependency: "direct main"
description:
name: flutter_displaymode
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.0"
flutter_hooks: flutter_hooks:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -868,6 +875,48 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.27.3" version: "0.27.3"
share_plus:
dependency: "direct main"
description:
name: share_plus
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.10"
share_plus_linux:
dependency: transitive
description:
name: share_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
share_plus_macos:
dependency: transitive
description:
name: share_plus_macos
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.3"
share_plus_web:
dependency: transitive
description:
name: share_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
share_plus_windows:
dependency: transitive
description:
name: share_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
shared_preferences: shared_preferences:
dependency: transitive dependency: transitive
description: description:

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: "none" publish_to: "none"
version: 1.20.0+30 version: 1.23.0+33
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"
@@ -41,6 +41,8 @@ dependencies:
http: 0.13.4 http: 0.13.4
cancellation_token_http: ^1.1.0 cancellation_token_http: ^1.1.0
easy_localization: ^3.0.1 easy_localization: ^3.0.1
share_plus: ^4.0.10
flutter_displaymode: ^0.4.0
path: ^1.8.1 path: ^1.8.1
path_provider: ^2.0.11 path_provider: ^2.0.11

View File

@@ -134,21 +134,14 @@ export class AlbumRepository implements IAlbumRepository {
.leftJoinAndSelect('album.sharedUsers', 'sharedUser') .leftJoinAndSelect('album.sharedUsers', 'sharedUser')
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo') .leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
.where('album.ownerId = :ownerId', { ownerId: userId }); .where('album.ownerId = :ownerId', { ownerId: userId });
// .orWhere((qb) => {
// const subQuery = qb
// .subQuery()
// .select('userAlbum.albumId')
// .from(UserAlbumEntity, 'userAlbum')
// .where('userAlbum.sharedUserId = :sharedUserId', { sharedUserId: userId })
// .getQuery();
// return `album.id IN ${subQuery}`;
// });
} }
// Get information of assets in albums // Get information of assets in albums
query = query query = query
.leftJoinAndSelect('album.assets', 'assets') .leftJoinAndSelect('album.assets', 'assets')
.leftJoinAndSelect('assets.assetInfo', 'assetInfo') .leftJoinAndSelect('assets.assetInfo', 'assetInfo')
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC'); .orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC');
const albums = await query.getMany(); const albums = await query.getMany();
albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf()); albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf());

View File

@@ -4,6 +4,7 @@ import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common'; import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
import { AlbumEntity } from '@app/database/entities/album.entity'; import { AlbumEntity } from '@app/database/entities/album.entity';
import { AlbumResponseDto } from './response-dto/album-response.dto'; import { AlbumResponseDto } from './response-dto/album-response.dto';
import { AssetAlbumEntity } from '@app/database/entities/asset-album.entity';
describe('Album service', () => { describe('Album service', () => {
let sut: AlbumService; let sut: AlbumService;
@@ -12,7 +13,7 @@ describe('Album service', () => {
id: '1111', id: '1111',
email: 'auth@test.com', email: 'auth@test.com',
}); });
const albumId = '0001'; const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
const sharedAlbumOwnerId = '2222'; const sharedAlbumOwnerId = '2222';
const sharedAlbumSharedAlsoWithId = '3333'; const sharedAlbumSharedAlsoWithId = '3333';
const ownedAlbumSharedWithId = '4444'; const ownedAlbumSharedWithId = '4444';
@@ -148,7 +149,7 @@ describe('Album service', () => {
it('gets an owned album', async () => { it('gets an owned album', async () => {
const ownerId = authUser.id; const ownerId = authUser.id;
const albumId = '0001'; const albumId = 'f19ab956-4761-41ea-a5d6-bae948308d58';
const albumEntity = _getOwnedAlbum(); const albumEntity = _getOwnedAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
@@ -157,11 +158,12 @@ describe('Album service', () => {
albumName: 'name', albumName: 'name',
albumThumbnailAssetId: null, albumThumbnailAssetId: null,
createdAt: 'date', createdAt: 'date',
id: '0001', id: 'f19ab956-4761-41ea-a5d6-bae948308d58',
ownerId, ownerId,
shared: false, shared: false,
assets: [], assets: [],
sharedUsers: [], sharedUsers: [],
assetCount: 0,
}; };
await expect(sut.getAlbumInfo(authUser, albumId)).resolves.toEqual(expectedResult); await expect(sut.getAlbumInfo(authUser, albumId)).resolves.toEqual(expectedResult);
}); });
@@ -270,6 +272,7 @@ describe('Album service', () => {
authUser, authUser,
{ {
albumName: updatedAlbumName, albumName: updatedAlbumName,
albumThumbnailAssetId: updatedAlbumThumbnailAssetId,
}, },
albumId, albumId,
); );
@@ -279,7 +282,7 @@ describe('Album service', () => {
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1); expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, { expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, {
albumName: updatedAlbumName, albumName: updatedAlbumName,
thumbnailAssetId: updatedAlbumThumbnailAssetId, albumThumbnailAssetId: updatedAlbumThumbnailAssetId,
}); });
}); });
@@ -357,45 +360,45 @@ describe('Album service', () => {
).rejects.toBeInstanceOf(ForbiddenException); ).rejects.toBeInstanceOf(ForbiddenException);
}); });
it('removes assets from owned album', async () => { // it('removes assets from owned album', async () => {
const albumEntity = _getOwnedAlbum(); // const albumEntity = _getOwnedAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); // albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); // albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
await expect( // await expect(
sut.removeAssetsFromAlbum( // sut.removeAssetsFromAlbum(
authUser, // authUser,
{ // {
assetIds: ['1'], // assetIds: ['f19ab956-4761-41ea-a5d6-bae948308d60'],
}, // },
albumEntity.id, // albumEntity.id,
), // ),
).resolves.toBeUndefined(); // ).resolves.toBeUndefined();
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1); // expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, { // expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, {
assetIds: ['1'], // assetIds: ['f19ab956-4761-41ea-a5d6-bae948308d60'],
}); // });
}); // });
it('removes assets from shared album (shared with auth user)', async () => { // it('removes assets from shared album (shared with auth user)', async () => {
const albumEntity = _getOwnedSharedAlbum(); // const albumEntity = _getOwnedSharedAlbum();
albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); // albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity)); // albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
await expect( // await expect(
sut.removeAssetsFromAlbum( // sut.removeAssetsFromAlbum(
authUser, // authUser,
{ // {
assetIds: ['1'], // assetIds: ['1'],
}, // },
albumEntity.id, // albumEntity.id,
), // ),
).resolves.toBeUndefined(); // ).resolves.toBeUndefined();
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1); // expect(albumRepositoryMock.removeAssets).toHaveBeenCalledTimes(1);
expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, { // expect(albumRepositoryMock.removeAssets).toHaveBeenCalledWith(albumEntity, {
assetIds: ['1'], // assetIds: ['1'],
}); // });
}); // });
it('prevents removing assets from a not owned / shared album', async () => { it('prevents removing assets from a not owned / shared album', async () => {
const albumEntity = _getNotOwnedNotSharedAlbum(); const albumEntity = _getNotOwnedNotSharedAlbum();
@@ -414,4 +417,33 @@ describe('Album service', () => {
), ),
).rejects.toBeInstanceOf(ForbiddenException); ).rejects.toBeInstanceOf(ForbiddenException);
}); });
it('counts assets correctly', async () => {
const albumEntity = new AlbumEntity();
albumEntity.ownerId = authUser.id;
albumEntity.id = albumId;
albumEntity.albumName = 'name';
albumEntity.createdAt = 'date';
albumEntity.sharedUsers = [];
albumEntity.assets = [
{
id: '1',
albumId: '2',
assetId: '3',
//@ts-expect-error Partial stub
albumInfo: {},
//@ts-expect-error Partial stub
assetInfo: {},
},
];
albumEntity.albumThumbnailAssetId = null;
albumRepositoryMock.getList.mockImplementation(() => Promise.resolve([albumEntity]));
const result = await sut.getAllAlbums(authUser, {});
expect(result).toHaveLength(1);
expect(result[0].assetCount).toEqual(1);
});
}); });

View File

@@ -7,7 +7,7 @@ import { AddUsersDto } from './dto/add-users.dto';
import { RemoveAssetsDto } from './dto/remove-assets.dto'; import { RemoveAssetsDto } from './dto/remove-assets.dto';
import { UpdateAlbumDto } from './dto/update-album.dto'; import { UpdateAlbumDto } from './dto/update-album.dto';
import { GetAlbumsDto } from './dto/get-albums.dto'; import { GetAlbumsDto } from './dto/get-albums.dto';
import { AlbumResponseDto, mapAlbum } from './response-dto/album-response.dto'; import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto';
import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository'; import { ALBUM_REPOSITORY, IAlbumRepository } from './album-repository';
@Injectable() @Injectable()
@@ -49,7 +49,8 @@ export class AlbumService {
*/ */
async getAllAlbums(authUser: AuthUserDto, getAlbumsDto: GetAlbumsDto): Promise<AlbumResponseDto[]> { async getAllAlbums(authUser: AuthUserDto, getAlbumsDto: GetAlbumsDto): Promise<AlbumResponseDto[]> {
const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto); const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
return albums.map((album) => mapAlbum(album));
return albums.map((album) => mapAlbumExcludeAssetInfo(album));
} }
async getAlbumInfo(authUser: AuthUserDto, albumId: string): Promise<AlbumResponseDto> { async getAlbumInfo(authUser: AuthUserDto, albumId: string): Promise<AlbumResponseDto> {
@@ -80,8 +81,6 @@ export class AlbumService {
await this._albumRepository.removeUser(album, sharedUserId); await this._albumRepository.removeUser(album, sharedUserId);
} }
// async removeUsersFromAlbum() {}
async removeAssetsFromAlbum( async removeAssetsFromAlbum(
authUser: AuthUserDto, authUser: AuthUserDto,
removeAssetsDto: RemoveAssetsDto, removeAssetsDto: RemoveAssetsDto,
@@ -89,7 +88,6 @@ export class AlbumService {
): Promise<AlbumResponseDto> { ): Promise<AlbumResponseDto> {
const album = await this._getAlbum({ authUser, albumId }); const album = await this._getAlbum({ authUser, albumId });
const updateAlbum = await this._albumRepository.removeAssets(album, removeAssetsDto); const updateAlbum = await this._albumRepository.removeAssets(album, removeAssetsDto);
return mapAlbum(updateAlbum); return mapAlbum(updateAlbum);
} }

View File

@@ -1,6 +1,7 @@
import { AlbumEntity } from '../../../../../../libs/database/src/entities/album.entity'; import { AlbumEntity } from '../../../../../../libs/database/src/entities/album.entity';
import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto'; import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto';
import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto'; import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto';
import { ApiProperty } from '@nestjs/swagger';
export class AlbumResponseDto { export class AlbumResponseDto {
id!: string; id!: string;
@@ -11,6 +12,9 @@ export class AlbumResponseDto {
shared!: boolean; shared!: boolean;
sharedUsers!: UserResponseDto[]; sharedUsers!: UserResponseDto[];
assets!: AssetResponseDto[]; assets!: AssetResponseDto[];
@ApiProperty({ type: 'integer' })
assetCount!: number;
} }
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto { export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
@@ -24,5 +28,21 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
sharedUsers, sharedUsers,
shared: sharedUsers.length > 0, shared: sharedUsers.length > 0,
assets: entity.assets?.map((assetAlbum) => mapAsset(assetAlbum.assetInfo)) || [], assets: entity.assets?.map((assetAlbum) => mapAsset(assetAlbum.assetInfo)) || [],
assetCount: entity.assets?.length || 0,
};
}
export function mapAlbumExcludeAssetInfo(entity: AlbumEntity): AlbumResponseDto {
const sharedUsers = entity.sharedUsers?.map((userAlbum) => mapUser(userAlbum.userInfo)) || [];
return {
albumName: entity.albumName,
albumThumbnailAssetId: entity.albumThumbnailAssetId,
createdAt: entity.createdAt,
id: entity.id,
ownerId: entity.ownerId,
sharedUsers,
shared: sharedUsers.length > 0,
assets: [],
assetCount: entity.assets?.length || 0,
}; };
} }

View File

@@ -25,7 +25,7 @@ export class AuthController {
@Post('/login') @Post('/login')
async login( async login(
@Body(ValidationPipe) loginCredential: LoginCredentialDto, @Body(new ValidationPipe({ transform: true })) loginCredential: LoginCredentialDto,
@Res() response: Response, @Res() response: Response,
): Promise<LoginResponseDto> { ): Promise<LoginResponseDto> {
const loginResponse = await this.authService.login(loginCredential); const loginResponse = await this.authService.login(loginCredential);
@@ -42,7 +42,9 @@ export class AuthController {
@Post('/admin-sign-up') @Post('/admin-sign-up')
@ApiBadRequestResponse({ description: 'The server already has an admin' }) @ApiBadRequestResponse({ description: 'The server already has an admin' })
async adminSignUp(@Body(ValidationPipe) signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> { async adminSignUp(
@Body(new ValidationPipe({ transform: true })) signUpCredential: SignUpDto,
): Promise<AdminSignupResponseDto> {
return await this.authService.adminSignUp(signUpCredential); return await this.authService.adminSignUp(signUpCredential);
} }
@@ -61,8 +63,7 @@ export class AuthController {
const status = new LogoutResponseDto(true); const status = new LogoutResponseDto(true);
response.send(status) response.send(status);
return status; return status;
} }
} }

View File

@@ -1,9 +1,11 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsNotEmpty } from 'class-validator'; import { IsNotEmpty } from 'class-validator';
export class LoginCredentialDto { export class LoginCredentialDto {
@IsNotEmpty() @IsNotEmpty()
@ApiProperty({ example: 'testuser@email.com' }) @ApiProperty({ example: 'testuser@email.com' })
@Transform(({ value }) => value?.toLowerCase())
email!: string; email!: string;
@IsNotEmpty() @IsNotEmpty()

View File

@@ -1,9 +1,11 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsNotEmpty, IsEmail } from 'class-validator'; import { IsNotEmpty, IsEmail } from 'class-validator';
export class SignUpDto { export class SignUpDto {
@IsEmail() @IsEmail()
@ApiProperty({ example: 'testuser@email.com' }) @ApiProperty({ example: 'testuser@email.com' })
@Transform(({ value }) => value?.toLowerCase())
email!: string; email!: string;
@IsNotEmpty() @IsNotEmpty()

View File

@@ -1,8 +1,10 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsNotEmpty, IsEmail } from 'class-validator'; import { IsNotEmpty, IsEmail } from 'class-validator';
export class CreateUserDto { export class CreateUserDto {
@IsEmail() @IsEmail()
@Transform(({ value }) => value?.toLowerCase())
@ApiProperty({ example: 'testuser@email.com' }) @ApiProperty({ example: 'testuser@email.com' })
email!: string; email!: string;

View File

@@ -61,7 +61,9 @@ export class UserController {
@ApiBearerAuth() @ApiBearerAuth()
@UseGuards(AdminRolesGuard) @UseGuards(AdminRolesGuard)
@Post() @Post()
async createUser(@Body(ValidationPipe) createUserDto: CreateUserDto): Promise<UserResponseDto> { async createUser(
@Body(new ValidationPipe({ transform: true })) createUserDto: CreateUserDto,
): Promise<UserResponseDto> {
return await this.userService.createUser(createUserDto); return await this.userService.createUser(createUserDto);
} }

View File

@@ -10,7 +10,7 @@ export interface IServerVersion {
export const serverVersion: IServerVersion = { export const serverVersion: IServerVersion = {
major: 1, major: 1,
minor: 20, minor: 23,
patch: 0, patch: 0,
build: 0, build: 0,
}; };

File diff suppressed because one or more lines are too long

View File

@@ -90,6 +90,12 @@ export interface AdminSignupResponseDto {
* @interface AlbumResponseDto * @interface AlbumResponseDto
*/ */
export interface AlbumResponseDto { export interface AlbumResponseDto {
/**
*
* @type {number}
* @memberof AlbumResponseDto
*/
'assetCount': number;
/** /**
* *
* @type {string} * @type {string}

View File

@@ -6,85 +6,86 @@
@tailwind utilities; @tailwind utilities;
:root { :root {
font-family: 'Work Sans', sans-serif; font-family: 'Work Sans', sans-serif;
/* --immich-icon-button-hover-color: #d3d3d3; */ /* --immich-icon-button-hover-color: #d3d3d3; */
} }
html { html {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
html::-webkit-scrollbar { html::-webkit-scrollbar {
width: 8px; width: 8px;
} }
/* Track */ /* Track */
html::-webkit-scrollbar-track { html::-webkit-scrollbar-track {
background: #f1f1f1; background: #f1f1f1;
border-radius: 16px; border-radius: 16px;
} }
/* Handle */ /* Handle */
html::-webkit-scrollbar-thumb { html::-webkit-scrollbar-thumb {
background: rgba(85, 86, 87, 0.408); background: rgba(85, 86, 87, 0.408);
border-radius: 16px; border-radius: 16px;
} }
/* Handle on hover */ /* Handle on hover */
html::-webkit-scrollbar-thumb:hover { html::-webkit-scrollbar-thumb:hover {
background: #4250afad; background: #4250afad;
border-radius: 16px; border-radius: 16px;
} }
body { body {
/* min-height: 100vh; */ /* min-height: 100vh; */
margin: 0; margin: 0;
background-color: #f6f8fe; background-color: #f6f8fe;
color: #5f6368; color: #5f6368;
} }
input:focus-visible { input:focus-visible {
outline-offset: 0px !important; outline-offset: 0px !important;
outline: none !important; outline: none !important;
} }
@layer utilities { @layer utilities {
.immich-form-input { .immich-form-input {
@apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm; @apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm ;
} }
.immich-form-label { .immich-form-label {
@apply font-medium text-sm text-gray-500; @apply font-medium text-sm text-gray-500;
} }
.immich-btn-primary { .immich-btn-primary {
@apply bg-immich-primary text-gray-100 border rounded-xl py-2 px-4 transition-all duration-150 hover:bg-immich-primary hover:shadow-lg text-sm font-medium; @apply bg-immich-primary text-gray-100 border rounded-xl py-2 px-4 transition-all duration-150 hover:bg-immich-primary hover:shadow-lg text-sm font-medium;
} }
.immich-text-button { .immich-text-button {
@apply flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium; @apply flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium;
} }
/* width */ /* width */
.immich-scrollbar::-webkit-scrollbar { .immich-scrollbar::-webkit-scrollbar {
width: 8px; width: 8px;
} }
/* Track */ /* Track */
.immich-scrollbar::-webkit-scrollbar-track { .immich-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1; background: #f1f1f1;
border-radius: 16px; border-radius: 16px;
} }
/* Handle */ /* Handle */
.immich-scrollbar::-webkit-scrollbar-thumb { .immich-scrollbar::-webkit-scrollbar-thumb {
background: rgba(85, 86, 87, 0.408); background: rgba(85, 86, 87, 0.408);
border-radius: 16px; border-radius: 16px;
} }
/* Handle on hover */ /* Handle on hover */
.immich-scrollbar::-webkit-scrollbar-thumb:hover { .immich-scrollbar::-webkit-scrollbar-thumb:hover {
background: #4250afad; background: #4250afad;
border-radius: 16px; border-radius: 16px;
} }
} }

View File

@@ -8,8 +8,6 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
</script> </script>
<p class="text-sm">USER LIST</p>
<table class="text-left w-full my-4"> <table class="text-left w-full my-4">
<thead class="border rounded-md mb-2 bg-gray-50 flex text-immich-primary w-full h-12 "> <thead class="border rounded-md mb-2 bg-gray-50 flex text-immich-primary w-full h-12 ">
<tr class="flex w-full place-items-center"> <tr class="flex w-full place-items-center">
@@ -31,6 +29,9 @@
<td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td> <td class="text-sm px-4 w-1/4 text-ellipsis">{user.lastName}</td>
<td class="text-sm px-4 w-1/4 text-ellipsis" <td class="text-sm px-4 w-1/4 text-ellipsis"
><button ><button
on:click={() => {
dispatch('edit-user', { user });
}}
class="bg-immich-primary text-gray-100 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75" class="bg-immich-primary text-gray-100 rounded-full p-3 transition-all duration-150 hover:bg-immich-primary/75"
><PencilOutline size="20" /></button ><PencilOutline size="20" /></button
></td ></td
@@ -40,4 +41,4 @@
</tbody> </tbody>
</table> </table>
<button on:click={() => dispatch('createUser')} class="immich-btn-primary">Create user</button> <button on:click={() => dispatch('create-user')} class="immich-btn-primary">Create user</button>

View File

@@ -32,7 +32,7 @@
}; };
onMount(async () => { onMount(async () => {
imageData = await loadHighQualityThumbnail(album.albumThumbnailAssetId) || 'no-thumbnail.png'; imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || 'no-thumbnail.png';
}); });
</script> </script>
@@ -67,7 +67,7 @@
</p> </p>
<span class="text-xs flex gap-2"> <span class="text-xs flex gap-2">
<p>{album.assets.length} items</p> <p>{album.assetCount} items</p>
{#if album.shared} {#if album.shared}
<p>·</p> <p>·</p>

View File

@@ -12,7 +12,6 @@
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte'; import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
import AssetSelection from './asset-selection.svelte'; import AssetSelection from './asset-selection.svelte';
import _ from 'lodash-es'; import _ from 'lodash-es';
import AlbumAppBar from './album-app-bar.svelte';
import UserSelectionModal from './user-selection-modal.svelte'; import UserSelectionModal from './user-selection-modal.svelte';
import ShareInfoModal from './share-info-modal.svelte'; import ShareInfoModal from './share-info-modal.svelte';
import CircleIconButton from '../shared-components/circle-icon-button.svelte'; import CircleIconButton from '../shared-components/circle-icon-button.svelte';
@@ -22,6 +21,7 @@
import ContextMenu from '../shared-components/context-menu/context-menu.svelte'; import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
import MenuOption from '../shared-components/context-menu/menu-option.svelte'; import MenuOption from '../shared-components/context-menu/menu-option.svelte';
import ThumbnailSelection from './thumbnail-selection.svelte'; import ThumbnailSelection from './thumbnail-selection.svelte';
import ControlAppBar from '../shared-components/control-app-bar.svelte';
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
@@ -61,7 +61,7 @@
$: { $: {
if (album.assets?.length < 6) { if (album.assets?.length < 6) {
thumbnailSize = Math.floor(viewWidth / album.assets.length - album.assets.length); thumbnailSize = Math.floor(viewWidth / album.assetCount - album.assetCount);
} else { } else {
thumbnailSize = Math.floor(viewWidth / 6 - 6); thumbnailSize = Math.floor(viewWidth / 6 - 6);
} }
@@ -69,7 +69,7 @@
const getDateRange = () => { const getDateRange = () => {
const startDate = new Date(album.assets[0].createdAt); const startDate = new Date(album.assets[0].createdAt);
const endDate = new Date(album.assets[album.assets.length - 1].createdAt); const endDate = new Date(album.assets[album.assetCount - 1].createdAt);
const timeFormatOption: Intl.DateTimeFormatOptions = { const timeFormatOption: Intl.DateTimeFormatOptions = {
month: 'short', month: 'short',
@@ -135,7 +135,7 @@
}; };
const navigateAssetForward = () => { const navigateAssetForward = () => {
try { try {
if (currentViewAssetIndex < album.assets.length - 1) { if (currentViewAssetIndex < album.assetCount - 1) {
currentViewAssetIndex++; currentViewAssetIndex++;
selectedAsset = album.assets[currentViewAssetIndex]; selectedAsset = album.assets[currentViewAssetIndex];
pushState(selectedAsset.id); pushState(selectedAsset.id);
@@ -272,7 +272,7 @@
<section class="bg-immich-bg"> <section class="bg-immich-bg">
<!-- Multiselection mode app bar --> <!-- Multiselection mode app bar -->
{#if isMultiSelectionMode} {#if isMultiSelectionMode}
<AlbumAppBar <ControlAppBar
on:close-button-click={clearMultiSelectAssetAssetHandler} on:close-button-click={clearMultiSelectAssetAssetHandler}
backIcon={Close} backIcon={Close}
tailwindClasses={'bg-white shadow-md'} tailwindClasses={'bg-white shadow-md'}
@@ -289,14 +289,14 @@
/> />
{/if} {/if}
</svelte:fragment> </svelte:fragment>
</AlbumAppBar> </ControlAppBar>
{/if} {/if}
<!-- Default app bar --> <!-- Default app bar -->
{#if !isMultiSelectionMode} {#if !isMultiSelectionMode}
<AlbumAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}> <ControlAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}>
<svelte:fragment slot="trailing"> <svelte:fragment slot="trailing">
{#if album.assets.length > 0} {#if album.assetCount > 0}
<CircleIconButton <CircleIconButton
title="Add Photos" title="Add Photos"
on:click={() => (isShowAssetSelection = true)} on:click={() => (isShowAssetSelection = true)}
@@ -322,14 +322,14 @@
{#if isCreatingSharedAlbum && album.sharedUsers.length == 0} {#if isCreatingSharedAlbum && album.sharedUsers.length == 0}
<button <button
disabled={album.assets.length == 0} disabled={album.assetCount == 0}
on:click={() => (isShowShareUserSelection = true)} on:click={() => (isShowShareUserSelection = true)}
class="immich-text-button border bg-immich-primary text-gray-50 hover:bg-immich-primary/75 px-6 text-sm disabled:opacity-25 disabled:bg-gray-500 disabled:cursor-not-allowed" class="immich-text-button border bg-immich-primary text-gray-50 hover:bg-immich-primary/75 px-6 text-sm disabled:opacity-25 disabled:bg-gray-500 disabled:cursor-not-allowed"
><span class="px-2">Share</span></button ><span class="px-2">Share</span></button
> >
{/if} {/if}
</svelte:fragment> </svelte:fragment>
</AlbumAppBar> </ControlAppBar>
{/if} {/if}
<section class="m-auto my-[160px] w-[60%]"> <section class="m-auto my-[160px] w-[60%]">
@@ -351,7 +351,7 @@
bind:this={titleInput} bind:this={titleInput}
/> />
{#if album.assets.length > 0} {#if album.assetCount > 0}
<p class="my-4 text-sm text-gray-500 font-medium">{getDateRange()}</p> <p class="my-4 text-sm text-gray-500 font-medium">{getDateRange()}</p>
{/if} {/if}
@@ -375,11 +375,11 @@
</div> </div>
{/if} {/if}
{#if album.assets.length > 0} {#if album.assetCount > 0}
<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}> <div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
{#each album.assets as asset} {#each album.assets as asset}
{#key asset.id} {#key asset.id}
{#if album.assets.length < 7} {#if album.assetCount < 7}
<ImmichThumbnail <ImmichThumbnail
{asset} {asset}
{thumbnailSize} {thumbnailSize}

View File

@@ -8,9 +8,9 @@
import moment from 'moment'; import moment from 'moment';
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte'; import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
import { AssetResponseDto } from '@api'; import { AssetResponseDto } from '@api';
import AlbumAppBar from './album-app-bar.svelte';
import { openFileUploadDialog, UploadType } from '$lib/utils/file-uploader'; import { openFileUploadDialog, UploadType } from '$lib/utils/file-uploader';
import { albumUploadAssetStore } from '$lib/stores/album-upload-asset'; import { albumUploadAssetStore } from '$lib/stores/album-upload-asset';
import ControlAppBar from '../shared-components/control-app-bar.svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -172,7 +172,7 @@
transition:fly={{ y: 500, duration: 100, easing: quintOut }} transition:fly={{ y: 500, duration: 100, easing: quintOut }}
class="absolute top-0 left-0 w-full h-full py-[160px] bg-immich-bg z-[9999]" class="absolute top-0 left-0 w-full h-full py-[160px] bg-immich-bg z-[9999]"
> >
<AlbumAppBar on:close-button-click={() => dispatch('go-back')}> <ControlAppBar on:close-button-click={() => dispatch('go-back')}>
<svelte:fragment slot="leading"> <svelte:fragment slot="leading">
{#if selectedAsset.size == 0} {#if selectedAsset.size == 0}
<p class="text-lg">Add to album</p> <p class="text-lg">Add to album</p>
@@ -195,7 +195,7 @@
><span class="px-2">Done</span></button ><span class="px-2">Done</span></button
> >
</svelte:fragment> </svelte:fragment>
</AlbumAppBar> </ControlAppBar>
<section class="flex flex-wrap gap-14 px-20 overflow-y-auto"> <section class="flex flex-wrap gap-14 px-20 overflow-y-auto">
{#each $assetsGroupByDate as assetsInDateGroup, groupIndex} {#each $assetsGroupByDate as assetsInDateGroup, groupIndex}

View File

@@ -3,8 +3,8 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import ControlAppBar from '../shared-components/control-app-bar.svelte';
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte'; import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
import AlbumAppBar from './album-app-bar.svelte';
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
@@ -24,7 +24,7 @@
transition:fly={{ y: 500, duration: 100, easing: quintOut }} transition:fly={{ y: 500, duration: 100, easing: quintOut }}
class="absolute top-0 left-0 w-full h-full py-[160px] bg-immich-bg z-[9999]" class="absolute top-0 left-0 w-full h-full py-[160px] bg-immich-bg z-[9999]"
> >
<AlbumAppBar on:close-button-click={() => dispatch('close')}> <ControlAppBar on:close-button-click={() => dispatch('close')}>
<svelte:fragment slot="leading"> <svelte:fragment slot="leading">
<p class="text-lg">Select album cover</p> <p class="text-lg">Select album cover</p>
</svelte:fragment> </svelte:fragment>
@@ -37,7 +37,7 @@
><span class="px-2">Done</span></button ><span class="px-2">Done</span></button
> >
</svelte:fragment> </svelte:fragment>
</AlbumAppBar> </ControlAppBar>
<section class="flex flex-wrap gap-14 px-20 overflow-y-auto"> <section class="flex flex-wrap gap-14 px-20 overflow-y-auto">
<!-- Image grid --> <!-- Image grid -->

View File

@@ -1,208 +1,207 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onDestroy, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import AsserViewerNavBar from './asser-viewer-nav-bar.svelte'; import AsserViewerNavBar from './asser-viewer-nav-bar.svelte';
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte'; import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte'; import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
import PhotoViewer from './photo-viewer.svelte'; import PhotoViewer from './photo-viewer.svelte';
import DetailPanel from './detail-panel.svelte'; import DetailPanel from './detail-panel.svelte';
import { downloadAssets } from '$lib/stores/download'; import { downloadAssets } from '$lib/stores/download';
import VideoViewer from './video-viewer.svelte'; import VideoViewer from './video-viewer.svelte';
import { api, AssetResponseDto, AssetTypeEnum } from '@api'; import { api, AssetResponseDto, AssetTypeEnum } from '@api';
import { browser } from '$app/env'; import { browser } from '$app/env';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
let halfLeftHover = false; let halfLeftHover = false;
let halfRightHover = false; let halfRightHover = false;
let isShowDetail = false; let isShowDetail = false;
onMount(() => { onMount(() => {
if (browser) { if (browser) {
document.addEventListener('keydown', (keyInfo) => handleKeyboardPress(keyInfo.key)); document.addEventListener('keydown', (keyInfo) => handleKeyboardPress(keyInfo.key));
} }
}); });
const handleKeyboardPress = (key: string) => { const handleKeyboardPress = (key: string) => {
switch (key) { switch (key) {
case 'Escape': case 'Escape':
closeViewer(); closeViewer();
return; return;
case 'i': case 'i':
isShowDetail = !isShowDetail; isShowDetail = !isShowDetail;
return; return;
case 'ArrowLeft': case 'ArrowLeft':
navigateAssetBackward(); navigateAssetBackward();
return; return;
case 'ArrowRight': case 'ArrowRight':
navigateAssetForward(); navigateAssetForward();
return; return;
} }
}; };
const closeViewer = () => { const closeViewer = () => {
dispatch('close'); dispatch('close');
}; };
const navigateAssetForward = (e?: Event) => { const navigateAssetForward = (e?: Event) => {
e?.stopPropagation(); e?.stopPropagation();
dispatch('navigate-forward'); dispatch('navigate-forward');
}; };
const navigateAssetBackward = (e?: Event) => { const navigateAssetBackward = (e?: Event) => {
e?.stopPropagation(); e?.stopPropagation();
dispatch('navigate-backward'); dispatch('navigate-backward');
}; };
const showDetailInfoHandler = () => { const showDetailInfoHandler = () => {
isShowDetail = !isShowDetail; isShowDetail = !isShowDetail;
}; };
const downloadFile = async () => { const downloadFile = async () => {
try { try {
const imageName = asset.exifInfo?.imageName ? asset.exifInfo?.imageName : asset.id; console.log(asset.exifInfo);
const imageExtension = asset.originalPath.split('.')[1]; const imageName = asset.exifInfo?.imageName ? asset.exifInfo?.imageName : asset.id;
const imageFileName = imageName + '.' + imageExtension; const imageExtension = asset.originalPath.split('.')[1];
const imageFileName = imageName + '.' + imageExtension;
// If assets is already download -> return; // If assets is already download -> return;
if ($downloadAssets[imageFileName]) { if ($downloadAssets[imageFileName]) {
return; return;
} }
$downloadAssets[imageFileName] = 0; $downloadAssets[imageFileName] = 0;
const { data, status } = await api.assetApi.downloadFile( const {data, status} = await api.assetApi.downloadFile(
asset.deviceAssetId, asset.deviceAssetId,
asset.deviceId, asset.deviceId,
false, false,
false, false,
{ {
responseType: 'blob', responseType: 'blob',
onDownloadProgress: (progressEvent) => { onDownloadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) { if (progressEvent.lengthComputable) {
const total = progressEvent.total; const total = progressEvent.total;
const current = progressEvent.loaded; const current = progressEvent.loaded;
let percentCompleted = Math.floor((current / total) * 100); $downloadAssets[imageFileName] = Math.floor((current / total) * 100);
}
}
}
);
$downloadAssets[imageFileName] = percentCompleted; if (!(data instanceof Blob)) {
} return;
} }
}
);
if (!(data instanceof Blob)) { if (status === 200) {
return; const fileUrl = URL.createObjectURL(data);
} const anchor = document.createElement('a');
anchor.href = fileUrl;
anchor.download = imageFileName;
if (status === 200) { document.body.appendChild(anchor);
const fileUrl = URL.createObjectURL(data); anchor.click();
const anchor = document.createElement('a'); document.body.removeChild(anchor);
anchor.href = fileUrl;
anchor.download = imageFileName;
document.body.appendChild(anchor); URL.revokeObjectURL(fileUrl);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(fileUrl); // Remove item from download list
setTimeout(() => {
// Remove item from download list const copy = $downloadAssets;
setTimeout(() => { delete copy[imageFileName];
const copy = $downloadAssets; $downloadAssets = copy;
delete copy[imageFileName]; }, 2000);
$downloadAssets = copy; }
}, 2000); } catch (e) {
} console.log('Error downloading file ', e);
} catch (e) { }
console.log('Error downloading file ', e); };
}
};
</script> </script>
<section <section
id="immich-asset-viewer" id="immich-asset-viewer"
class="fixed h-screen w-screen top-0 overflow-y-hidden bg-black z-[999] grid grid-rows-[64px_1fr] grid-cols-4 " class="fixed h-screen w-screen top-0 overflow-y-hidden bg-black z-[999] grid grid-rows-[64px_1fr] grid-cols-4 "
> >
<div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform"> <div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform">
<AsserViewerNavBar <AsserViewerNavBar
on:goBack={closeViewer} on:goBack={closeViewer}
on:showDetail={showDetailInfoHandler} on:showDetail={showDetailInfoHandler}
on:download={downloadFile} on:download={downloadFile}
/> />
</div> </div>
<div <div
class={`row-start-2 row-span-end col-start-1 col-span-2 flex place-items-center hover:cursor-pointer w-3/4 ${ class={`row-start-2 row-span-end col-start-1 col-span-2 flex place-items-center hover:cursor-pointer w-3/4 ${
asset.type == AssetTypeEnum.Video ? '' : 'z-[999]' asset.type === AssetTypeEnum.Video ? '' : 'z-[999]'
}`} }`}
on:mouseenter={() => { on:mouseenter={() => {
halfLeftHover = true; halfLeftHover = true;
halfRightHover = false; halfRightHover = false;
}} }}
on:mouseleave={() => { on:mouseleave={() => {
halfLeftHover = false; halfLeftHover = false;
}} }}
on:click={navigateAssetBackward} on:click={navigateAssetBackward}
> >
<button <button
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 z-[1000] text-gray-500 mx-4" class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 z-[1000] text-gray-500 mx-4"
class:navigation-button-hover={halfLeftHover} class:navigation-button-hover={halfLeftHover}
on:click={navigateAssetBackward} on:click={navigateAssetBackward}
> >
<ChevronLeft size="36" /> <ChevronLeft size="36"/>
</button> </button>
</div> </div>
<div class="row-start-1 row-span-full col-start-1 col-span-4"> <div class="row-start-1 row-span-full col-start-1 col-span-4">
{#key asset.id} {#key asset.id}
{#if asset.type == AssetTypeEnum.Image} {#if asset.type === AssetTypeEnum.Image}
<PhotoViewer assetId={asset.id} deviceId={asset.deviceId} on:close={closeViewer} /> <PhotoViewer assetId={asset.id} deviceId={asset.deviceId} on:close={closeViewer}/>
{:else} {:else}
<VideoViewer assetId={asset.id} on:close={closeViewer} /> <VideoViewer assetId={asset.id} on:close={closeViewer}/>
{/if} {/if}
{/key} {/key}
</div> </div>
<div <div
class={`row-start-2 row-span-full col-start-3 col-span-2 flex justify-end place-items-center hover:cursor-pointer w-3/4 justify-self-end ${ class={`row-start-2 row-span-full col-start-3 col-span-2 flex justify-end place-items-center hover:cursor-pointer w-3/4 justify-self-end ${
asset.type == AssetTypeEnum.Video ? '' : 'z-[500]' asset.type === AssetTypeEnum.Video ? '' : 'z-[500]'
}`} }`}
on:click={navigateAssetForward} on:click={navigateAssetForward}
on:mouseenter={() => { on:mouseenter={() => {
halfLeftHover = false; halfLeftHover = false;
halfRightHover = true; halfRightHover = true;
}} }}
on:mouseleave={() => { on:mouseleave={() => {
halfRightHover = false; halfRightHover = false;
}} }}
> >
<button <button
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4 z-[1000]" class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4 z-[1000]"
class:navigation-button-hover={halfRightHover} class:navigation-button-hover={halfRightHover}
on:click={navigateAssetForward} on:click={navigateAssetForward}
> >
<ChevronRight size="36" /> <ChevronRight size="36"/>
</button> </button>
</div> </div>
{#if isShowDetail} {#if isShowDetail}
<div <div
transition:fly={{ duration: 150 }} transition:fly={{ duration: 150 }}
id="detail-panel" id="detail-panel"
class="bg-immich-bg w-[360px] row-span-full transition-all " class="bg-immich-bg w-[360px] row-span-full transition-all "
translate="yes" translate="yes"
> >
<DetailPanel {asset} on:close={() => (isShowDetail = false)} /> <DetailPanel {asset} on:close={() => (isShowDetail = false)}/>
</div> </div>
{/if} {/if}
</section> </section>
<style> <style>
.navigation-button-hover { .navigation-button-hover {
background-color: rgb(107 114 128 / var(--tw-bg-opacity)); background-color: rgb(107 114 128 / var(--tw-bg-opacity));
color: rgb(55 65 81 / var(--tw-text-opacity)); color: rgb(55 65 81 / var(--tw-text-opacity));
transition: all 150ms; transition: all 150ms;
} }
</style> </style>

View File

@@ -47,7 +47,7 @@
<div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8"> <div class="border bg-white p-4 shadow-sm w-[500px] rounded-md py-8">
<div class="flex flex-col place-items-center place-content-center gap-4 px-4"> <div class="flex flex-col place-items-center place-content-center gap-4 px-4">
<img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo" /> <img class="text-center" src="/immich-logo.svg" height="100" width="100" alt="immich-logo" />
<h1 class="text-2xl text-immich-primary font-medium">Chage Password</h1> <h1 class="text-2xl text-immich-primary font-medium">Change Password</h1>
<p class="text-sm border rounded-md p-4 font-mono text-gray-600"> <p class="text-sm border rounded-md p-4 font-mono text-gray-600">
Hi {user.firstName} Hi {user.firstName}

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