Compare commits
34 Commits
v1.20.2_30
...
v1.23.0_33
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1390d01763 | ||
|
|
86f780871c | ||
|
|
c1b22125fd | ||
|
|
30f069a5db | ||
|
|
2bf6cd9241 | ||
|
|
87d2a954a3 | ||
|
|
a388c5a642 | ||
|
|
4b34f017ca | ||
|
|
5c1d1dd5a1 | ||
|
|
1580d27c23 | ||
|
|
4b9187928c | ||
|
|
5b7236f6ad | ||
|
|
6fb439b580 | ||
|
|
a8334b5c27 | ||
|
|
e1cac93945 | ||
|
|
081f9f5bce | ||
|
|
25ccc5660d | ||
|
|
b6d3e578f2 | ||
|
|
52377c2dcf | ||
|
|
5c78f707fe | ||
|
|
bd5ed1b684 | ||
|
|
e89339b813 | ||
|
|
0b69feda40 | ||
|
|
339f7f776f | ||
|
|
7e6ccbad21 | ||
|
|
aac53e5cdc | ||
|
|
cbec75a175 | ||
|
|
bf04d9eb39 | ||
|
|
3058c894b1 | ||
|
|
e57e279fe1 | ||
|
|
f43c58fc6d | ||
|
|
dea304ac39 | ||
|
|
b46e834220 | ||
|
|
46f4905259 |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
18
.github/workflows/test.yml
vendored
18
.github/workflows/test.yml
vendored
@@ -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
|
||||||
|
|||||||
3
Makefile
3
Makefile
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
* Improve performance
|
||||||
|
* Fix album title overflow
|
||||||
|
* New feature - Share asset from mobile to other apps
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Modify Album API endpoint to return count attribute instead of all assets to reduce network consumption and CPU processing.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* Added setting screen
|
||||||
|
* Implemented dark mode
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:229:in `chdir' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing' Fastfile:30:in `block (2 levels) in parsing_binding' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/lane.rb:33:in `call' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:45:in `chdir' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:45:in `execute' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle' /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' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command' /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!' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/commands_generator.rb:354:in `run' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/commands_generator.rb:43:in `start' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/bin/fastlane:23:in `<top (required)>' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/bin/fastlane:25:in `load' /opt/homebrew/Cellar/fastlane/2.207.0/libexec/bin/fastlane:25:in `<main>' Error uploading ipa file: [Transporter Error Output]: ERROR ITMS-90186: Invalid Pre-Release Train. The train version '1.19.0' is closed for new build submissions
|
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)],
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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()
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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))),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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))),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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: () {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart
Normal file
124
mobile/lib/modules/home/ui/profile_drawer/server_info_box.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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());
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
107
mobile/lib/modules/settings/ui/theme_setting/theme_setting.dart
Normal file
107
mobile/lib/modules/settings/ui/theme_setting/theme_setting.dart
Normal 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");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
mobile/lib/modules/settings/views/settings_page.dart
Normal file
45
mobile/lib/modules/settings/views/settings_page.dart
Normal 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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
47
mobile/lib/shared/services/share.service.dart
Normal file
47
mobile/lib/shared/services/share.service.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
mobile/lib/shared/ui/share_dialog.dart
Normal file
23
mobile/lib/shared/ui/share_dialog.dart
Normal 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(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
16
mobile/lib/utils/image_url_builder.dart
Normal file
16
mobile/lib/utils/image_url_builder.dart
Normal 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';
|
||||||
|
}
|
||||||
133
mobile/lib/utils/immich_app_theme.dart
Normal file
133
mobile/lib/utils/immich_app_theme.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -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** | |
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user