Compare commits

...

16 Commits

Author SHA1 Message Date
Alex
953d18e795 Remove serverEndpoint completely and fix upload path (#434) 2022-08-07 08:12:31 -05:00
Alex
b45024a97e Update README.md 2022-08-07 00:17:12 -05:00
Alex Tran
3dcdfa0166 Up android build version 2022-08-06 23:45:31 -05:00
Alex
2079583866 Update installation method and documentation (#424)
* Add installation script

* Populate instsall.sh

* format

* Get IP address on both macos and linux

* Update mobile version

* Remove test folder

* Added sed command for ios

* Added sed command for ios

* Fixed ios command

* Fixed ios command

* Added friendly debug message

* Update README

* Update Readme with new installation instruction

* Update message on instsallation script
2022-08-06 23:42:50 -05:00
Alex
b68358766b Remove VITE_SERVER_ENDPOINT dependency (#428)
* Move backend api to its own instance

* Remove external fetch hook

* Added endpoint for album

* Added endpoint for admin page

* Make request directly to immich-server

* Refactor unsued code
2022-08-06 18:14:54 -05:00
Alex Tran
cf2b9eddfa Pump version 1.20 2022-08-03 15:43:42 -05:00
Stevenson Chittumuri
8c184dc4d4 Enable swiping between assets (#381)
Enable swiping between assets (#381)

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Malte Kiefer <59220985+MalteKiefer@users.noreply.github.com>
Co-authored-by: Matthias Rupp <matthias.rupp@posteo.de>
2022-08-03 15:36:12 -05:00
Alex
e8d1f89a47 Implement album feature on mobile (#420)
* Refactor sharing to album

* Added library page in the bottom navigation bar

* Refactor SharedAlbumService to album service

* Refactor apiProvider to its file

* Added image grid

* render album thumbnail

* Using the wrap to render thumbnail and album info better

* Navigate to album viewer

* After deletion, navigate to the respective page of the shared and non-shared album

* Correctly remove album in local state

* Refactor create album page

* Implemented create non-shared album
2022-08-03 00:04:34 -05:00
Alex
0e85b0fd8f Remove print statement 2022-07-31 22:26:09 -05:00
Alex Tran
f7dc916e80 Fixed problem with Recent (isAll) album is both in exclude and include album list at the same time 2022-07-31 21:56:41 -05:00
Alex
03e7a254a2 Fixed logging out not redirect correctly in reverse proxy (#414)
* Remove check due to logout always success

* Added console log

* Remove console.log

* Up server version
2022-07-31 16:53:07 -05:00
Matthias Rupp
0ac9fe5a54 Load low- and high quality thumbnail in the same img tag to avoid flickering (#413) 2022-07-31 15:56:03 -05:00
Malte Kiefer
dc61fd925f fixed some German translations (#399) 2022-07-30 07:41:39 -05:00
Alex Tran
2aea08726f Update donation info 2022-07-29 13:42:39 -05:00
Alex Tran
746bec908b Update donation info 2022-07-29 13:41:29 -05:00
Alex Tran
8102e3b3f5 Fixed github action to conform with the move to org 2022-07-29 12:54:40 -05:00
97 changed files with 1319 additions and 560 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,4 +1,4 @@
# These are supported funding model platforms
github: alextran1502
custom: https://www.buymeacoffee.com/altran1502?new=1
custom: https://www.buymeacoffee.com/altran1502

View File

@@ -24,7 +24,7 @@ jobs:
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
if: ${{ github.repository == 'alextran1502/immich' }}
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -35,7 +35,7 @@ jobs:
context: ./server
file: ./server/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' && github.repository == 'alextran1502/immich' }}
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-server:staging
@@ -53,7 +53,7 @@ jobs:
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
if: ${{ github.repository == 'alextran1502/immich' }}
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -64,7 +64,7 @@ jobs:
context: ./machine-learning
file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' && github.repository == 'alextran1502/immich' }}
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-machine-learning:staging
@@ -81,7 +81,7 @@ jobs:
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
if: ${{ github.repository == 'alextran1502/immich' }}
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -93,7 +93,7 @@ jobs:
file: ./web/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
target: prod
push: ${{ github.event_name == 'pull_request' && github.repository == 'alextran1502/immich' }}
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-web:staging
@@ -110,7 +110,7 @@ jobs:
id: buildx
uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub
if: ${{ github.repository == 'alextran1502/immich' }}
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -121,6 +121,6 @@ jobs:
context: ./nginx
file: ./nginx/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64
push: ${{ github.event_name == 'pull_request' && github.repository == 'alextran1502/immich' }}
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-proxy:staging

107
README.md
View File

@@ -61,11 +61,11 @@ This project is under heavy development, there will be continuous functions, fea
| | Mobile | Web |
| - | - | - |
| Upload and view videos and photos | Yes | Yes
| Auto backup when app is opened | Yes | N/A
| Auto backup when the app is opened | Yes | N/A
| Selective album(s) for backup | Yes | N/A
| Download photos and videos to local device | Yes | Yes
| Multi-user support | Yes | Yes
| Album | No | Yes
| Album | Yes | Yes
| Shared Albums | Yes | Yes
| Quick navigation with draggable scrollbar | Yes | Yes
| Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | Yes | Yes
@@ -82,9 +82,9 @@ This project is under heavy development, there will be continuous functions, fea
**Core**: At least 2 cores, preffered 4 cores.
# Getting Started
# Technology Stack
You can use docker compose for development and testing out the application, there are several services that compose Immich:
There are several services that compose Immich:
1. **NestJs** - Backend of the application
2. **SvelteKit** - Web frontend of the application
@@ -93,19 +93,51 @@ You can use docker compose for development and testing out the application, ther
5. **Nginx** - Load balancing and optimized file uploading.
6. **TensorFlow** - Object Detection (COCO SSD) and Image Classification (ImageNet).
## Step 1: Populate .env file
# Installing
Navigate to `docker` directory and run
## One-step installation - for evaluating only
```
cp .env.example .env
*Applicable system: Ubuntu, Debian, MacOS*
*This installation method is for evaluating Immich before futher customization to meet the users' needs.*
In the shell, from the directory of your choice, run the following command:
```bash
curl -o- https://raw.githubusercontent.com/immich-app/immich/main/install.sh | bash
```
Then populate the value in there.
This script will download the `docker-compose.yml` file and the `.env` file, then populate the necessary information, and finally run the `docker-compose up` or `docker compose up` (based on your docker's version) command.
Notice that if set `ENABLE_MAPBOX` to `true`, you will have to provide `MAPBOX_KEY` for the server to run.
The web application will be available at `http://<machine-ip-address>:2283`, and the server URL for the mobile app will be `http://<machine-ip-address>:2283/api`.
Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is owned by the user that run the `docker-compose` command below.
The directory which is used to store the backup file is `./immich-app/immich-data`.
## Customize installation - for production usage
### Step 1 - Download necessary files
Create a directory called `immich-app` and cd into it. Then
Get `docker-compose.yml`
```bash
wget https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml
```
Get `.env`
```bash
wget -O .env wget https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example
```
### Step 2 - Populate .env file with customed information
* Populate customised database information if necessary.
* Populate `UPLOAD_LOCATION` as prefered location for storing backup assets.
* Populate a secret value for `JWT_SECRET`
* [Optional] Populate Mapbox value.
**Example**
@@ -133,36 +165,15 @@ JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
# ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
ENABLE_MAPBOX=false
MAPBOX_KEY=
###################################################################################
# WEB
###################################################################################
# This is the URL of your vm/server where you host Immich, so that the web frontend
# know where can it make the request to.
# For example: If your server IP address is 10.1.11.50, the environment variable will
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283/api
VITE_SERVER_ENDPOINT=http://192.168.1.216:2283/api
```
## Step 2: Start the server
### Step 3 - Start the containers
To **start**, run
Run `docker-compose up` or `docker-compose up` (based on your docker's version)
```bash
docker-compose -f ./docker/docker-compose.yml up
```
### Step 4 - Register admin user
To *update* docker-compose with newest image (if you have started the docker-compose previously)
```bash
docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up
```
The server will be running at `http://your-ip:2283/api`
## Step 3: Register User
Access the web interface at `http://your-ip:2283` to register an admin account.
Navigate to the web at `http://<machine-ip-address>:2283` and follow the prompts to register admin user.
<p align="left">
<img src="design/admin-registration-form.png" width="300" title="Admin Registration">
@@ -174,14 +185,16 @@ Additional accounts on the server can be created by the admin account.
<img src="design/admin-interface.png" width="500" title="Admin User Management">
<p/>
## Step 4: Run mobile app
### Step 5 - Access the mobile app
Login the mobile app with your server address
Login the mobile app with the server endpoint URL at `http://<machine-ip-address>:2283/api`
<p align="left">
<img src="design/login-screen.jpeg" width="250" title="Example login screen">
<p/>
## Mobile app
## F-Droid
You can get the app on F-droid by clicking the image below.
@@ -233,9 +246,23 @@ You can find the generated client SDK in the [`web/src/api`](web/src/api) for Ty
# Support
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**Github Sponsor**](https://github.com/sponsors/alextran1502), or a one time donation with the Buy Me a coffee link below.
If you like the app, find it helpful, and want to support me to offset the cost of publishing to AppStores, you can sponsor the project with [**one time**](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) or monthly donation from [**Github Sponsor**](https://github.com/sponsors/alextran1502)
You can also donate using crypto currency with the following addresses:
<p align="left" style="display: flex; place-items: center; gap: 20px" title="Bitcoin(BTC)">
<img src="design/bitcoin.png" width="25" title="Bitcoin">
<code>1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX</code>
</p>
<p align="left" style="display: flex; place-items: center; gap: 15px" title="Cardano(ADA)">
<img src="design/cardano.png" width="30" title="Cardano">
<code>
addr1qyy567vqhqrr3p7vpszr5p264gw89sqcwts2z8wqy4yek87cdmy79zazyjp7tmwhkluhk3krvslkzfvg0h43tytp3f5q49nycc
</code>
</p>
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/altran1502)
This is also a meaningful way to give me motivation and encouragement to continue working on the app.

BIN
design/bitcoin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
design/cardano.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -56,21 +56,6 @@ ENABLE_MAPBOX=false
MAPBOX_KEY=
###################################################################################
# WEB - Required
###################################################################################
# This is the URL of your vm/server where you host Immich, so that the web frontend
# know where can it make the request to.
# For example: If your server IP address is 10.1.11.50, the environment variable will
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283/api
# !CAUTION! THERE IS NO FORWARD SLASH AT THE END
VITE_SERVER_ENDPOINT=
####################################################################################
# WEB - Optional
####################################################################################

83
install.sh Executable file
View File

@@ -0,0 +1,83 @@
echo "Starting Immich installation..."
ip_address=$(hostname -I | awk '{print $1}')
RED='\033[0;31m'
GREEN='\032[0;31m'
NC='\033[0m' # No Color
machine_has() {
type "$1" >/dev/null 2>&1
}
create_immich_directory() {
echo "Creating Immich directory..."
mkdir -p ./immich-app/immich-data
}
download_docker_compose_file() {
echo "Downloading docker-compose.yml..."
curl -L https://raw.githubusercontent.com/immich-app/immich/main/docker/docker-compose.yml -o ./immich-app/docker-compose.yml >/dev/null 2>&1
}
download_dot_env_file() {
echo "Downloading .env file..."
curl -L https://raw.githubusercontent.com/immich-app/immich/main/docker/.env.example -o ./immich-app/.env >/dev/null 2>&1
}
populate_upload_location() {
echo "Populating default UPLOAD_LOCATION value..."
cd ./immich-app/immich-data
upload_location=$(pwd)
# Replace value of UPLOAD_LOCATION in .env with upload_location path
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s|UPLOAD_LOCATION=.*|UPLOAD_LOCATION=$upload_location|" ../.env
else
sed -i "s|UPLOAD_LOCATION=.*|UPLOAD_LOCATION=$upload_location|" ../.env
fi
cd ..
}
start_docker_compose() {
echo "Starting Immich's docker containers"
if machine_has "docker compose"; then {
docker compose up --remove-orphans -d
show_friendly_message
exit 0
}; fi
if machine_has "docker-compose"; then
docker-compose up --remove-orphans -d
show_friendly_message
exit 0
fi
}
show_friendly_message() {
echo "Succesfully deployed Immich!"
echo "You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api"
echo "The backup (or upload) location is $upload_location"
echo "---------------------------------------------------"
echo "If you want to confgure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
1. First bring down the containers with the command 'docker-compose down' in the immich-app directory,
2. Then change the information that fits your needs in the '.env' file,
3. Finally, bring the containers back up with the command 'docker-compose up --remove-orphans -d' in the immich-app directory"
}
# MAIN
create_immich_directory
download_docker_compose_file
download_dot_env_file
populate_upload_location
start_docker_compose

2
mobile/.gitignore vendored
View File

@@ -24,7 +24,7 @@
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
**/ios/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies

View File

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

View File

@@ -0,0 +1,2 @@
* New feature - Gallery view now enable with swipping action
* New feature - Add album feature

View File

@@ -67,10 +67,10 @@
"login_form_err_invalid_email": "Ungültige E-Mail",
"login_form_err_leading_whitespace": "Führendes Leerzichen",
"login_form_err_trailing_whitespace": "Folgendes Leerzeichen",
"login_form_failed_login": "Error logging you in, check server url, email and password",
"login_form_failed_login": "Fehler bei der Anmeldung, überprüfen Sie Server URL, E-Mail und Passwort",
"login_form_label_email": "E-Mail",
"login_form_label_password": "Passwort",
"login_form_password_hint": "password",
"login_form_password_hint": "Passwort",
"login_form_save_login": "Angemeldet bleiben",
"monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "App und Server sind aktuell",
@@ -83,7 +83,7 @@
"search_result_page_new_search_hint": "Neue Suche",
"select_additional_user_for_sharing_page_suggestions": "Vorschläge",
"select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
"select_user_for_sharing_page_share_suggestions": "Suggestions",
"select_user_for_sharing_page_share_suggestions": "Vorschläge",
"share_add": "Hinzufügen",
"share_add_photos": "Fotos hinzufügen",
"share_add_title": "Titel hinzufügen",
@@ -103,4 +103,4 @@
"version_announcement_overlay_text_2": "Bitte nehm dir die Zeit und lese das ",
"version_announcement_overlay_text_3": " und achte darauf, dass deine docker-compose und .env Dateien aktuell sind, vor allem wenn du ein System für automatische Updates benutzt (z.B. Watchtower).",
"version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89"
}
}

View File

@@ -47,6 +47,7 @@
"backup_info_card_assets": "assets",
"control_bottom_app_bar_delete": "Delete",
"create_shared_album_page_share": "Share",
"create_shared_album_page_create": "Create",
"create_shared_album_page_share_add_assets": "ADD ASSETS",
"create_shared_album_page_share_select_photos": "Select Photos",
"daily_title_text_date": "E, MMM dd",
@@ -97,10 +98,11 @@
"tab_controller_nav_photos": "Photos",
"tab_controller_nav_search": "Search",
"tab_controller_nav_sharing": "Sharing",
"tab_controller_nav_library": "Library",
"version_announcement_overlay_ack": "Acknowledge",
"version_announcement_overlay_release_notes": "release notes",
"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_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"
}
}

View File

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

View File

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

View File

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

View File

@@ -5,32 +5,34 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000227">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000213">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.526426">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.088407">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="7.096281">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="22.635867">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.476898">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.376681">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="102.893162">
<testcase classname="fastlane.lanes" name="4: build_app" time="91.762747">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="130.468341">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="49.149884">
<failure message="/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/actions/actions_helper.rb:67:in `execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:255:in `block in execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:229:in `chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:229:in `execute_action&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:157:in `trigger_action_by_name&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/fast_file.rb:159:in `method_missing&apos;&#10;Fastfile:30:in `block (2 levels) in parsing_binding&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/lane.rb:33:in `call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:49:in `block in execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:45:in `chdir&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/runner.rb:45:in `execute&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/lane_manager.rb:47:in `cruise_lane&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/command_line_handler.rb:36:in `handle&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/commands_generator.rb:110:in `block (2 levels) in run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in `call&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in `run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in `run_active_command&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in `run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in `run!&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/commands_generator.rb:354:in `run&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/commands_generator.rb:43:in `start&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/fastlane/lib/fastlane/cli_tools_distributor.rb:123:in `take_off&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/gems/fastlane-2.207.0/bin/fastlane:23:in `&lt;top (required)&gt;&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/bin/fastlane:25:in `load&apos;&#10;/opt/homebrew/Cellar/fastlane/2.207.0/libexec/bin/fastlane:25:in `&lt;main&gt;&apos;&#10;&#10;Error uploading ipa file: &#10; [Transporter Error Output]: ERROR ITMS-90186: Invalid Pre-Release Train. The train version &apos;1.19.0&apos; is closed for new build submissions
&#10;[Transporter Error Output]: ERROR ITMS-90062: This bundle is invalid. The value for key CFBundleShortVersionString [1.19.0] in the Info.plist file must contain a higher version than that of the previously approved version [1.19.0]. Please find more information about CFBundleShortVersionString at https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring
&#10;[Transporter Error Output]: Return status of iTunes Transporter was 1: ERROR ITMS-90186: Invalid Pre-Release Train. The train version &apos;1.19.0&apos; is closed for new build submissions
\nERROR ITMS-90062: This bundle is invalid. The value for key CFBundleShortVersionString [1.19.0] in the Info.plist file must contain a higher version than that of the previously approved version [1.19.0]. Please find more information about CFBundleShortVersionString at https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring

View File

@@ -17,7 +17,6 @@ import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
import 'constants/hive_box.dart';
void main() async {

View File

@@ -0,0 +1,40 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:openapi/api.dart';
class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
AlbumNotifier(this._albumService) : super([]);
final AlbumService _albumService;
getAllAlbums() async {
List<AlbumResponseDto>? albums =
await _albumService.getAlbums(isShared: false);
if (albums != null) {
state = albums;
}
}
deleteAlbum(String albumId) {
state = state.where((album) => album.id != albumId).toList();
}
Future<AlbumResponseDto?> createAlbum(
String albumTitle,
Set<AssetResponseDto> assets,
) async {
AlbumResponseDto? album =
await _albumService.createAlbum(albumTitle, assets, []);
if (album != null) {
state = [...state, album];
return album;
}
return null;
}
}
final albumProvider =
StateNotifierProvider<AlbumNotifier, List<AlbumResponseDto>>((ref) {
return AlbumNotifier(ref.watch(albumServiceProvider));
});

View File

@@ -1,7 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/models/album_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
import 'package:immich_mobile/modules/album/models/album_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
AlbumViewerNotifier(this.ref)
@@ -34,7 +34,7 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
String ownerId,
String newAlbumTitle,
) async {
SharedAlbumService service = ref.watch(sharedAlbumServiceProvider);
AlbumService service = ref.watch(albumServiceProvider);
bool isSuccess =
await service.changeTitleAlbum(albumId, ownerId, newAlbumTitle);

View File

@@ -1,5 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/models/asset_selection_state.model.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_state.model.dart';
import 'package:openapi/api.dart';

View File

@@ -1,30 +1,48 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:openapi/api.dart';
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
SharedAlbumNotifier(this._sharedAlbumService) : super([]);
final SharedAlbumService _sharedAlbumService;
final AlbumService _sharedAlbumService;
Future<AlbumResponseDto?> createSharedAlbum(
String albumName,
Set<AssetResponseDto> assets,
List<String> sharedUserIds,
) async {
try {
var newAlbum = await _sharedAlbumService.createAlbum(
albumName,
assets,
sharedUserIds,
);
if (newAlbum != null) {
state = [...state, newAlbum];
}
return newAlbum;
} catch (e) {
debugPrint("Error createSharedAlbum ${e.toString()}");
return null;
}
}
getAllSharedAlbums() async {
List<AlbumResponseDto>? sharedAlbums =
await _sharedAlbumService.getAllSharedAlbum();
await _sharedAlbumService.getAlbums(isShared: true);
if (sharedAlbums != null) {
state = sharedAlbums;
}
}
Future<bool> deleteAlbum(String albumId) async {
var res = await _sharedAlbumService.deleteAlbum(albumId);
if (res) {
state = state.where((album) => album.id != albumId).toList();
return true;
} else {
return false;
}
deleteAlbum(String albumId) async {
state = state.where((album) => album.id != albumId).toList();
}
Future<bool> leaveAlbum(String albumId) async {
@@ -54,13 +72,12 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
final sharedAlbumProvider =
StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
return SharedAlbumNotifier(ref.watch(sharedAlbumServiceProvider));
return SharedAlbumNotifier(ref.watch(albumServiceProvider));
});
final sharedAlbumDetailProvider = FutureProvider.autoDispose
.family<AlbumResponseDto?, String>((ref, albumId) async {
final SharedAlbumService sharedAlbumService =
ref.watch(sharedAlbumServiceProvider);
final AlbumService sharedAlbumService = ref.watch(albumServiceProvider);
return await sharedAlbumService.getAlbumDetail(albumId);
});

View File

@@ -2,46 +2,47 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:openapi/api.dart';
final sharedAlbumServiceProvider = Provider(
(ref) => SharedAlbumService(
final albumServiceProvider = Provider(
(ref) => AlbumService(
ref.watch(apiServiceProvider),
),
);
class SharedAlbumService {
class AlbumService {
final ApiService _apiService;
SharedAlbumService(this._apiService);
Future<List<AlbumResponseDto>?> getAllSharedAlbum() async {
AlbumService(this._apiService);
Future<List<AlbumResponseDto>?> getAlbums({required bool isShared}) async {
try {
return await _apiService.albumApi.getAllAlbums(shared: true);
return await _apiService.albumApi
.getAllAlbums(shared: isShared ? isShared : null);
} catch (e) {
debugPrint("Error getAllSharedAlbum ${e.toString()}");
return null;
}
}
Future<bool> createSharedAlbum(
Future<AlbumResponseDto?> createAlbum(
String albumName,
Set<AssetResponseDto> assets,
List<String> sharedUserIds,
) async {
try {
_apiService.albumApi.createAlbum(
return await _apiService.albumApi.createAlbum(
CreateAlbumDto(
albumName: albumName,
assetIds: assets.map((asset) => asset.id).toList(),
sharedWithUserIds: sharedUserIds,
),
);
return true;
} catch (e) {
debugPrint("Error createSharedAlbum ${e.toString()}");
return false;
return null;
}
}

View File

@@ -0,0 +1,77 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:openapi/api.dart';
import 'package:transparent_image/transparent_image.dart';
class AlbumThumbnailCard extends StatelessWidget {
const AlbumThumbnailCard({Key? key, required this.album}) : super(key: key);
final AlbumResponseDto album;
@override
Widget build(BuildContext context) {
var box = Hive.box(userInfoBox);
return GestureDetector(
onTap: () {
AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id));
},
child: Padding(
padding: const EdgeInsets.only(bottom: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: FadeInImage(
width: MediaQuery.of(context).size.width / 2 - 18,
height: MediaQuery.of(context).size.width / 2 - 18,
fit: BoxFit.cover,
placeholder: MemoryImage(kTransparentImage),
image: NetworkImage(
'${box.get(serverEndpointKey)}/asset/thumbnail/${album.albumThumbnailAssetId}?format=JPEG',
headers: {
"Authorization": "Bearer ${box.get(accessTokenKey)}"
},
),
fadeInDuration: const Duration(milliseconds: 200),
fadeOutDuration: const Duration(milliseconds: 200),
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
album.albumName,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${album.assets.length} item${album.assets.length > 1 ? 's' : ''}',
style: const TextStyle(
fontSize: 10,
),
),
if (album.shared)
const Text(
' · Shared',
style: TextStyle(
fontSize: 10,
),
)
],
)
],
),
),
);
}
}

View File

@@ -1,7 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
class AlbumTitleTextField extends ConsumerWidget {
const AlbumTitleTextField({

View File

@@ -4,9 +4,11 @@ import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
@@ -15,13 +17,12 @@ import 'package:openapi/api.dart';
class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
const AlbumViewerAppbar({
Key? key,
required AsyncValue<AlbumResponseDto?> albumInfo,
required this.albumInfo,
required this.userId,
required this.albumId,
}) : _albumInfo = albumInfo,
super(key: key);
}) : super(key: key);
final AsyncValue<AlbumResponseDto?> _albumInfo;
final AlbumResponseDto albumInfo;
final String userId;
final String albumId;
@@ -38,11 +39,18 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
ImmichLoadingOverlayController.appLoader.show();
bool isSuccess =
await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(albumId);
await ref.watch(albumServiceProvider).deleteAlbum(albumId);
if (isSuccess) {
AutoRouter.of(context)
.navigate(const TabControllerRoute(children: [SharingRoute()]));
if (albumInfo.shared) {
ref.watch(sharedAlbumProvider.notifier).deleteAlbum(albumId);
AutoRouter.of(context)
.navigate(const TabControllerRoute(children: [SharingRoute()]));
} else {
ref.watch(albumProvider.notifier).deleteAlbum(albumId);
AutoRouter.of(context)
.navigate(const TabControllerRoute(children: [LibraryRoute()]));
}
} else {
ImmichToast.show(
context: context,
@@ -105,7 +113,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
_buildBottomSheetActionButton() {
if (isMultiSelectionEnable) {
if (_albumInfo.asData?.value?.ownerId == userId) {
if (albumInfo.ownerId == userId) {
return ListTile(
leading: const Icon(Icons.delete_sweep_rounded),
title: const Text(
@@ -118,7 +126,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
return const SizedBox();
}
} else {
if (_albumInfo.asData?.value?.ownerId == userId) {
if (albumInfo.ownerId == userId) {
return ListTile(
leading: const Icon(Icons.delete_forever_rounded),
title: const Text(

View File

@@ -2,7 +2,7 @@ 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/sharing/providers/album_viewer.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
import 'package:openapi/api.dart';
class AlbumViewerEditableTitle extends HookConsumerWidget {

View File

@@ -6,14 +6,19 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/sharing/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:openapi/api.dart';
class AlbumViewerThumbnail extends HookConsumerWidget {
final AssetResponseDto asset;
final List<AssetResponseDto> assetList;
const AlbumViewerThumbnail({Key? key, required this.asset}) : super(key: key);
const AlbumViewerThumbnail({
Key? key,
required this.asset,
required this.assetList,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -28,25 +33,13 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
ref.watch(assetSelectionProvider).isMultiselectEnable;
_viewAsset() {
if (asset.type == AssetTypeEnum.IMAGE) {
AutoRouter.of(context).push(
ImageViewerRoute(
imageUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false',
heroTag: asset.id,
thumbnailUrl: thumbnailRequestUrl,
asset: asset,
),
);
} else {
AutoRouter.of(context).push(
VideoViewerRoute(
videoUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
asset: asset,
),
);
}
AutoRouter.of(context).push(
GalleryViewerRoute(
asset: asset,
assetList: assetList,
thumbnailRequestUrl: thumbnailRequestUrl,
),
);
}
BoxBorder drawBorderColor() {

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/ui/selection_thumbnail_image.dart';
import 'package:immich_mobile/modules/album/ui/selection_thumbnail_image.dart';
import 'package:openapi/api.dart';
class AssetGridByMonth extends HookConsumerWidget {

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:openapi/api.dart';
class MonthGroupTitle extends HookConsumerWidget {

View File

@@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:openapi/api.dart';
class SelectionThumbnailImage extends HookConsumerWidget {

View File

@@ -16,8 +16,6 @@ class SharingSliverAppBar extends StatelessWidget {
pinned: true,
snap: false,
automaticallyImplyLeading: false,
// leading: Container(),
// elevation: 0,
title: Text(
'IMMICH',
style: TextStyle(
@@ -46,7 +44,7 @@ class SharingSliverAppBar extends StatelessWidget {
),
onPressed: () {
AutoRouter.of(context)
.push(const CreateSharedAlbumRoute());
.push(CreateAlbumRoute(isSharedAlbum: true));
},
icon: const Icon(
Icons.photo_album_outlined,

View File

@@ -6,14 +6,14 @@ 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/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
import 'package:immich_mobile/modules/sharing/ui/album_action_outlined_button.dart';
import 'package:immich_mobile/modules/sharing/ui/album_viewer_appbar.dart';
import 'package:immich_mobile/modules/sharing/ui/album_viewer_editable_title.dart';
import 'package:immich_mobile/modules/sharing/ui/album_viewer_thumbnail.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
@@ -29,8 +29,7 @@ class AlbumViewerPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
FocusNode titleFocusNode = useFocusNode();
ScrollController scrollController = useScrollController();
AsyncValue<AlbumResponseDto?> albumInfo =
ref.watch(sharedAlbumDetailProvider(albumId));
var albumInfo = ref.watch(sharedAlbumDetailProvider(albumId));
final userId = ref.watch(authenticationProvider).userId;
@@ -53,12 +52,11 @@ class AlbumViewerPage extends HookConsumerWidget {
if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
ImmichLoadingOverlayController.appLoader.show();
var isSuccess = await ref
.watch(sharedAlbumServiceProvider)
.addAdditionalAssetToAlbum(
returnPayload.selectedAdditionalAsset,
albumId,
);
var isSuccess =
await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
returnPayload.selectedAdditionalAsset,
albumId,
);
if (isSuccess) {
ref.refresh(sharedAlbumDetailProvider(albumId));
@@ -83,7 +81,7 @@ class AlbumViewerPage extends HookConsumerWidget {
ImmichLoadingOverlayController.appLoader.show();
var isSuccess = await ref
.watch(sharedAlbumServiceProvider)
.watch(albumServiceProvider)
.addAdditionalUserToAlbum(sharedUserIds, albumId);
if (isSuccess) {
@@ -132,7 +130,11 @@ class AlbumViewerPage extends HookConsumerWidget {
String endDate = DateFormat('LLL d, y').format(parsedEndDate);
return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8),
padding: EdgeInsets.only(
left: 16.0,
top: 8.0,
bottom: albumInfo.shared ? 0.0 : 8.0,
),
child: Text(
"$startDate-$endDate",
style: const TextStyle(
@@ -152,31 +154,33 @@ class AlbumViewerPage extends HookConsumerWidget {
_buildTitle(albumInfo),
if (albumInfo.assets.isNotEmpty == true)
_buildAlbumDateRange(albumInfo),
SizedBox(
height: 60,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal,
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: CircleAvatar(
backgroundColor: Colors.grey[300],
radius: 18,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(50.0),
child:
Image.asset('assets/immich-logo-no-outline.png'),
if (albumInfo.shared)
SizedBox(
height: 60,
child: ListView.builder(
padding: const EdgeInsets.only(left: 16),
scrollDirection: Axis.horizontal,
itemBuilder: ((context, index) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: CircleAvatar(
backgroundColor: Colors.grey[300],
radius: 18,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(50.0),
child: Image.asset(
'assets/immich-logo-no-outline.png',
),
),
),
),
),
);
}),
itemCount: albumInfo.sharedUsers.length,
),
)
);
}),
itemCount: albumInfo.sharedUsers.length,
),
)
],
),
);
@@ -194,7 +198,10 @@ class AlbumViewerPage extends HookConsumerWidget {
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return AlbumViewerThumbnail(asset: albumInfo.assets[index]);
return AlbumViewerThumbnail(
asset: albumInfo.assets[index],
assetList: albumInfo.assets,
);
},
childCount: albumInfo.assets.length,
),
@@ -261,10 +268,19 @@ class AlbumViewerPage extends HookConsumerWidget {
}
return Scaffold(
appBar: AlbumViewerAppbar(
albumInfo: albumInfo,
userId: userId,
albumId: albumId,
appBar: albumInfo.when(
data: (AlbumResponseDto? data) {
if (data != null) {
return AlbumViewerAppbar(
albumInfo: data,
userId: userId,
albumId: albumId,
);
}
return null;
},
error: (e, _) => null,
loading: () => null,
),
body: albumInfo.when(
data: (albumInfo) => albumInfo != null

View File

@@ -3,15 +3,16 @@ 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/sharing/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/sharing/ui/asset_grid_by_month.dart';
import 'package:immich_mobile/modules/sharing/ui/month_group_title.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/ui/asset_grid_by_month.dart';
import 'package:immich_mobile/modules/album/ui/month_group_title.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
class AssetSelectionPage extends HookConsumerWidget {
const AssetSelectionPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
ScrollController scrollController = useScrollController();

View File

@@ -3,16 +3,20 @@ 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/sharing/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/sharing/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/sharing/ui/album_action_outlined_button.dart';
import 'package:immich_mobile/modules/sharing/ui/album_title_text_field.dart';
import 'package:immich_mobile/modules/sharing/ui/shared_album_thumbnail_image.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
import 'package:immich_mobile/modules/album/ui/album_title_text_field.dart';
import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart';
import 'package:immich_mobile/routing/router.dart';
class CreateSharedAlbumPage extends HookConsumerWidget {
const CreateSharedAlbumPage({Key? key}) : super(key: key);
// ignore: must_be_immutable
class CreateAlbumPage extends HookConsumerWidget {
bool isSharedAlbum;
CreateAlbumPage({Key? key, required this.isSharedAlbum}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -165,6 +169,21 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
return const SliverToBoxAdapter();
}
_createNonSharedAlbum() async {
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
ref.watch(albumTitleProvider),
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
);
if (newAlbum != null) {
ref.watch(albumProvider.notifier).getAllAlbums();
ref.watch(assetSelectionProvider.notifier).removeAll();
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
AutoRouter.of(context).replace(AlbumViewerRoute(albumId: newAlbum.id));
}
}
return Scaffold(
appBar: AppBar(
elevation: 0,
@@ -181,17 +200,31 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
style: TextStyle(color: Colors.black),
).tr(),
actions: [
TextButton(
onPressed: albumTitleController.text.isNotEmpty
? _showSelectUserPage
: null,
child: Text(
'create_shared_album_page_share'.tr(),
style: const TextStyle(
fontWeight: FontWeight.bold,
if (isSharedAlbum)
TextButton(
onPressed: albumTitleController.text.isNotEmpty
? _showSelectUserPage
: null,
child: Text(
'create_shared_album_page_share'.tr(),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
if (!isSharedAlbum)
TextButton(
onPressed: albumTitleController.text.isNotEmpty &&
selectedAssets.isNotEmpty
? _createNonSharedAlbum
: null,
child: Text(
'create_shared_album_page_create'.tr(),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
),
],
),
body: GestureDetector(

View File

@@ -0,0 +1,116 @@
import 'package:auto_route/auto_route.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/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
import 'package:immich_mobile/routing/router.dart';
class LibraryPage extends HookConsumerWidget {
const LibraryPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final albums = ref.watch(albumProvider);
useEffect(
() {
ref.read(albumProvider.notifier).getAllAlbums();
return null;
},
[],
);
Widget _buildAppBar() {
return SliverAppBar(
centerTitle: true,
floating: true,
pinned: false,
snap: false,
automaticallyImplyLeading: false,
title: Text(
'IMMICH',
style: TextStyle(
fontFamily: 'SnowburstOne',
fontWeight: FontWeight.bold,
fontSize: 22,
color: Theme.of(context).primaryColor,
),
),
);
}
Widget _buildCreateAlbumButton() {
return GestureDetector(
onTap: () {
AutoRouter.of(context).push(CreateAlbumRoute(isSharedAlbum: false));
},
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: MediaQuery.of(context).size.width / 2 - 18,
height: MediaQuery.of(context).size.width / 2 - 18,
decoration: BoxDecoration(
border: Border.all(
color: Colors.grey,
),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Icon(
Icons.add_rounded,
size: 28,
color: Theme.of(context).primaryColor,
),
),
),
const Padding(
padding: EdgeInsets.only(top: 8.0),
child: Text(
"New album",
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
)
],
),
);
}
return Scaffold(
body: CustomScrollView(
slivers: [
_buildAppBar(),
const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(12.0),
child: Text(
"Albums",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
SliverPadding(
padding: const EdgeInsets.only(left: 12.0, right: 12, bottom: 50),
sliver: SliverToBoxAdapter(
child: Wrap(
spacing: 12,
children: [
_buildCreateAlbumButton(),
for (var album in albums)
AlbumThumbnailCard(
album: album,
),
],
),
),
)
],
),
);
}
}

View File

@@ -3,7 +3,7 @@ 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/sharing/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:openapi/api.dart';

View File

@@ -3,11 +3,10 @@ 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/sharing/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:openapi/api.dart';
@@ -22,14 +21,14 @@ class SelectUserForSharingPage extends HookConsumerWidget {
ref.watch(suggestedSharedUsersProvider);
_createSharedAlbum() async {
var isSuccess =
await ref.watch(sharedAlbumServiceProvider).createSharedAlbum(
var newAlbum =
await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum(
ref.watch(albumTitleProvider),
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
sharedUsersList.value.map((userInfo) => userInfo.id).toList(),
);
if (isSuccess) {
if (newAlbum != null) {
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
ref.watch(assetSelectionProvider.notifier).removeAll();
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();

View File

@@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/sharing/ui/sharing_sliver_appbar.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/ui/sharing_sliver_appbar.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:openapi/api.dart';
import 'package:transparent_image/transparent_image.dart';
@@ -23,7 +23,6 @@ class SharingPage extends HookConsumerWidget {
useEffect(
() {
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
return null;
},
[],

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:openapi/api.dart';
import 'package:path/path.dart' as p;
@@ -14,6 +15,7 @@ final imageViewerServiceProvider =
class ImageViewerService {
final ApiService _apiService;
ImageViewerService(this._apiService);
Future<bool> downloadAssetToDevice(AssetResponseDto asset) async {

View File

@@ -15,7 +15,6 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
@override
Widget build(BuildContext context) {
bool allowMoving = _status == _RemoteImageStatus.full;
return PhotoView(
imageProvider: _imageProvider,
minScale: PhotoViewComputedScale.contained,
@@ -32,8 +31,9 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
PhotoViewControllerValue controllerValue,
) {
// Disable swipe events when zoomed in
if (_zoomedIn) return;
if (_zoomedIn) {
return;
}
if (controllerValue.position.dy > swipeThreshold) {
widget.onSwipeDown();
} else if (controllerValue.position.dy < -swipeThreshold) {
@@ -42,7 +42,14 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
}
void _scaleStateChanged(PhotoViewScaleState state) {
_zoomedIn = state == PhotoViewScaleState.zoomedIn;
// _onScaleListener;
_zoomedIn = state != PhotoViewScaleState.initial;
if (_zoomedIn) {
widget.isZoomedListener.value = true;
} else {
widget.isZoomedListener.value = false;
}
widget.isZoomedFunction();
}
CachedNetworkImageProvider _authorizedImageProvider(String url) {
@@ -107,6 +114,8 @@ class RemotePhotoView extends StatefulWidget {
required this.thumbnailUrl,
required this.imageUrl,
required this.authToken,
required this.isZoomedFunction,
required this.isZoomedListener,
required this.onSwipeDown,
required this.onSwipeUp,
}) : super(key: key);
@@ -117,6 +126,9 @@ class RemotePhotoView extends StatefulWidget {
final void Function() onSwipeDown;
final void Function() onSwipeUp;
final void Function() isZoomedFunction;
final ValueNotifier<bool> isZoomedListener;
@override
State<StatefulWidget> createState() {

View File

@@ -0,0 +1,134 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_swipe_detector/flutter_swipe_detector.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
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/video_viewer_page.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:openapi/api.dart';
// ignore: must_be_immutable
class GalleryViewerPage extends HookConsumerWidget {
late List<AssetResponseDto> assetList;
final AssetResponseDto asset;
final String thumbnailRequestUrl;
GalleryViewerPage({
Key? key,
required this.assetList,
required this.asset,
required this.thumbnailRequestUrl,
}) : super(key: key);
AssetResponseDto? assetDetail;
@override
Widget build(BuildContext context, WidgetRef ref) {
final Box<dynamic> box = Hive.box(userInfoBox);
int indexOfAsset = assetList.indexOf(asset);
@override
void initState(int index) {
indexOfAsset = index;
}
PageController controller =
PageController(initialPage: assetList.indexOf(asset));
getAssetExif() async {
assetDetail = await ref
.watch(assetServiceProvider)
.getAssetById(assetList[indexOfAsset].id);
}
void showInfo() {
showModalBottomSheet(
backgroundColor: Colors.black,
barrierColor: Colors.transparent,
isScrollControlled: false,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
},
);
}
final isZoomed = useState<bool>(false);
ValueNotifier<bool> isZoomedListener = ValueNotifier<bool>(false);
//make isZoomed listener call instead
void isZoomedMethod() {
if (isZoomedListener.value) {
isZoomed.value = true;
} else {
isZoomed.value = false;
}
}
return Scaffold(
backgroundColor: Colors.black,
appBar: TopControlAppBar(
asset: assetList[indexOfAsset],
onMoreInfoPressed: () {
showInfo();
},
onDownloadPressed: () {
ref
.watch(imageViewerStateProvider.notifier)
.downloadAsset(assetList[indexOfAsset], context);
},
),
body: SafeArea(
child: PageView.builder(
controller: controller,
pageSnapping: true,
physics: isZoomed.value
? const NeverScrollableScrollPhysics()
: const BouncingScrollPhysics(),
itemCount: assetList.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
initState(index);
getAssetExif();
if (assetList[index].type == AssetTypeEnum.IMAGE) {
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)}',
isZoomedFunction: isZoomedMethod,
isZoomedListener: isZoomedListener,
asset: assetList[index],
heroTag: assetList[index].id,
);
} else {
return SwipeDetector(
onSwipeDown: (_) {
AutoRouter.of(context).pop();
},
onSwipeUp: (_) {
showInfo();
},
child: Hero(
tag: assetList[index].id,
child: VideoViewerPage(
asset: assetList[index],
videoUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${assetList[index].deviceAssetId}&did=${assetList[index].deviceId}',
),
),
);
}
},
),
),
);
}
}

View File

@@ -1,15 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.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/top_control_app_bar.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:openapi/api.dart';
@@ -19,8 +16,9 @@ class ImageViewerPage extends HookConsumerWidget {
final String heroTag;
final String thumbnailUrl;
final AssetResponseDto asset;
AssetResponseDto? assetDetail;
final String authToken;
final ValueNotifier<bool> isZoomedListener;
final void Function() isZoomedFunction;
ImageViewerPage({
Key? key,
@@ -28,31 +26,22 @@ class ImageViewerPage extends HookConsumerWidget {
required this.heroTag,
required this.thumbnailUrl,
required this.asset,
required this.authToken,
required this.isZoomedFunction,
required this.isZoomedListener,
}) : super(key: key);
AssetResponseDto? assetDetail;
@override
Widget build(BuildContext context, WidgetRef ref) {
final downloadAssetStatus =
ref.watch(imageViewerStateProvider).downloadAssetStatus;
var box = Hive.box(userInfoBox);
getAssetExif() async {
assetDetail =
await ref.watch(assetServiceProvider).getAssetById(asset.id);
}
showInfo() {
showModalBottomSheet(
backgroundColor: Colors.black,
barrierColor: Colors.transparent,
isScrollControlled: false,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
},
);
}
useEffect(
() {
getAssetExif();
@@ -61,39 +50,39 @@ class ImageViewerPage extends HookConsumerWidget {
[],
);
return Scaffold(
backgroundColor: Colors.black,
appBar: TopControlAppBar(
asset: asset,
onMoreInfoPressed: showInfo,
onDownloadPressed: () {
ref
.watch(imageViewerStateProvider.notifier)
.downloadAsset(asset, context);
showInfo() {
showModalBottomSheet(
backgroundColor: Colors.black,
barrierColor: Colors.transparent,
isScrollControlled: false,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail ?? asset);
},
),
body: SafeArea(
child: Stack(
children: [
Center(
child: Hero(
tag: heroTag,
child: RemotePhotoView(
thumbnailUrl: thumbnailUrl,
imageUrl: imageUrl,
authToken: "Bearer ${box.get(accessTokenKey)}",
onSwipeDown: () => AutoRouter.of(context).pop(),
onSwipeUp: () => showInfo(),
),
),
);
}
return Stack(
children: [
Center(
child: Hero(
tag: heroTag,
child: RemotePhotoView(
thumbnailUrl: thumbnailUrl,
imageUrl: imageUrl,
authToken: authToken,
isZoomedFunction: isZoomedFunction,
isZoomedListener: isZoomedListener,
onSwipeDown: () => AutoRouter.of(context).pop(),
onSwipeUp: () => showInfo(),
),
if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
child: DownloadLoadingIndicator(),
),
],
),
),
),
if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
child: DownloadLoadingIndicator(),
),
],
);
}
}

View File

@@ -1,7 +1,4 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_swipe_detector/flutter_swipe_detector.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
@@ -9,9 +6,6 @@ import 'package:chewie/chewie.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:openapi/api.dart';
import 'package:video_player/video_player.dart';
@@ -31,66 +25,17 @@ class VideoViewerPage extends HookConsumerWidget {
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
void showInfo() {
showModalBottomSheet(
backgroundColor: Colors.black,
barrierColor: Colors.transparent,
isScrollControlled: false,
context: context,
builder: (context) {
return ExifBottomSheet(assetDetail: assetDetail!);
},
);
}
getAssetExif() async {
assetDetail =
await ref.watch(assetServiceProvider).getAssetById(asset.id);
}
useEffect(
() {
getAssetExif();
return null;
},
[],
);
return Scaffold(
backgroundColor: Colors.black,
appBar: TopControlAppBar(
asset: asset,
onMoreInfoPressed: () {
showInfo();
},
onDownloadPressed: () {
ref
.watch(imageViewerStateProvider.notifier)
.downloadAsset(asset, context);
},
),
body: SwipeDetector(
onSwipeDown: (_) {
AutoRouter.of(context).pop();
},
onSwipeUp: (_) {
showInfo();
},
child: SafeArea(
child: Stack(
children: [
VideoThumbnailPlayer(
url: videoUrl,
jwtToken: jwtToken,
),
if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
child: DownloadLoadingIndicator(),
),
],
),
return Stack(
children: [
VideoThumbnailPlayer(
url: videoUrl,
jwtToken: jwtToken,
),
),
if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
child: DownloadLoadingIndicator(),
),
],
);
}
}
@@ -134,10 +79,13 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
_createChewieController() {
chewieController = ChewieController(
showOptions: true,
showControlsOnInitialize: false,
showControlsOnInitialize: true,
videoPlayerController: videoPlayerController,
autoPlay: true,
autoInitialize: false,
autoInitialize: true,
allowFullScreen: true,
showControls: true,
hideControlsTimer: const Duration(seconds: 5),
);
}
@@ -157,11 +105,13 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
controller: chewieController!,
),
)
: const SizedBox(
width: 75,
height: 75,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
: const Center(
child: SizedBox(
width: 75,
height: 75,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
),
);
}

View File

@@ -162,6 +162,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
onlyAll: true,
type: RequestType.common,
);
if (list.isEmpty) {
return;
}
AssetPathEntity albumHasAllAssets = list.first;
backupAlbumInfoBox.put(

View File

@@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/utils/files_helper.dart';
import 'package:openapi/api.dart';
@@ -24,6 +25,7 @@ final backupServiceProvider = Provider(
class BackupService {
final ApiService _apiService;
BackupService(this._apiService);
Future<List<String>?> getDeviceBackupAsset() async {

View File

@@ -102,10 +102,12 @@ class AlbumInfoCard extends HookConsumerWidget {
HapticFeedback.selectionClick();
if (isExcluded) {
// Remove from exclude album list
ref
.watch(backupProvider.notifier)
.removeExcludedAlbumForBackup(albumInfo);
} else {
// Add to exclude album list
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
ref
.watch(backupProvider)
@@ -120,6 +122,16 @@ class AlbumInfoCard extends HookConsumerWidget {
return;
}
if (albumInfo.id == 'isAll') {
ImmichToast.show(
context: context,
msg: 'Cannot exclude album contains all assets',
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
return;
}
ref
.watch(backupProvider.notifier)
.addExcludedAlbumForBackup(albumInfo);

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:openapi/api.dart';

View File

@@ -3,10 +3,18 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
import 'package:openapi/api.dart';
// ignore: must_be_immutable
class ImageGrid extends ConsumerWidget {
final List<AssetResponseDto> assetGroup;
final List<AssetResponseDto> sortedAssetGroup;
const ImageGrid({Key? key, required this.assetGroup}) : super(key: key);
ImageGrid({
Key? key,
required this.assetGroup,
required this.sortedAssetGroup,
}) : super(key: key);
List<AssetResponseDto> imageSortedList = [];
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -19,12 +27,14 @@ class ImageGrid extends ConsumerWidget {
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
var assetType = assetGroup[index].type;
return GestureDetector(
onTap: () {},
child: Stack(
children: [
ThumbnailImage(asset: assetGroup[index]),
ThumbnailImage(
asset: assetGroup[index],
assetList: sortedAssetGroup,
),
if (assetType != AssetTypeEnum.IMAGE)
Positioned(
top: 5,

View File

@@ -13,8 +13,10 @@ import 'package:openapi/api.dart';
class ThumbnailImage extends HookConsumerWidget {
final AssetResponseDto asset;
final List<AssetResponseDto> assetList;
const ThumbnailImage({Key? key, required this.asset}) : super(key: key);
const ThumbnailImage({Key? key, required this.asset, required this.assetList})
: super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -60,29 +62,17 @@ class ThumbnailImage extends HookConsumerWidget {
.watch(homePageStateProvider.notifier)
.addSingleSelectedItem(asset);
} else {
if (asset.type == AssetTypeEnum.IMAGE) {
AutoRouter.of(context).push(
ImageViewerRoute(
imageUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false',
heroTag: asset.id,
thumbnailUrl: thumbnailRequestUrl,
asset: asset,
),
);
} else {
AutoRouter.of(context).push(
VideoViewerRoute(
videoUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
asset: asset,
),
);
}
AutoRouter.of(context).push(
GalleryViewerRoute(
assetList: assetList,
thumbnailRequestUrl: thumbnailRequestUrl,
asset: asset,
),
);
}
},
onLongPress: () {
// Enable multi selecte function
// Enable multi select function
ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset});
HapticFeedback.heavyImpact();
},

View File

@@ -10,9 +10,11 @@ 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/monthly_title_text.dart';
import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:openapi/api.dart';
class HomePage extends HookConsumerWidget {
const HomePage({Key? key}) : super(key: key);
@@ -25,6 +27,13 @@ class HomePage extends HookConsumerWidget {
var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable;
var homePageState = ref.watch(homePageStateProvider);
List<AssetResponseDto> sortedAssetList = [];
// set sorted List
for (var group in assetGroupByDateTime.values) {
for (var value in group) {
sortedAssetList.add(value);
}
}
useEffect(
() {
@@ -73,7 +82,10 @@ class HomePage extends HookConsumerWidget {
);
imageGridGroup.add(
ImageGrid(assetGroup: immichAssetList),
ImageGrid(
assetGroup: immichAssetList,
sortedAssetGroup: sortedAssetList,
),
);
lastMonth = currentMonth;

View File

@@ -5,6 +5,7 @@ import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/shared/services/device_info.service.dart';
import 'package:openapi/api.dart';

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:openapi/api.dart';
@@ -11,6 +12,7 @@ final searchServiceProvider = Provider(
class SearchService {
final ApiService _apiService;
SearchService(this._apiService);
Future<List<String>?> getUserSuggestedSearchTerms() async {

View File

@@ -11,6 +11,7 @@ import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
import 'package:immich_mobile/modules/search/providers/search_result_page.provider.dart';
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
import 'package:openapi/api.dart';
class SearchResultPage extends HookConsumerWidget {
const SearchResultPage({Key? key, required this.searchTerm})
@@ -27,7 +28,9 @@ class SearchResultPage extends HookConsumerWidget {
final List<Widget> imageGridGroup = [];
late FocusNode searchFocusNode;
FocusNode? searchFocusNode;
List<AssetResponseDto> sortedAssetList = [];
useEffect(
() {
@@ -37,14 +40,14 @@ class SearchResultPage extends HookConsumerWidget {
Duration.zero,
() => ref.read(searchResultPageProvider.notifier).search(searchTerm),
);
return () => searchFocusNode.dispose();
return () => searchFocusNode?.dispose();
},
[],
);
_onSearchSubmitted(String newSearchTerm) {
debugPrint("Re-Search with $newSearchTerm");
searchFocusNode.unfocus();
searchFocusNode?.unfocus();
isNewSearch.value = false;
currentSearchTerm.value = newSearchTerm;
ref.watch(searchResultPageProvider.notifier).search(newSearchTerm);
@@ -58,7 +61,7 @@ class SearchResultPage extends HookConsumerWidget {
onTap: () {
searchTermController.clear();
ref.watch(searchPageStateProvider.notifier).setSearchTerm("");
searchFocusNode.requestFocus();
searchFocusNode?.requestFocus();
},
textInputAction: TextInputAction.search,
onSubmitted: (searchTerm) {
@@ -131,7 +134,12 @@ class SearchResultPage extends HookConsumerWidget {
if (searchResultPageState.isSuccess) {
if (searchResultPageState.searchResult.isNotEmpty) {
int? lastMonth;
// set sorted List
for (var group in assetGroupByDateTime.values) {
for (var value in group) {
sortedAssetList.add(value);
}
}
assetGroupByDateTime.forEach((dateGroup, immichAssetList) {
DateTime parseDateGroup = DateTime.parse(dateGroup);
int currentMonth = parseDateGroup.month;
@@ -154,7 +162,10 @@ class SearchResultPage extends HookConsumerWidget {
);
imageGridGroup.add(
ImageGrid(assetGroup: immichAssetList),
ImageGrid(
assetGroup: immichAssetList,
sortedAssetGroup: sortedAssetList,
),
);
lastMonth = currentMonth;
@@ -193,7 +204,7 @@ class SearchResultPage extends HookConsumerWidget {
title: GestureDetector(
onTap: () {
isNewSearch.value = true;
searchFocusNode.requestFocus();
searchFocusNode?.requestFocus();
},
child: isNewSearch.value ? _buildTextField() : _buildChip(),
),
@@ -201,7 +212,10 @@ class SearchResultPage extends HookConsumerWidget {
),
body: GestureDetector(
onTap: () {
searchFocusNode.unfocus();
if (searchFocusNode != null) {
searchFocusNode?.unfocus();
}
ref.watch(searchPageStateProvider.notifier).disableSearch();
},
child: Stack(

View File

@@ -1,6 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/views/library_page.dart';
import 'package:immich_mobile/modules/asset_viewer/views/gallery_viewer.dart';
import 'package:immich_mobile/modules/backup/views/album_preview_page.dart';
import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart';
import 'package:immich_mobile/modules/backup/views/failed_backup_status_page.dart';
@@ -9,16 +11,17 @@ import 'package:immich_mobile/modules/login/views/login_page.dart';
import 'package:immich_mobile/modules/home/views/home_page.dart';
import 'package:immich_mobile/modules/search/views/search_page.dart';
import 'package:immich_mobile/modules/search/views/search_result_page.dart';
import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/sharing/views/album_viewer_page.dart';
import 'package:immich_mobile/modules/sharing/views/asset_selection_page.dart';
import 'package:immich_mobile/modules/sharing/views/create_shared_album_page.dart';
import 'package:immich_mobile/modules/sharing/views/select_additional_user_for_sharing_page.dart';
import 'package:immich_mobile/modules/sharing/views/select_user_for_sharing_page.dart';
import 'package:immich_mobile/modules/sharing/views/sharing_page.dart';
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/album/views/album_viewer_page.dart';
import 'package:immich_mobile/modules/album/views/asset_selection_page.dart';
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_user_for_sharing_page.dart';
import 'package:immich_mobile/modules/album/views/sharing_page.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/asset_viewer/views/image_viewer_page.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/shared/views/splash_screen.dart';
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
@@ -40,15 +43,17 @@ part 'router.gr.dart';
children: [
AutoRoute(page: HomePage, guards: [AuthGuard]),
AutoRoute(page: SearchPage, guards: [AuthGuard]),
AutoRoute(page: SharingPage, guards: [AuthGuard])
AutoRoute(page: SharingPage, guards: [AuthGuard]),
AutoRoute(page: LibraryPage, guards: [AuthGuard])
],
transitionsBuilder: TransitionsBuilders.fadeIn,
),
AutoRoute(page: GalleryViewerPage, guards: [AuthGuard]),
AutoRoute(page: ImageViewerPage, guards: [AuthGuard]),
AutoRoute(page: VideoViewerPage, guards: [AuthGuard]),
AutoRoute(page: BackupControllerPage, guards: [AuthGuard]),
AutoRoute(page: SearchResultPage, guards: [AuthGuard]),
AutoRoute(page: CreateSharedAlbumPage, guards: [AuthGuard]),
AutoRoute(page: CreateAlbumPage, guards: [AuthGuard]),
CustomRoute<AssetSelectionPageResult?>(
page: AssetSelectionPage,
guards: [AuthGuard],
@@ -75,7 +80,9 @@ part 'router.gr.dart';
],
)
class AppRouter extends _$AppRouter {
// ignore: unused_field
final ApiService _apiService;
AppRouter(this._apiService) : super(authGuard: AuthGuard(_apiService));
}

View File

@@ -41,6 +41,16 @@ class _$AppRouter extends RootStackRouter {
opaque: true,
barrierDismissible: false);
},
GalleryViewerRoute.name: (routeData) {
final args = routeData.argsAs<GalleryViewerRouteArgs>();
return MaterialPageX<dynamic>(
routeData: routeData,
child: GalleryViewerPage(
key: args.key,
assetList: args.assetList,
asset: args.asset,
thumbnailRequestUrl: args.thumbnailRequestUrl));
},
ImageViewerRoute.name: (routeData) {
final args = routeData.argsAs<ImageViewerRouteArgs>();
return MaterialPageX<dynamic>(
@@ -50,7 +60,10 @@ class _$AppRouter extends RootStackRouter {
imageUrl: args.imageUrl,
heroTag: args.heroTag,
thumbnailUrl: args.thumbnailUrl,
asset: args.asset));
asset: args.asset,
authToken: args.authToken,
isZoomedFunction: args.isZoomedFunction,
isZoomedListener: args.isZoomedListener));
},
VideoViewerRoute.name: (routeData) {
final args = routeData.argsAs<VideoViewerRouteArgs>();
@@ -69,9 +82,12 @@ class _$AppRouter extends RootStackRouter {
routeData: routeData,
child: SearchResultPage(key: args.key, searchTerm: args.searchTerm));
},
CreateSharedAlbumRoute.name: (routeData) {
CreateAlbumRoute.name: (routeData) {
final args = routeData.argsAs<CreateAlbumRouteArgs>();
return MaterialPageX<dynamic>(
routeData: routeData, child: const CreateSharedAlbumPage());
routeData: routeData,
child: CreateAlbumPage(
key: args.key, isSharedAlbum: args.isSharedAlbum));
},
AssetSelectionRoute.name: (routeData) {
return CustomPage<AssetSelectionPageResult?>(
@@ -136,6 +152,10 @@ class _$AppRouter extends RootStackRouter {
SharingRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData, child: const SharingPage());
},
LibraryRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData, child: const LibraryPage());
}
};
@@ -161,8 +181,14 @@ class _$AppRouter extends RootStackRouter {
RouteConfig(SharingRoute.name,
path: 'sharing-page',
parent: TabControllerRoute.name,
guards: [authGuard]),
RouteConfig(LibraryRoute.name,
path: 'library-page',
parent: TabControllerRoute.name,
guards: [authGuard])
]),
RouteConfig(GalleryViewerRoute.name,
path: '/gallery-viewer-page', guards: [authGuard]),
RouteConfig(ImageViewerRoute.name,
path: '/image-viewer-page', guards: [authGuard]),
RouteConfig(VideoViewerRoute.name,
@@ -171,8 +197,8 @@ class _$AppRouter extends RootStackRouter {
path: '/backup-controller-page', guards: [authGuard]),
RouteConfig(SearchResultRoute.name,
path: '/search-result-page', guards: [authGuard]),
RouteConfig(CreateSharedAlbumRoute.name,
path: '/create-shared-album-page', guards: [authGuard]),
RouteConfig(CreateAlbumRoute.name,
path: '/create-album-page', guards: [authGuard]),
RouteConfig(AssetSelectionRoute.name,
path: '/asset-selection-page', guards: [authGuard]),
RouteConfig(SelectUserForSharingRoute.name,
@@ -226,6 +252,46 @@ class TabControllerRoute extends PageRouteInfo<void> {
static const String name = 'TabControllerRoute';
}
/// generated route for
/// [GalleryViewerPage]
class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
GalleryViewerRoute(
{Key? key,
required List<AssetResponseDto> assetList,
required AssetResponseDto asset,
required String thumbnailRequestUrl})
: super(GalleryViewerRoute.name,
path: '/gallery-viewer-page',
args: GalleryViewerRouteArgs(
key: key,
assetList: assetList,
asset: asset,
thumbnailRequestUrl: thumbnailRequestUrl));
static const String name = 'GalleryViewerRoute';
}
class GalleryViewerRouteArgs {
const GalleryViewerRouteArgs(
{this.key,
required this.assetList,
required this.asset,
required this.thumbnailRequestUrl});
final Key? key;
final List<AssetResponseDto> assetList;
final AssetResponseDto asset;
final String thumbnailRequestUrl;
@override
String toString() {
return 'GalleryViewerRouteArgs{key: $key, assetList: $assetList, asset: $asset, thumbnailRequestUrl: $thumbnailRequestUrl}';
}
}
/// generated route for
/// [ImageViewerPage]
class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
@@ -234,7 +300,10 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
required String imageUrl,
required String heroTag,
required String thumbnailUrl,
required AssetResponseDto asset})
required AssetResponseDto asset,
required String authToken,
required void Function() isZoomedFunction,
required ValueNotifier<bool> isZoomedListener})
: super(ImageViewerRoute.name,
path: '/image-viewer-page',
args: ImageViewerRouteArgs(
@@ -242,7 +311,10 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
imageUrl: imageUrl,
heroTag: heroTag,
thumbnailUrl: thumbnailUrl,
asset: asset));
asset: asset,
authToken: authToken,
isZoomedFunction: isZoomedFunction,
isZoomedListener: isZoomedListener));
static const String name = 'ImageViewerRoute';
}
@@ -253,7 +325,10 @@ class ImageViewerRouteArgs {
required this.imageUrl,
required this.heroTag,
required this.thumbnailUrl,
required this.asset});
required this.asset,
required this.authToken,
required this.isZoomedFunction,
required this.isZoomedListener});
final Key? key;
@@ -265,9 +340,15 @@ class ImageViewerRouteArgs {
final AssetResponseDto asset;
final String authToken;
final void Function() isZoomedFunction;
final ValueNotifier<bool> isZoomedListener;
@override
String toString() {
return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl, asset: $asset}';
return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener}';
}
}
@@ -334,12 +415,27 @@ class SearchResultRouteArgs {
}
/// generated route for
/// [CreateSharedAlbumPage]
class CreateSharedAlbumRoute extends PageRouteInfo<void> {
const CreateSharedAlbumRoute()
: super(CreateSharedAlbumRoute.name, path: '/create-shared-album-page');
/// [CreateAlbumPage]
class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
CreateAlbumRoute({Key? key, required bool isSharedAlbum})
: super(CreateAlbumRoute.name,
path: '/create-album-page',
args: CreateAlbumRouteArgs(key: key, isSharedAlbum: isSharedAlbum));
static const String name = 'CreateSharedAlbumRoute';
static const String name = 'CreateAlbumRoute';
}
class CreateAlbumRouteArgs {
const CreateAlbumRouteArgs({this.key, required this.isSharedAlbum});
final Key? key;
final bool isSharedAlbum;
@override
String toString() {
return 'CreateAlbumRouteArgs{key: $key, isSharedAlbum: $isSharedAlbum}';
}
}
/// generated route for
@@ -492,3 +588,11 @@ class SharingRoute extends PageRouteInfo<void> {
static const String name = 'SharingRoute';
}
/// generated route for
/// [LibraryPage]
class LibraryRoute extends PageRouteInfo<void> {
const LibraryRoute() : super(LibraryRoute.name, path: 'library-page');
static const String name = 'LibraryRoute';
}

View File

@@ -1,8 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
class TabNavigationObserver extends AutoRouterObserver {
@@ -37,6 +38,9 @@ class TabNavigationObserver extends AutoRouterObserver {
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
}
if (route.name == 'LibraryRoute') {
ref.read(albumProvider.notifier).getAllAlbums();
}
ref.watch(serverInfoProvider.notifier).getServerVersion();
}
}

View File

@@ -0,0 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
final apiServiceProvider = Provider((ref) => ApiService());

View File

@@ -1,8 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:openapi/api.dart';
final apiServiceProvider = Provider((ref) => ApiService());
class ApiService {
late ApiClient _apiClient;
@@ -15,7 +12,6 @@ class ApiService {
setEndpoint(String endpoint) {
_apiClient = ApiClient(basePath: endpoint);
userApi = UserApi(_apiClient);
authenticationApi = AuthenticationApi(_apiClient);
albumApi = AlbumApi(_apiClient);

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:openapi/api.dart';
@@ -11,6 +12,7 @@ final serverInfoServiceProvider = Provider(
class ServerInfoService {
final ApiService _apiService;
ServerInfoService(this._apiService);
Future<ServerInfoResponseDto?> getServerInfo() async {

View File

@@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart';
import 'package:http_parser/http_parser.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/utils/files_helper.dart';
import 'package:openapi/api.dart';

View File

@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@@ -18,6 +19,7 @@ class TabControllerPage extends ConsumerWidget {
const HomeRoute(),
SearchRoute(),
const SharingRoute(),
const LibraryRoute()
],
builder: (context, child, animation) {
final tabsRouter = AutoTabsRouter.of(context);
@@ -34,12 +36,14 @@ class TabControllerPage extends ConsumerWidget {
bottomNavigationBar: isMultiSelectEnable
? null
: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
backgroundColor: immichBackgroundColor,
selectedLabelStyle: const TextStyle(
fontSize: 15,
fontSize: 13,
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: const TextStyle(
fontSize: 15,
fontSize: 13,
fontWeight: FontWeight.w600,
),
currentIndex: tabsRouter.activeIndex,
@@ -59,6 +63,12 @@ class TabControllerPage extends ConsumerWidget {
label: 'tab_controller_nav_sharing'.tr(),
icon: const Icon(Icons.group_outlined),
),
BottomNavigationBarItem(
label: 'tab_controller_nav_library'.tr(),
icon: const Icon(
Icons.photo_album_outlined,
),
)
],
),
),

View File

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

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.19.0+29
version: 1.20.0+30
environment:
sdk: ">=2.17.0 <3.0.0"

View File

@@ -16,6 +16,5 @@ export const immichAppConfig: ConfigModuleOptions = {
then: Joi.string().optional().allow(null, ''),
otherwise: Joi.string().required(),
}),
VITE_SERVER_ENDPOINT: Joi.string().required(),
}),
};

View File

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

View File

@@ -1 +0,0 @@
npm start immich

View File

@@ -1,4 +1,3 @@
import { serverEndpoint } from '$lib/constants';
import {
AlbumApi,
AssetApi,
@@ -16,7 +15,7 @@ class ImmichApi {
public authenticationApi: AuthenticationApi;
public deviceInfoApi: DeviceInfoApi;
public serverInfoApi: ServerInfoApi;
private config = new Configuration({ basePath: serverEndpoint });
private config = new Configuration({ basePath: '/api' });
constructor() {
this.userApi = new UserApi(this.config);
@@ -34,6 +33,12 @@ class ImmichApi {
public removeAccessToken() {
this.config.accessToken = undefined;
}
public setBaseUrl(baseUrl: string) {
this.config.basePath = baseUrl;
}
}
export const api = new ImmichApi();
export const serverApi = new ImmichApi();
serverApi.setBaseUrl('http://immich-server:3001');

View File

@@ -1,6 +1,6 @@
import type { ExternalFetch, GetSession, Handle } from '@sveltejs/kit';
import * as cookie from 'cookie';
import { api } from '@api';
import { api, serverApi } from '@api';
export const handle: Handle = async ({ event, resolve }) => {
const cookies = cookie.parse(event.request.headers.get('cookie') || '');
@@ -11,8 +11,8 @@ export const handle: Handle = async ({ event, resolve }) => {
const accessToken = cookies['immich_access_token'];
try {
api.setAccessToken(accessToken);
const { data } = await api.userApi.getMyUserInfo();
serverApi.setAccessToken(accessToken);
const { data } = await serverApi.userApi.getMyUserInfo();
event.locals.user = data;
return await resolve(event);

View File

@@ -7,20 +7,20 @@
export let album: AlbumResponseDto;
let imageData: string = '/no-thumbnail.png';
let imageData: string = `/api/asset/thumbnail/${album.albumThumbnailAssetId}?format=${ThumbnailFormat.Webp}`;
const dispatch = createEventDispatcher();
const loadHighQualityThumbnail = async (thubmnailId: string | null) => {
if (thubmnailId == null) {
return '/no-thumbnail.png';
return;
}
const { data } = await api.assetApi.getAssetThumbnail(thubmnailId!, ThumbnailFormat.Jpeg, {
responseType: 'blob'
});
if (data instanceof Blob) {
imageData = URL.createObjectURL(data);
return imageData;
return URL.createObjectURL(data);
}
};
@@ -30,6 +30,10 @@
y: e.clientY
});
};
onMount(async () => {
imageData = await loadHighQualityThumbnail(album.albumThumbnailAssetId) || 'no-thumbnail.png';
});
</script>
<div
@@ -50,19 +54,11 @@
</div>
<div class={`h-[275px] w-[275px] z-[-1]`}>
{#await loadHighQualityThumbnail(album.albumThumbnailAssetId)}
<img
src={`/api/asset/thumbnail/${album.albumThumbnailAssetId}?format=${ThumbnailFormat.Webp}`}
alt={album.id}
class={`object-cover w-full h-full transition-all z-0 rounded-xl duration-300 hover:shadow-lg`}
/>
{:then imageData}
<img
src={imageData}
alt={album.id}
class={`object-cover w-full h-full transition-all z-0 rounded-xl duration-300 hover:shadow-lg`}
/>
{/await}
<img
src={imageData}
alt={album.id}
class={`object-cover h-full w-full transition-all z-0 rounded-xl duration-300 hover:shadow-lg`}
/>
</div>
<div class="mt-4">

View File

@@ -60,7 +60,7 @@
});
$: {
if (album.assets.length < 6) {
if (album.assets?.length < 6) {
thumbnailSize = Math.floor(viewWidth / album.assets.length - album.assets.length);
} else {
thumbnailSize = Math.floor(viewWidth / 6 - 6);

View File

@@ -4,7 +4,6 @@
import type { ImmichUser } from '$lib/models/immich-user';
import { createEventDispatcher, onMount } from 'svelte';
import { fade, fly, slide } from 'svelte/transition';
import { serverEndpoint } from '../../constants';
import TrayArrowUp from 'svelte-material-icons/TrayArrowUp.svelte';
import { clickOutside } from '../../utils/click-outside';
import { api } from '@api';
@@ -43,11 +42,8 @@
};
const logOut = async () => {
const res = await fetch('auth/logout', { method: 'POST' });
if (res.status == 200 && res.statusText == 'OK') {
goto('/auth/login');
}
await fetch('auth/logout', { method: 'POST' });
goto('/auth/login');
};
</script>
@@ -96,7 +92,7 @@
>
{#if shouldShowProfileImage}
<img
src={`${serverEndpoint}/user/profile-image/${user.id}`}
src={`api/user/profile-image/${user.id}`}
alt="profile-img"
class="inline rounded-full h-12 w-12 object-cover shadow-md"
/>
@@ -134,7 +130,7 @@
>
{#if shouldShowProfileImage}
<img
src={`${serverEndpoint}/user/profile-image/${user.id}`}
src={`api/user/profile-image/${user.id}`}
alt="profile-img"
class="inline rounded-full h-20 w-20 object-cover shadow-md"
/>

View File

@@ -1,12 +1,10 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { serverEndpoint } from '$lib/constants';
import Cloud from 'svelte-material-icons/Cloud.svelte';
import Dns from 'svelte-material-icons/Dns.svelte';
import LoadingSpinner from './loading-spinner.svelte';
import { api, ServerInfoResponseDto } from '@api';
let endpoint = serverEndpoint;
let isServerOk = true;
let serverVersion = '';
let serverInfo: ServerInfoResponseDto;
@@ -82,11 +80,6 @@
<div class="text-xs">
<p class="text-sm font-medium text-immich-primary">Server</p>
<input
class="border p-2 rounded-md bg-gray-200 mt-2 text-immich-primary font-medium"
value={endpoint}
disabled={true}
/>
<div class="flex justify-items-center justify-between mt-2">
<p>Status</p>

View File

@@ -1,2 +1 @@
export const serverEndpoint: string = import.meta.env.VITE_SERVER_ENDPOINT;
export const loginPageMessage: string = import.meta.env.VITE_LOGIN_PAGE_MESSAGE;

View File

@@ -29,3 +29,7 @@ export const getAssetsInfo = async () => {
console.log('Error [getAssetsInfo]');
}
};
export const setAssetInfo = (data: AssetResponseDto[]) => {
assets.set(data);
};

View File

@@ -1,12 +1,9 @@
import { Socket, io } from 'socket.io-client';
import { writable } from 'svelte/store';
import { serverEndpoint } from '../constants';
let websocket: Socket;
export const openWebsocketConnection = () => {
const websocketEndpoint = serverEndpoint.replace('/api', '');
try {
websocket = io('', {
path: '/api/socket.io',

View File

@@ -1,6 +1,5 @@
/* @vite-ignore */
import * as exifr from 'exifr';
import { serverEndpoint } from '../constants';
import { uploadAssetsStore } from '$lib/stores/upload';
import type { UploadAsset } from '../models/upload-asset';
import { api, AssetFileUploadResponseDto } from '@api';
@@ -168,7 +167,7 @@ async function fileUploader(asset: File, uploadType: UploadType) {
uploadAssetsStore.updateProgress(deviceAssetId, percentComplete);
};
request.open('POST', `${serverEndpoint}/asset/upload`);
request.open('POST', `/api/asset/upload`);
request.send(formData);
} catch (e) {

View File

@@ -2,10 +2,12 @@
import type { Load } from '@sveltejs/kit';
import { api, UserResponseDto } from '@api';
export const load: Load = async () => {
export const load: Load = async ({ fetch }) => {
try {
const { data: allUsers } = await api.userApi.getAllUsers(false);
const { data: user } = await api.userApi.getMyUserInfo();
const [user, allUsers] = await Promise.all([
fetch('/data/user/get-my-user-info').then((r) => r.json()),
fetch('/data/user/get-all-users?isAll=false').then((r) => r.json())
]);
return {
status: 200,

View File

@@ -2,13 +2,15 @@
export const prerender = false;
import type { Load } from '@sveltejs/kit';
import { AlbumResponseDto, api } from '@api';
import { AlbumResponseDto } from '@api';
export const load: Load = async ({ params }) => {
export const load: Load = async ({ fetch, params }) => {
try {
const albumId = params['albumId'];
const { data: albumInfo } = await api.albumApi.getAlbumInfo(albumId);
const albumInfo = await fetch(`/data/album/get-album-info?albumId=${albumId}`).then((r) =>
r.json()
);
return {
status: 200,

View File

@@ -1,12 +1,10 @@
<script context="module" lang="ts">
export const prerender = false;
import { api } from '@api';
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ params }) => {
try {
await api.userApi.getMyUserInfo();
await fetch('/data/user/get-my-user-info');
} catch (e) {
return {
status: 302,

View File

@@ -9,10 +9,12 @@
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
import { AlbumResponseDto, api } from '@api';
export const load: Load = async () => {
export const load: Load = async ({ fetch }) => {
try {
const { data: user } = await api.userApi.getMyUserInfo();
const { data: albums } = await api.albumApi.getAllAlbums();
const [user, albums] = await Promise.all([
fetch('/data/user/get-my-user-info').then((r) => r.json()),
fetch('/data/album/get-all-albums').then((r) => r.json())
]);
return {
status: 200,

View File

@@ -0,0 +1 @@
This directory contain SSR endpoints to user serverApi instance to make request directly to DNS

View File

@@ -0,0 +1,18 @@
import { AlbumResponseDto, serverApi } from '@api';
import type { RequestEvent, RequestHandlerOutput } from '@sveltejs/kit';
export const GET = async ({
url
}: RequestEvent): Promise<RequestHandlerOutput<AlbumResponseDto>> => {
try {
const albumId = url.searchParams.get('albumId') || '';
const { data } = await serverApi.albumApi.getAlbumInfo(albumId);
return {
body: data
};
} catch {
return {
status: 500
};
}
};

View File

@@ -0,0 +1,18 @@
import { AlbumResponseDto, serverApi } from '@api';
import type { RequestEvent, RequestHandler, RequestHandlerOutput } from '@sveltejs/kit';
export const GET = async ({
url
}: RequestEvent): Promise<RequestHandlerOutput<AlbumResponseDto[]>> => {
try {
const isShared = url.searchParams.get('isShared') === 'true' || undefined;
const { data } = await serverApi.albumApi.getAllAlbums(isShared);
return {
body: data
};
} catch {
return {
status: 500
};
}
};

View File

@@ -0,0 +1,15 @@
import { AssetResponseDto, serverApi } from '@api';
import type { RequestHandlerOutput } from '@sveltejs/kit';
export const GET = async (): Promise<RequestHandlerOutput<AssetResponseDto[]>> => {
try {
const { data } = await serverApi.assetApi.getAllAssets();
return {
body: data
};
} catch {
return {
status: 500
};
}
};

View File

@@ -0,0 +1,17 @@
import { serverApi, UserResponseDto } from '@api';
import type { RequestEvent, RequestHandlerOutput } from '@sveltejs/kit';
export const GET = async ({url} : RequestEvent): Promise<RequestHandlerOutput<UserResponseDto[]>> => {
try {
const isAll = url.searchParams.get('isAll') === 'true';
const { data } = await serverApi.userApi.getAllUsers(isAll);
return {
body: data
};
} catch {
return {
status: 500
};
}
};

View File

@@ -0,0 +1,15 @@
import { serverApi, UserResponseDto } from '@api';
import type { RequestHandlerOutput } from '@sveltejs/kit';
export const GET = async (): Promise<RequestHandlerOutput<UserResponseDto>> => {
try {
const { data } = await serverApi.userApi.getMyUserInfo();
return {
body: data
};
} catch {
return {
status: 500
};
}
};

View File

@@ -4,28 +4,31 @@
import { api } from '@api';
export const load: Load = async () => {
try {
const { data: user } = await api.userApi.getMyUserInfo();
if (browser) {
try {
const { data: user } = await api.userApi.getMyUserInfo();
return {
status: 302,
redirect: '/photos'
};
} catch (e) {}
const { data } = await api.userApi.getUserCount();
return {
status: 302,
redirect: '/photos'
status: 200,
props: {
isAdminUserExist: data.userCount == 0 ? false : true
}
};
} catch (e) {}
const { data } = await api.userApi.getUserCount();
return {
status: 200,
props: {
isAdminUserExist: data.userCount == 0 ? false : true
}
};
}
};
</script>
<script lang="ts">
import { goto } from '$app/navigation';
import { browser } from '$app/env';
export let isAdminUserExist: boolean;

View File

@@ -1,12 +1,12 @@
<script context="module" lang="ts">
export const prerender = false;
import { api } from '@api';
import type { Load } from '@sveltejs/kit';
export const load: Load = async () => {
export const load: Load = async ({ fetch }) => {
try {
await api.userApi.getMyUserInfo();
await fetch('/data/user/get-my-user-info');
return {
status: 302,
redirect: '/photos'

View File

@@ -2,20 +2,25 @@
export const prerender = false;
import type { Load } from '@sveltejs/kit';
import { getAssetsInfo } from '$lib/stores/assets';
import { setAssetInfo } from '$lib/stores/assets';
export const load: Load = async () => {
export const load: Load = async ({ fetch }) => {
try {
const { data } = await api.userApi.getMyUserInfo();
await getAssetsInfo();
const [userInfo, assets] = await Promise.all([
fetch('/data/user/get-my-user-info').then((r) => r.json()),
fetch('/data/asset/get-all-assets').then((r) => r.json())
]);
setAssetInfo(assets);
return {
status: 200,
props: {
user: data
user: userInfo
}
};
} catch (e) {
console.log('ERROR load photos index');
return {
status: 302,
redirect: '/auth/login'
@@ -33,7 +38,7 @@
import moment from 'moment';
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
import { openFileUploadDialog, UploadType } from '$lib/utils/file-uploader';
import { api, AssetResponseDto, UserResponseDto } from '@api';
import { AssetResponseDto, UserResponseDto } from '@api';
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
export let user: UserResponseDto;

View File

@@ -4,10 +4,12 @@
import type { Load } from '@sveltejs/kit';
import { AlbumResponseDto, api, UserResponseDto } from '@api';
export const load: Load = async () => {
export const load: Load = async ({ fetch }) => {
try {
const { data: user } = await api.userApi.getMyUserInfo();
const { data: sharedAlbums } = await api.albumApi.getAllAlbums(true);
const [user, sharedAlbums] = await Promise.all([
fetch('/data/user/get-my-user-info').then((r) => r.json()),
fetch('/data/album/get-all-albums?isShared=true').then((r) => r.json())
]);
return {
status: 200,