Compare commits

...

11 Commits

Author SHA1 Message Date
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
41 changed files with 604 additions and 165 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

@@ -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" => 31,
"android.injected.version.name" => "1.20.0", "android.injected.version.name" => "1.21.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

@@ -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

@@ -104,5 +104,13 @@
"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"
} }

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.21.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

@@ -1,6 +1,10 @@
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/constants/immich_colors.dart';
@@ -49,6 +53,14 @@ void main() async {
Locale('it', 'IT'), Locale('it', 'IT'),
]; ];
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,

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,14 @@ 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,
fontSize: 12,
),
), ),
), ),
), ),
@@ -55,18 +61,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.assets.length == 1
? 'album_thumbnail_card_item'
: 'album_thumbnail_card_items',
style: const TextStyle( style: const TextStyle(
fontSize: 10, fontSize: 10,
), ),
), ).tr(args: ['${album.assets.length }']),
if (album.shared) if (album.shared)
const Text( const Text(
' · Shared', 'album_thumbnail_card_shared',
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
), ),
) ).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

@@ -37,8 +37,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());
} }
} }

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';
@@ -66,15 +67,15 @@ 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, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ).tr(),
) )
], ],
), ),
@@ -85,13 +86,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

@@ -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() {
if (widget.onLoadingStart != null) widget.onLoadingStart!();
}
void _fireFinishedLoadingEvent() {
if (widget.onLoadingCompleted != null) 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(
@@ -109,20 +141,26 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
} }
class RemotePhotoView extends StatefulWidget { class RemotePhotoView extends StatefulWidget {
const RemotePhotoView({ const RemotePhotoView(
Key? key, {Key? key,
required this.thumbnailUrl, required this.thumbnailUrl,
required this.imageUrl, required this.imageUrl,
required this.authToken, required this.authToken,
required this.isZoomedFunction, required this.isZoomedFunction,
required this.isZoomedListener, required this.isZoomedListener,
required this.onSwipeDown, required this.onSwipeDown,
required this.onSwipeUp, required this.onSwipeUp,
}) : super(key: key); this.previewUrl,
this.onLoadingCompleted,
this.onLoadingStart})
: 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

@@ -17,13 +17,13 @@ import 'package:openapi/api.dart';
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;
static const _threeStageLoading = false;
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;
@@ -32,6 +32,7 @@ class GalleryViewerPage extends HookConsumerWidget {
final Box<dynamic> box = Hive.box(userInfoBox); final Box<dynamic> box = Hive.box(userInfoBox);
int indexOfAsset = assetList.indexOf(asset); int indexOfAsset = assetList.indexOf(asset);
final loading = useState(false);
@override @override
void initState(int index) { void initState(int index) {
@@ -74,6 +75,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();
@@ -82,6 +84,10 @@ class GalleryViewerPage extends HookConsumerWidget {
ref ref
.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(
@@ -98,15 +104,14 @@ class GalleryViewerPage extends HookConsumerWidget {
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: () => loading.value = false,
onLoadingStart: () => loading.value = _threeStageLoading,
asset: assetList[index], asset: assetList[index],
heroTag: assetList[index].id, heroTag: assetList[index].id,
threeStageLoading: _threeStageLoading
); );
} else { } else {
return SwipeDetector( return SwipeDetector(

View File

@@ -8,27 +8,30 @@ 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;
@@ -68,14 +71,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),
authToken: authToken, previewUrl: threeStageLoading
isZoomedFunction: isZoomedFunction, ? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG)
isZoomedListener: isZoomedListener, : null,
onSwipeDown: () => AutoRouter.of(context).pop(), authToken: authToken,
onSwipeUp: () => showInfo(), isZoomedFunction: isZoomedFunction,
), isZoomedListener: isZoomedListener,
onSwipeDown: () => AutoRouter.of(context).pop(),
onSwipeUp: () => showInfo(),
onLoadingCompleted: onLoadingCompleted,
onLoadingStart: onLoadingStart),
), ),
), ),
if (downloadAssetStatus == DownloadAssetStatus.loading) if (downloadAssetStatus == DownloadAssetStatus.loading)

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,16 @@
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/ui/delete_diaglog.dart'; import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
class ControlBottomAppBar extends StatelessWidget { import '../../../shared/providers/asset.provider.dart';
import '../providers/home_page_state.provider.dart';
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,
@@ -25,7 +29,7 @@ class ControlBottomAppBar extends StatelessWidget {
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 +43,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 +85,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

@@ -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

@@ -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,
), ),

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>();
@@ -258,25 +256,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 +275,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 +286,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 +313,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 +334,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}';
} }
} }

View File

@@ -0,0 +1,45 @@
import 'dart:io';
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));
}
}

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

@@ -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

@@ -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.21.0+31
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

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

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;
@@ -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,12 +289,12 @@
/> />
{/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.assets.length > 0}
<CircleIconButton <CircleIconButton
@@ -329,7 +329,7 @@
> >
{/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%]">

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

@@ -3,6 +3,7 @@
import type { Load } from '@sveltejs/kit'; import type { Load } from '@sveltejs/kit';
import { setAssetInfo } from '$lib/stores/assets'; import { setAssetInfo } from '$lib/stores/assets';
export const load: Load = async ({ fetch, session }) => { export const load: Load = async ({ fetch, session }) => {
if (!browser && !session.user) { if (!browser && !session.user) {
return { return {
@@ -39,20 +40,31 @@
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte'; import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte'; import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { assetsGroupByDate, flattenAssetGroupByDate } from '$lib/stores/assets'; import { assetsGroupByDate, flattenAssetGroupByDate, assets } from '$lib/stores/assets';
import ImmichThumbnail from '$lib/components/shared-components/immich-thumbnail.svelte'; import ImmichThumbnail from '$lib/components/shared-components/immich-thumbnail.svelte';
import moment from 'moment'; import moment from 'moment';
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte'; import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
import { openFileUploadDialog, UploadType } from '$lib/utils/file-uploader'; import { openFileUploadDialog, UploadType } from '$lib/utils/file-uploader';
import { AssetResponseDto, UserResponseDto } from '@api'; import { api, AssetResponseDto, UserResponseDto } from '@api';
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte'; import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
import CircleOutline from 'svelte-material-icons/CircleOutline.svelte';
import CircleIconButton from '$lib/components/shared-components/circle-icon-button.svelte';
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
import Close from 'svelte-material-icons/Close.svelte';
import { browser } from '$app/env'; import { browser } from '$app/env';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
export let user: UserResponseDto; export let user: UserResponseDto;
let selectedGroupThumbnail: number | null; let selectedGroupThumbnail: number | null;
let isMouseOverGroup: boolean; let isMouseOverGroup: boolean;
let multiSelectedAssets = new Set<AssetResponseDto>();
$: isMultiSelectionMode = multiSelectedAssets.size > 0;
let selectedGroup: Set<number> = new Set();
let existingGroup: Set<number> = new Set();
$: if (isMouseOverGroup == false) { $: if (isMouseOverGroup == false) {
selectedGroupThumbnail = null; selectedGroupThumbnail = null;
} }
@@ -110,6 +122,91 @@
isShowAssetViewer = false; isShowAssetViewer = false;
history.pushState(null, '', `/photos`); history.pushState(null, '', `/photos`);
}; };
const selectAssetHandler = (asset: AssetResponseDto, groupIndex: number) => {
let temp = new Set(multiSelectedAssets);
if (multiSelectedAssets.has(asset)) {
temp.delete(asset);
const tempSelectedGroup = new Set(selectedGroup);
tempSelectedGroup.delete(groupIndex);
selectedGroup = tempSelectedGroup;
} else {
temp.add(asset);
}
multiSelectedAssets = temp;
// Check if all assets are selected in a group to toggle the group selection's icon
if (!selectedGroup.has(groupIndex)) {
const assetsInGroup = $assetsGroupByDate[groupIndex];
let selectedAssetsInGroupCount = 0;
assetsInGroup.forEach((asset) => {
if (multiSelectedAssets.has(asset)) {
selectedAssetsInGroupCount++;
}
});
// if all assets are selected in a group, add the group to selected group
if (selectedAssetsInGroupCount == assetsInGroup.length) {
selectedGroup = selectedGroup.add(groupIndex);
}
}
};
const clearMultiSelectAssetAssetHandler = () => {
multiSelectedAssets = new Set();
selectedGroup = new Set();
existingGroup = new Set();
};
const selectAssetGroupHandler = (groupIndex: number) => {
if (existingGroup.has(groupIndex)) return;
let tempSelectedGroup = new Set(selectedGroup);
let tempSelectedAsset = new Set(multiSelectedAssets);
if (selectedGroup.has(groupIndex)) {
tempSelectedGroup.delete(groupIndex);
tempSelectedAsset.forEach((asset) => {
if ($assetsGroupByDate[groupIndex].find((a) => a.id == asset.id)) {
tempSelectedAsset.delete(asset);
}
});
} else {
tempSelectedGroup.add(groupIndex);
tempSelectedAsset = new Set([...multiSelectedAssets, ...$assetsGroupByDate[groupIndex]]);
}
multiSelectedAssets = tempSelectedAsset;
selectedGroup = tempSelectedGroup;
};
const deleteSelectedAssetHandler = async () => {
try {
if (
window.confirm(
`Caution! Are you sure you want to delete ${multiSelectedAssets.size} assets? This step also deletes assets in the album(s) to which they belong. You can not undo this action!`
)
) {
const { data: deletedAssets } = await api.assetApi.deleteAsset({
ids: Array.from(multiSelectedAssets).map((a) => a.id)
});
for (const asset of deletedAssets) {
if (asset.status == 'SUCCESS') {
$assets = $assets.filter((a) => a.id !== asset.id);
}
}
clearMultiSelectAssetAssetHandler();
}
} catch (e) {
console.log('Error deleteSelectedAssetHandler', e);
}
};
</script> </script>
<svelte:head> <svelte:head>
@@ -117,7 +214,28 @@
</svelte:head> </svelte:head>
<section> <section>
<NavigationBar {user} on:uploadClicked={() => openFileUploadDialog(UploadType.GENERAL)} /> {#if isMultiSelectionMode}
<ControlAppBar
on:close-button-click={clearMultiSelectAssetAssetHandler}
backIcon={Close}
tailwindClasses={'bg-white shadow-md'}
>
<svelte:fragment slot="leading">
<p class="font-medium text-immich-primary">Selected {multiSelectedAssets.size}</p>
</svelte:fragment>
<svelte:fragment slot="trailing">
<CircleIconButton
title="Delete"
logo={DeleteOutline}
on:click={deleteSelectedAssetHandler}
/>
</svelte:fragment>
</ControlAppBar>
{/if}
{#if !isMultiSelectionMode}
<NavigationBar {user} on:uploadClicked={() => openFileUploadDialog(UploadType.GENERAL)} />
{/if}
</section> </section>
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg"> <section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
@@ -136,13 +254,20 @@
> >
<!-- Date group title --> <!-- Date group title -->
<p class="font-medium text-sm text-immich-fg mb-2 flex place-items-center h-6"> <p class="font-medium text-sm text-immich-fg mb-2 flex place-items-center h-6">
{#if selectedGroupThumbnail === groupIndex && isMouseOverGroup} {#if (selectedGroupThumbnail === groupIndex && isMouseOverGroup) || isMultiSelectionMode}
<div <div
in:fly={{ x: -24, duration: 200, opacity: 0.5 }} in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
out:fly={{ x: -24, duration: 200 }} out:fly={{ x: -24, duration: 200 }}
class="inline-block px-2 hover:cursor-pointer" class="inline-block px-2 hover:cursor-pointer"
on:click={() => selectAssetGroupHandler(groupIndex)}
> >
<CheckCircle size="24" color="#757575" /> {#if selectedGroup.has(groupIndex)}
<CheckCircle size="24" color="#4250af" />
{:else if existingGroup.has(groupIndex)}
<CheckCircle size="24" color="#757575" />
{:else}
<CircleOutline size="24" color="#757575" />
{/if}
</div> </div>
{/if} {/if}
@@ -156,7 +281,12 @@
<ImmichThumbnail <ImmichThumbnail
{asset} {asset}
on:mouseEvent={thumbnailMouseEventHandler} on:mouseEvent={thumbnailMouseEventHandler}
on:click={viewAssetHandler} on:click={(event) =>
isMultiSelectionMode
? selectAssetHandler(asset, groupIndex)
: viewAssetHandler(event)}
on:select={() => selectAssetHandler(asset, groupIndex)}
selected={multiSelectedAssets.has(asset)}
{groupIndex} {groupIndex}
/> />
{/key} {/key}