Compare commits

...

44 Commits

Author SHA1 Message Date
Alex Tran
5c1d1dd5a1 Added version note for f-droid 2022-08-12 20:10:00 -05:00
Alex Tran
1580d27c23 Up version 2022-08-12 20:06:45 -05:00
Alex
4b9187928c Edit user on the web (#458)
* Added dispatch event for edit user

* Fixed import location

* solve merge conflict

* Fixed issue not admin user can access admin page

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

* Refactor assets.length

* Explicitly declare type of assetCount so Dart SDK understand it

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

* Added control app bar

* Refactor AlbumAppBar into ControlAppBar

* Added addtional micro interactions when in multi selection mode

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

* Style share preparing dialog

* Share assets from multiselect

* Fix i18n

* Use navigator like in delete dialog

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

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

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

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

* i18n

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

* Revert asset response caching

* 3-step image loading in asset viewer

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

* Loading indicator

* Adapt to gallery PR

* Cleanup

* Dart format

* Fix exif sheet

* Disable three stage loading until settings are available
2022-08-07 19:43:09 -05:00
Alex Tran
46f4905259 Up server version 2022-08-07 18:42:21 -05:00
Alex
28c7736ecd Fix error in logout procedure and guard for each route (#439) 2022-08-07 18:36:34 -05:00
Alex Tran
f881981c44 Fix typo in Readme 2022-08-07 08:22:03 -05:00
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
129 changed files with 2473 additions and 997 deletions

2
.github/FUNDING.yml vendored
View File

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

View File

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

View File

@@ -24,18 +24,18 @@ jobs:
id: buildx id: buildx
uses: docker/setup-buildx-action@v2.0.0 uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub - name: Login to Docker Hub
if: ${{ github.repository == 'alextran1502/immich' }} if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich Mono Repo - name: Build and push Immich Mono Repo
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./server context: ./server
file: ./server/Dockerfile file: ./server/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64 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: | tags: |
altran1502/immich-server:staging altran1502/immich-server:staging
@@ -53,18 +53,18 @@ jobs:
id: buildx id: buildx
uses: docker/setup-buildx-action@v2.0.0 uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub - name: Login to Docker Hub
if: ${{ github.repository == 'alextran1502/immich' }} if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning - name: Build and Push Machine Learning
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./machine-learning context: ./machine-learning
file: ./machine-learning/Dockerfile file: ./machine-learning/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64 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: | tags: |
altran1502/immich-machine-learning:staging altran1502/immich-machine-learning:staging
@@ -81,19 +81,19 @@ jobs:
id: buildx id: buildx
uses: docker/setup-buildx-action@v2.0.0 uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub - name: Login to Docker Hub
if: ${{ github.repository == 'alextran1502/immich' }} if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Web - name: Build and Push Web
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./web context: ./web
file: ./web/Dockerfile file: ./web/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64 platforms: linux/arm/v7,linux/amd64,linux/arm64
target: prod target: prod
push: ${{ github.event_name == 'pull_request' && github.repository == 'alextran1502/immich' }} push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: | tags: |
altran1502/immich-web:staging altran1502/immich-web:staging
@@ -110,17 +110,17 @@ jobs:
id: buildx id: buildx
uses: docker/setup-buildx-action@v2.0.0 uses: docker/setup-buildx-action@v2.0.0
- name: Login to Docker Hub - name: Login to Docker Hub
if: ${{ github.repository == 'alextran1502/immich' }} if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Proxy - name: Build and Push Proxy
uses: docker/build-push-action@v3.1.0 uses: docker/build-push-action@v3.1.1
with: with:
context: ./nginx context: ./nginx
file: ./nginx/Dockerfile file: ./nginx/Dockerfile
platforms: linux/arm/v7,linux/amd64,linux/arm64 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: | tags: |
altran1502/immich-proxy:staging altran1502/immich-proxy:staging

View File

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

View File

@@ -2,11 +2,12 @@ name: Test
on: on:
workflow_dispatch: workflow_dispatch:
pull_request: pull_request:
push: { branches: master } push:
branches: [main]
jobs: jobs:
test-server-e2e: e2e-tests:
name: Run test suite name: Run end-to-end test suites
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -16,3 +17,14 @@ jobs:
- name: Run Immich Server 2E2 Test - name: Run Immich Server 2E2 Test
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
unit-tests:
name: Run unit test suites
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run tests
run: cd server && npm install && npm run test

View File

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

107
README.md
View File

@@ -61,11 +61,11 @@ This project is under heavy development, there will be continuous functions, fea
| | Mobile | Web | | | Mobile | Web |
| - | - | - | | - | - | - |
| Upload and view videos and photos | Yes | Yes | 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 | Selective album(s) for backup | Yes | N/A
| Download photos and videos to local device | Yes | Yes | Download photos and videos to local device | Yes | Yes
| Multi-user support | Yes | Yes | Multi-user support | Yes | Yes
| Album | No | Yes | Album | Yes | Yes
| Shared Albums | Yes | Yes | Shared Albums | Yes | Yes
| Quick navigation with draggable scrollbar | Yes | Yes | Quick navigation with draggable scrollbar | Yes | Yes
| Support RAW (HEIC, HEIF, DNG, Apple ProRaw) | 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. **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 1. **NestJs** - Backend of the application
2. **SvelteKit** - Web frontend 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. 5. **Nginx** - Load balancing and optimized file uploading.
6. **TensorFlow** - Object Detection (COCO SSD) and Image Classification (ImageNet). 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
``` *Applicable system: Ubuntu, Debian, MacOS*
cp .env.example .env
*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 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** **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 is either true of false -> if true, you have to provide MAPBOX_KEY
ENABLE_MAPBOX=false ENABLE_MAPBOX=false
MAPBOX_KEY= 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 ### Step 4 - Register admin user
docker-compose -f ./docker/docker-compose.yml up
```
To *update* docker-compose with newest image (if you have started the docker-compose previously) Navigate to the web at `http://<machine-ip-address>:2283` and follow the prompts to register admin user.
```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.
<p align="left"> <p align="left">
<img src="design/admin-registration-form.png" width="300" title="Admin Registration"> <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"> <img src="design/admin-interface.png" width="500" title="Admin User Management">
<p/> <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"> <p align="left">
<img src="design/login-screen.jpeg" width="250" title="Example login screen"> <img src="design/login-screen.jpeg" width="250" title="Example login screen">
<p/> <p/>
## Mobile app
## F-Droid ## F-Droid
You can get the app on F-droid by clicking the image below. 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 # 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. 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= 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 # 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 # Flutter/Dart/Pub related
**/doc/api/ **/doc/api/
**/ios/Flutter/.last_build_id **/ios/
.dart_tool/ .dart_tool/
.flutter-plugins .flutter-plugins
.flutter-plugins-dependencies .flutter-plugins-dependencies

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,6 +47,7 @@
"backup_info_card_assets": "Elemente", "backup_info_card_assets": "Elemente",
"control_bottom_app_bar_delete": "Löschen", "control_bottom_app_bar_delete": "Löschen",
"create_shared_album_page_share": "Teilen", "create_shared_album_page_share": "Teilen",
"create_shared_album_page_create": "Erstellen",
"create_shared_album_page_share_add_assets": "ELEMENTE HINZUFÜGEN", "create_shared_album_page_share_add_assets": "ELEMENTE HINZUFÜGEN",
"create_shared_album_page_share_select_photos": "Fotos auswählen", "create_shared_album_page_share_select_photos": "Fotos auswählen",
"daily_title_text_date": "E, dd MMM", "daily_title_text_date": "E, dd MMM",
@@ -67,10 +68,10 @@
"login_form_err_invalid_email": "Ungültige E-Mail", "login_form_err_invalid_email": "Ungültige E-Mail",
"login_form_err_leading_whitespace": "Führendes Leerzichen", "login_form_err_leading_whitespace": "Führendes Leerzichen",
"login_form_err_trailing_whitespace": "Folgendes Leerzeichen", "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_email": "E-Mail",
"login_form_label_password": "Passwort", "login_form_label_password": "Passwort",
"login_form_password_hint": "password", "login_form_password_hint": "Passwort",
"login_form_save_login": "Angemeldet bleiben", "login_form_save_login": "Angemeldet bleiben",
"monthly_title_text_date_format": "MMMM y", "monthly_title_text_date_format": "MMMM y",
"profile_drawer_client_server_up_to_date": "App und Server sind aktuell", "profile_drawer_client_server_up_to_date": "App und Server sind aktuell",
@@ -83,7 +84,7 @@
"search_result_page_new_search_hint": "Neue Suche", "search_result_page_new_search_hint": "Neue Suche",
"select_additional_user_for_sharing_page_suggestions": "Vorschläge", "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_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": "Hinzufügen",
"share_add_photos": "Fotos hinzufügen", "share_add_photos": "Fotos hinzufügen",
"share_add_title": "Titel hinzufügen", "share_add_title": "Titel hinzufügen",
@@ -97,10 +98,19 @@
"tab_controller_nav_photos": "Fotos", "tab_controller_nav_photos": "Fotos",
"tab_controller_nav_search": "Suche", "tab_controller_nav_search": "Suche",
"tab_controller_nav_sharing": "Teilen", "tab_controller_nav_sharing": "Teilen",
"tab_controller_nav_library": "Bibliothek",
"version_announcement_overlay_ack": "Ich habe verstanden", "version_announcement_overlay_ack": "Ich habe verstanden",
"version_announcement_overlay_release_notes": "Änderungsprotokoll", "version_announcement_overlay_release_notes": "Änderungsprotokoll",
"version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von", "version_announcement_overlay_text_1": "Hallo mein Freund! Es gibt eine neue Version von",
"version_announcement_overlay_text_2": "Bitte nehm dir die Zeit und lese das ", "version_announcement_overlay_text_2": "Bitte nehm dir die Zeit und lese das ",
"version_announcement_overlay_text_3": " und achte darauf, dass deine docker-compose und .env Dateien aktuell sind, vor allem wenn du ein System für automatische Updates benutzt (z.B. Watchtower).", "version_announcement_overlay_text_3": " und achte darauf, dass deine docker-compose und .env Dateien aktuell sind, vor allem wenn du ein System für automatische Updates benutzt (z.B. Watchtower).",
"version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89" "version_announcement_overlay_title": "Neue Server-Version verfügbar \uD83C\uDF89",
} "album_thumbnail_card_item": "1 Element",
"album_thumbnail_card_items": "{} Elemente",
"album_thumbnail_card_shared": " · Geteilt",
"library_page_albums": "Alben",
"library_page_new_album": "Neues Album",
"create_album_page_untitled": "Unbenannt",
"share_dialog_preparing": "Vorbereiten...",
"control_bottom_app_bar_share": "Teilen"
}

View File

@@ -47,6 +47,7 @@
"backup_info_card_assets": "assets", "backup_info_card_assets": "assets",
"control_bottom_app_bar_delete": "Delete", "control_bottom_app_bar_delete": "Delete",
"create_shared_album_page_share": "Share", "create_shared_album_page_share": "Share",
"create_shared_album_page_create": "Create",
"create_shared_album_page_share_add_assets": "ADD ASSETS", "create_shared_album_page_share_add_assets": "ADD ASSETS",
"create_shared_album_page_share_select_photos": "Select Photos", "create_shared_album_page_share_select_photos": "Select Photos",
"daily_title_text_date": "E, MMM dd", "daily_title_text_date": "E, MMM dd",
@@ -97,10 +98,19 @@
"tab_controller_nav_photos": "Photos", "tab_controller_nav_photos": "Photos",
"tab_controller_nav_search": "Search", "tab_controller_nav_search": "Search",
"tab_controller_nav_sharing": "Sharing", "tab_controller_nav_sharing": "Sharing",
"tab_controller_nav_library": "Library",
"version_announcement_overlay_ack": "Acknowledge", "version_announcement_overlay_ack": "Acknowledge",
"version_announcement_overlay_release_notes": "release notes", "version_announcement_overlay_release_notes": "release notes",
"version_announcement_overlay_text_1": "Hi friend, there is a new release of", "version_announcement_overlay_text_1": "Hi friend, there is a new release of",
"version_announcement_overlay_text_2": "please take your time to visit the ", "version_announcement_overlay_text_2": "please take your time to visit the ",
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89" "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
} "album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · Shared",
"library_page_albums": "Albums",
"library_page_new_album": "New album",
"create_album_page_untitled": "Untitled",
"share_dialog_preparing": "Preparing...",
"control_bottom_app_bar_share": "Share"
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000227"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000205">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.526426"> <testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.360401">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="7.096281"> <testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.012696">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.476898"> <testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.378836">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="102.893162"> <testcase classname="fastlane.lanes" name="4: build_app" time="80.023705">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="130.468341"> <testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="98.18403">
</testcase> </testcase>

View File

@@ -1,6 +1,10 @@
import 'dart:io';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/constants/immich_colors.dart';
@@ -17,7 +21,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/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:immich_mobile/shared/views/version_announcement_overlay.dart'; import 'package:immich_mobile/shared/views/version_announcement_overlay.dart';
import 'constants/hive_box.dart'; import 'constants/hive_box.dart';
void main() async { void main() async {
@@ -50,6 +53,14 @@ void main() async {
Locale('it', 'IT'), Locale('it', 'IT'),
]; ];
if (kReleaseMode && Platform.isAndroid) {
try {
await FlutterDisplayMode.setHighRefreshRate();
} catch (e) {
debugPrint("Error setting high refresh rate: $e");
}
}
runApp( runApp(
EasyLocalization( EasyLocalization(
supportedLocales: locales, supportedLocales: locales,

View File

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

View File

@@ -1,5 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; 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'; 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: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'; import 'package:openapi/api.dart';
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> { class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
SharedAlbumNotifier(this._sharedAlbumService) : super([]); 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 { getAllSharedAlbums() async {
List<AlbumResponseDto>? sharedAlbums = List<AlbumResponseDto>? sharedAlbums =
await _sharedAlbumService.getAllSharedAlbum(); await _sharedAlbumService.getAlbums(isShared: true);
if (sharedAlbums != null) { if (sharedAlbums != null) {
state = sharedAlbums; state = sharedAlbums;
} }
} }
Future<bool> deleteAlbum(String albumId) async { deleteAlbum(String albumId) async {
var res = await _sharedAlbumService.deleteAlbum(albumId); state = state.where((album) => album.id != albumId).toList();
if (res) {
state = state.where((album) => album.id != albumId).toList();
return true;
} else {
return false;
}
} }
Future<bool> leaveAlbum(String albumId) async { Future<bool> leaveAlbum(String albumId) async {
@@ -54,13 +72,12 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
final sharedAlbumProvider = final sharedAlbumProvider =
StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) { StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
return SharedAlbumNotifier(ref.watch(sharedAlbumServiceProvider)); return SharedAlbumNotifier(ref.watch(albumServiceProvider));
}); });
final sharedAlbumDetailProvider = FutureProvider.autoDispose final sharedAlbumDetailProvider = FutureProvider.autoDispose
.family<AlbumResponseDto?, String>((ref, albumId) async { .family<AlbumResponseDto?, String>((ref, albumId) async {
final SharedAlbumService sharedAlbumService = final AlbumService sharedAlbumService = ref.watch(albumServiceProvider);
ref.watch(sharedAlbumServiceProvider);
return await sharedAlbumService.getAlbumDetail(albumId); return await sharedAlbumService.getAlbumDetail(albumId);
}); });

View File

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

View File

@@ -0,0 +1,85 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.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);
final cardSize = MediaQuery.of(context).size.width / 2 - 18;
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: cardSize,
height: cardSize,
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: SizedBox(
width: cardSize,
child: Text(
album.albumName,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
album.assetCount == 1
? 'album_thumbnail_card_item'
: 'album_thumbnail_card_items',
style: const TextStyle(
fontSize: 10,
),
).tr(args: ['${album.assetCount}']),
if (album.shared)
const Text(
'album_thumbnail_card_shared',
style: TextStyle(
fontSize: 10,
),
).tr()
],
)
],
),
),
);
}
}

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class AlbumViewerEditableTitle extends HookConsumerWidget { class AlbumViewerEditableTitle extends HookConsumerWidget {

View File

@@ -6,21 +6,26 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.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:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class AlbumViewerThumbnail extends HookConsumerWidget { class AlbumViewerThumbnail extends HookConsumerWidget {
final AssetResponseDto asset; 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1); final cacheKey = useState(1);
var box = Hive.box(userInfoBox); var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = var thumbnailRequestUrl = getThumbnailUrl(asset);
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
var deviceId = ref.watch(authenticationProvider).deviceId; var deviceId = ref.watch(authenticationProvider).deviceId;
final selectedAssetsInAlbumViewer = final selectedAssetsInAlbumViewer =
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer; ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
@@ -28,25 +33,12 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
ref.watch(assetSelectionProvider).isMultiselectEnable; ref.watch(assetSelectionProvider).isMultiselectEnable;
_viewAsset() { _viewAsset() {
if (asset.type == AssetTypeEnum.IMAGE) { AutoRouter.of(context).push(
AutoRouter.of(context).push( GalleryViewerRoute(
ImageViewerRoute( asset: asset,
imageUrl: assetList: assetList,
'${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,
),
);
}
} }
BoxBorder drawBorderColor() { BoxBorder drawBorderColor() {

View File

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

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.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'; import 'package:openapi/api.dart';
class MonthGroupTitle extends HookConsumerWidget { 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:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart'; import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class SelectionThumbnailImage extends HookConsumerWidget { class SelectionThumbnailImage extends HookConsumerWidget {

View File

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

View File

@@ -16,8 +16,6 @@ class SharingSliverAppBar extends StatelessWidget {
pinned: true, pinned: true,
snap: false, snap: false,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
// leading: Container(),
// elevation: 0,
title: Text( title: Text(
'IMMICH', 'IMMICH',
style: TextStyle( style: TextStyle(
@@ -46,7 +44,7 @@ class SharingSliverAppBar extends StatelessWidget {
), ),
onPressed: () { onPressed: () {
AutoRouter.of(context) AutoRouter.of(context)
.push(const CreateSharedAlbumRoute()); .push(CreateAlbumRoute(isSharedAlbum: true));
}, },
icon: const Icon( icon: const Icon(
Icons.photo_album_outlined, 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/constants/immich_colors.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.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/modules/sharing/providers/shared_album.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart'; import 'package:immich_mobile/modules/album/services/album.service.dart';
import 'package:immich_mobile/modules/sharing/ui/album_action_outlined_button.dart'; import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
import 'package:immich_mobile/modules/sharing/ui/album_viewer_appbar.dart'; import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
import 'package:immich_mobile/modules/sharing/ui/album_viewer_editable_title.dart'; import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
import 'package:immich_mobile/modules/sharing/ui/album_viewer_thumbnail.dart'; import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.dart';
import 'package:immich_mobile/routing/router.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_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.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) { Widget build(BuildContext context, WidgetRef ref) {
FocusNode titleFocusNode = useFocusNode(); FocusNode titleFocusNode = useFocusNode();
ScrollController scrollController = useScrollController(); ScrollController scrollController = useScrollController();
AsyncValue<AlbumResponseDto?> albumInfo = var albumInfo = ref.watch(sharedAlbumDetailProvider(albumId));
ref.watch(sharedAlbumDetailProvider(albumId));
final userId = ref.watch(authenticationProvider).userId; final userId = ref.watch(authenticationProvider).userId;
@@ -53,12 +52,11 @@ class AlbumViewerPage extends HookConsumerWidget {
if (returnPayload.selectedAdditionalAsset.isNotEmpty) { if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
ImmichLoadingOverlayController.appLoader.show(); ImmichLoadingOverlayController.appLoader.show();
var isSuccess = await ref var isSuccess =
.watch(sharedAlbumServiceProvider) await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
.addAdditionalAssetToAlbum( returnPayload.selectedAdditionalAsset,
returnPayload.selectedAdditionalAsset, albumId,
albumId, );
);
if (isSuccess) { if (isSuccess) {
ref.refresh(sharedAlbumDetailProvider(albumId)); ref.refresh(sharedAlbumDetailProvider(albumId));
@@ -83,7 +81,7 @@ class AlbumViewerPage extends HookConsumerWidget {
ImmichLoadingOverlayController.appLoader.show(); ImmichLoadingOverlayController.appLoader.show();
var isSuccess = await ref var isSuccess = await ref
.watch(sharedAlbumServiceProvider) .watch(albumServiceProvider)
.addAdditionalUserToAlbum(sharedUserIds, albumId); .addAdditionalUserToAlbum(sharedUserIds, albumId);
if (isSuccess) { if (isSuccess) {
@@ -132,7 +130,11 @@ class AlbumViewerPage extends HookConsumerWidget {
String endDate = DateFormat('LLL d, y').format(parsedEndDate); String endDate = DateFormat('LLL d, y').format(parsedEndDate);
return Padding( 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( child: Text(
"$startDate-$endDate", "$startDate-$endDate",
style: const TextStyle( style: const TextStyle(
@@ -152,31 +154,33 @@ class AlbumViewerPage extends HookConsumerWidget {
_buildTitle(albumInfo), _buildTitle(albumInfo),
if (albumInfo.assets.isNotEmpty == true) if (albumInfo.assets.isNotEmpty == true)
_buildAlbumDateRange(albumInfo), _buildAlbumDateRange(albumInfo),
SizedBox( if (albumInfo.shared)
height: 60, SizedBox(
child: ListView.builder( height: 60,
padding: const EdgeInsets.only(left: 16), child: ListView.builder(
scrollDirection: Axis.horizontal, padding: const EdgeInsets.only(left: 16),
itemBuilder: ((context, index) { scrollDirection: Axis.horizontal,
return Padding( itemBuilder: ((context, index) {
padding: const EdgeInsets.only(right: 8.0), return Padding(
child: CircleAvatar( padding: const EdgeInsets.only(right: 8.0),
backgroundColor: Colors.grey[300], child: CircleAvatar(
radius: 18, backgroundColor: Colors.grey[300],
child: Padding( radius: 18,
padding: const EdgeInsets.all(2.0), child: Padding(
child: ClipRRect( padding: const EdgeInsets.all(2.0),
borderRadius: BorderRadius.circular(50.0), child: ClipRRect(
child: borderRadius: BorderRadius.circular(50.0),
Image.asset('assets/immich-logo-no-outline.png'), child: Image.asset(
'assets/immich-logo-no-outline.png',
),
),
), ),
), ),
), );
); }),
}), itemCount: albumInfo.sharedUsers.length,
itemCount: albumInfo.sharedUsers.length, ),
), )
)
], ],
), ),
); );
@@ -194,9 +198,12 @@ class AlbumViewerPage extends HookConsumerWidget {
), ),
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {
return AlbumViewerThumbnail(asset: albumInfo.assets[index]); return AlbumViewerThumbnail(
asset: albumInfo.assets[index],
assetList: albumInfo.assets,
);
}, },
childCount: albumInfo.assets.length, childCount: albumInfo.assetCount,
), ),
), ),
); );
@@ -261,10 +268,19 @@ class AlbumViewerPage extends HookConsumerWidget {
} }
return Scaffold( return Scaffold(
appBar: AlbumViewerAppbar( appBar: albumInfo.when(
albumInfo: albumInfo, data: (AlbumResponseDto? data) {
userId: userId, if (data != null) {
albumId: albumId, return AlbumViewerAppbar(
albumInfo: data,
userId: userId,
albumId: albumId,
);
}
return null;
},
error: (e, _) => null,
loading: () => null,
), ),
body: albumInfo.when( body: albumInfo.when(
data: (albumInfo) => albumInfo != null 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/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.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/modules/sharing/ui/asset_grid_by_month.dart'; import 'package:immich_mobile/modules/album/ui/asset_grid_by_month.dart';
import 'package:immich_mobile/modules/sharing/ui/month_group_title.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/shared/providers/asset.provider.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
class AssetSelectionPage extends HookConsumerWidget { class AssetSelectionPage extends HookConsumerWidget {
const AssetSelectionPage({Key? key}) : super(key: key); const AssetSelectionPage({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
ScrollController scrollController = useScrollController(); ScrollController scrollController = useScrollController();

View File

@@ -3,16 +3,20 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/sharing/providers/album_title.provider.dart'; import 'package:immich_mobile/modules/album/providers/album.provider.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
import 'package:immich_mobile/modules/sharing/ui/album_action_outlined_button.dart'; import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
import 'package:immich_mobile/modules/sharing/ui/album_title_text_field.dart'; import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
import 'package:immich_mobile/modules/sharing/ui/shared_album_thumbnail_image.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'; import 'package:immich_mobile/routing/router.dart';
class CreateSharedAlbumPage extends HookConsumerWidget { // ignore: must_be_immutable
const CreateSharedAlbumPage({Key? key}) : super(key: key); class CreateAlbumPage extends HookConsumerWidget {
bool isSharedAlbum;
CreateAlbumPage({Key? key, required this.isSharedAlbum}) : super(key: key);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -33,8 +37,10 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
isAlbumTitleTextFieldFocus.value = false; isAlbumTitleTextFieldFocus.value = false;
if (albumTitleController.text.isEmpty) { if (albumTitleController.text.isEmpty) {
albumTitleController.text = 'Untitled'; albumTitleController.text = 'create_album_page_untitled'.tr();
ref.watch(albumTitleProvider.notifier).setAlbumTitle('Untitled'); ref
.watch(albumTitleProvider.notifier)
.setAlbumTitle('create_album_page_untitled'.tr());
} }
} }
@@ -165,6 +171,21 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
return const SliverToBoxAdapter(); 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
elevation: 0, elevation: 0,
@@ -181,17 +202,31 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
style: TextStyle(color: Colors.black), style: TextStyle(color: Colors.black),
).tr(), ).tr(),
actions: [ actions: [
TextButton( if (isSharedAlbum)
onPressed: albumTitleController.text.isNotEmpty TextButton(
? _showSelectUserPage onPressed: albumTitleController.text.isNotEmpty
: null, ? _showSelectUserPage
child: Text( : null,
'create_shared_album_page_share'.tr(), child: Text(
style: const TextStyle( 'create_shared_album_page_share'.tr(),
fontWeight: FontWeight.bold, 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( body: GestureDetector(

View File

@@ -0,0 +1,117 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package: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,
),
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: const Text(
'library_page_new_album',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
).tr(),
)
],
),
);
}
return Scaffold(
body: CustomScrollView(
slivers: [
_buildAppBar(),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: const Text(
'library_page_albums',
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
),
),
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/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/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:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:openapi/api.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/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/providers/album_title.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_title.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/modules/sharing/providers/shared_album.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.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/modules/sharing/services/shared_album.service.dart';
import 'package:immich_mobile/routing/router.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_loading_indicator.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@@ -22,14 +21,14 @@ class SelectUserForSharingPage extends HookConsumerWidget {
ref.watch(suggestedSharedUsersProvider); ref.watch(suggestedSharedUsersProvider);
_createSharedAlbum() async { _createSharedAlbum() async {
var isSuccess = var newAlbum =
await ref.watch(sharedAlbumServiceProvider).createSharedAlbum( await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum(
ref.watch(albumTitleProvider), ref.watch(albumTitleProvider),
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum, ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
sharedUsersList.value.map((userInfo) => userInfo.id).toList(), sharedUsersList.value.map((userInfo) => userInfo.id).toList(),
); );
if (isSuccess) { if (newAlbum != null) {
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums(); await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
ref.watch(assetSelectionProvider.notifier).removeAll(); ref.watch(assetSelectionProvider.notifier).removeAll();
ref.watch(albumTitleProvider.notifier).clearAlbumTitle(); 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:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
import 'package:immich_mobile/modules/sharing/ui/sharing_sliver_appbar.dart'; import 'package:immich_mobile/modules/album/ui/sharing_sliver_appbar.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
import 'package:transparent_image/transparent_image.dart'; import 'package:transparent_image/transparent_image.dart';
@@ -23,7 +23,6 @@ class SharingPage extends HookConsumerWidget {
useEffect( useEffect(
() { () {
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
return null; return null;
}, },
[], [],

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
enum _RemoteImageStatus { empty, thumbnail, full } enum _RemoteImageStatus { empty, thumbnail, preview, full }
class _RemotePhotoViewState extends State<RemotePhotoView> { class _RemotePhotoViewState extends State<RemotePhotoView> {
late CachedNetworkImageProvider _imageProvider; late CachedNetworkImageProvider _imageProvider;
@@ -16,13 +16,15 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool allowMoving = _status == _RemoteImageStatus.full; bool allowMoving = _status == _RemoteImageStatus.full;
return PhotoView( return IgnorePointer(
imageProvider: _imageProvider, ignoring: !allowMoving,
minScale: PhotoViewComputedScale.contained, child: PhotoView(
maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained, imageProvider: _imageProvider,
enablePanAlways: true, minScale: PhotoViewComputedScale.contained,
scaleStateChangedCallback: _scaleStateChanged, enablePanAlways: true,
onScaleEnd: _onScaleListener, scaleStateChangedCallback: _scaleStateChanged,
onScaleEnd: _onScaleListener,
),
); );
} }
@@ -32,8 +34,9 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
PhotoViewControllerValue controllerValue, PhotoViewControllerValue controllerValue,
) { ) {
// Disable swipe events when zoomed in // Disable swipe events when zoomed in
if (_zoomedIn) return; if (_zoomedIn) {
return;
}
if (controllerValue.position.dy > swipeThreshold) { if (controllerValue.position.dy > swipeThreshold) {
widget.onSwipeDown(); widget.onSwipeDown();
} else if (controllerValue.position.dy < -swipeThreshold) { } else if (controllerValue.position.dy < -swipeThreshold) {
@@ -42,7 +45,22 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
} }
void _scaleStateChanged(PhotoViewScaleState state) { 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();
}
void _fireStartLoadingEvent() {
if (widget.onLoadingStart != null) widget.onLoadingStart!();
}
void _fireFinishedLoadingEvent() {
if (widget.onLoadingCompleted != null) widget.onLoadingCompleted!();
} }
CachedNetworkImageProvider _authorizedImageProvider(String url) { CachedNetworkImageProvider _authorizedImageProvider(String url) {
@@ -57,14 +75,25 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
_RemoteImageStatus newStatus, _RemoteImageStatus newStatus,
CachedNetworkImageProvider provider, CachedNetworkImageProvider provider,
) { ) {
// Transition to same status is forbidden
if (_status == newStatus) return; if (_status == newStatus) return;
// Transition full -> thumbnail is forbidden
if (_status == _RemoteImageStatus.full && if (_status == _RemoteImageStatus.full &&
newStatus == _RemoteImageStatus.thumbnail) return; newStatus == _RemoteImageStatus.thumbnail) return;
if (_status == _RemoteImageStatus.preview &&
newStatus == _RemoteImageStatus.thumbnail) return;
if (_status == _RemoteImageStatus.full &&
newStatus == _RemoteImageStatus.preview) return;
if (!mounted) return; if (!mounted) return;
if (newStatus != _RemoteImageStatus.full) {
_fireStartLoadingEvent();
} else {
_fireFinishedLoadingEvent();
}
setState(() { setState(() {
_status = newStatus; _status = newStatus;
_imageProvider = provider; _imageProvider = provider;
@@ -85,6 +114,16 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
}), }),
); );
if (widget.previewUrl != null) {
CachedNetworkImageProvider previewProvider =
_authorizedImageProvider(widget.previewUrl!);
previewProvider.resolve(const ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo imageInfo, _) {
_performStateTransition(_RemoteImageStatus.preview, previewProvider);
}),
);
}
CachedNetworkImageProvider fullProvider = CachedNetworkImageProvider fullProvider =
_authorizedImageProvider(widget.imageUrl); _authorizedImageProvider(widget.imageUrl);
fullProvider.resolve(const ImageConfiguration()).addListener( fullProvider.resolve(const ImageConfiguration()).addListener(
@@ -102,21 +141,32 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
} }
class RemotePhotoView extends StatefulWidget { class RemotePhotoView extends StatefulWidget {
const RemotePhotoView({ const RemotePhotoView(
Key? key, {Key? key,
required this.thumbnailUrl, required this.thumbnailUrl,
required this.imageUrl, required this.imageUrl,
required this.authToken, required this.authToken,
required this.onSwipeDown, required this.isZoomedFunction,
required this.onSwipeUp, required this.isZoomedListener,
}) : super(key: key); required this.onSwipeDown,
required this.onSwipeUp,
this.previewUrl,
this.onLoadingCompleted,
this.onLoadingStart})
: super(key: key);
final String thumbnailUrl; final String thumbnailUrl;
final String imageUrl; final String imageUrl;
final String authToken; final String authToken;
final String? previewUrl;
final Function? onLoadingCompleted;
final Function? onLoadingStart;
final void Function() onSwipeDown; final void Function() onSwipeDown;
final void Function() onSwipeUp; final void Function() onSwipeUp;
final void Function() isZoomedFunction;
final ValueNotifier<bool> isZoomedListener;
@override @override
State<StatefulWidget> createState() { State<StatefulWidget> createState() {

View File

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

View File

@@ -0,0 +1,139 @@
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;
static const _threeStageLoading = false;
GalleryViewerPage({
Key? key,
required this.assetList,
required this.asset,
}) : super(key: key);
AssetResponseDto? assetDetail;
@override
Widget build(BuildContext context, WidgetRef ref) {
final Box<dynamic> box = Hive.box(userInfoBox);
int indexOfAsset = assetList.indexOf(asset);
final loading = useState(false);
@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(
loading: loading.value,
asset: assetList[indexOfAsset],
onMoreInfoPressed: () {
showInfo();
},
onDownloadPressed: () {
ref
.watch(imageViewerStateProvider.notifier)
.downloadAsset(assetList[indexOfAsset], context);
}, onSharePressed: () {
ref
.watch(imageViewerStateProvider.notifier)
.shareAsset(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(
authToken: 'Bearer ${box.get(accessTokenKey)}',
isZoomedFunction: isZoomedMethod,
isZoomedListener: isZoomedListener,
onLoadingCompleted: () => loading.value = false,
onLoadingStart: () => loading.value = _threeStageLoading,
asset: assetList[index],
heroTag: assetList[index].id,
threeStageLoading: _threeStageLoading
);
} 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,58 +1,50 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.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/download_loading_indicator.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/home/services/asset.service.dart'; import 'package:immich_mobile/modules/home/services/asset.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
// ignore: must_be_immutable // ignore: must_be_immutable
class ImageViewerPage extends HookConsumerWidget { class ImageViewerPage extends HookConsumerWidget {
final String imageUrl;
final String heroTag; final String heroTag;
final String thumbnailUrl;
final AssetResponseDto asset; final AssetResponseDto asset;
final String authToken;
AssetResponseDto? assetDetail; final ValueNotifier<bool> isZoomedListener;
final void Function() isZoomedFunction;
final void Function() onLoadingCompleted;
final void Function() onLoadingStart;
final bool threeStageLoading;
ImageViewerPage({ ImageViewerPage({
Key? key, Key? key,
required this.imageUrl,
required this.heroTag, required this.heroTag,
required this.thumbnailUrl,
required this.asset, required this.asset,
required this.authToken,
required this.isZoomedFunction,
required this.isZoomedListener,
required this.onLoadingCompleted,
required this.onLoadingStart,
required this.threeStageLoading,
}) : super(key: key); }) : super(key: key);
AssetResponseDto? assetDetail;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final downloadAssetStatus = final downloadAssetStatus =
ref.watch(imageViewerStateProvider).downloadAssetStatus; ref.watch(imageViewerStateProvider).downloadAssetStatus;
var box = Hive.box(userInfoBox);
getAssetExif() async { getAssetExif() async {
assetDetail = assetDetail =
await ref.watch(assetServiceProvider).getAssetById(asset.id); 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( useEffect(
() { () {
getAssetExif(); getAssetExif();
@@ -61,39 +53,43 @@ class ImageViewerPage extends HookConsumerWidget {
[], [],
); );
return Scaffold( showInfo() {
backgroundColor: Colors.black, showModalBottomSheet(
appBar: TopControlAppBar( backgroundColor: Colors.black,
asset: asset, barrierColor: Colors.transparent,
onMoreInfoPressed: showInfo, isScrollControlled: false,
onDownloadPressed: () { context: context,
ref builder: (context) {
.watch(imageViewerStateProvider.notifier) return ExifBottomSheet(assetDetail: assetDetail ?? asset);
.downloadAsset(asset, context);
}, },
), );
body: SafeArea( }
child: Stack(
children: [ return Stack(
Center( children: [
child: Hero( Center(
tag: heroTag, child: Hero(
child: RemotePhotoView( tag: heroTag,
thumbnailUrl: thumbnailUrl, child: RemotePhotoView(
imageUrl: imageUrl, thumbnailUrl: getThumbnailUrl(asset),
authToken: "Bearer ${box.get(accessTokenKey)}", imageUrl: getImageUrl(asset),
onSwipeDown: () => AutoRouter.of(context).pop(), previewUrl: threeStageLoading
onSwipeUp: () => showInfo(), ? getThumbnailUrl(asset, type: ThumbnailFormat.JPEG)
), : null,
), authToken: authToken,
), isZoomedFunction: isZoomedFunction,
if (downloadAssetStatus == DownloadAssetStatus.loading) isZoomedListener: isZoomedListener,
const Center( onSwipeDown: () => AutoRouter.of(context).pop(),
child: DownloadLoadingIndicator(), onSwipeUp: () => showInfo(),
), onLoadingCompleted: onLoadingCompleted,
], onLoadingStart: onLoadingStart),
),
), ),
), 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/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:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
@@ -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/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/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/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:openapi/api.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
@@ -31,66 +25,17 @@ class VideoViewerPage extends HookConsumerWidget {
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey); String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
void showInfo() { return Stack(
showModalBottomSheet( children: [
backgroundColor: Colors.black, VideoThumbnailPlayer(
barrierColor: Colors.transparent, url: videoUrl,
isScrollControlled: false, jwtToken: jwtToken,
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(),
),
],
),
), ),
), if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center(
child: DownloadLoadingIndicator(),
),
],
); );
} }
} }
@@ -134,10 +79,13 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
_createChewieController() { _createChewieController() {
chewieController = ChewieController( chewieController = ChewieController(
showOptions: true, showOptions: true,
showControlsOnInitialize: false, showControlsOnInitialize: true,
videoPlayerController: videoPlayerController, videoPlayerController: videoPlayerController,
autoPlay: true, 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!, controller: chewieController!,
), ),
) )
: const SizedBox( : const Center(
width: 75, child: SizedBox(
height: 75, width: 75,
child: CircularProgressIndicator.adaptive( height: 75,
strokeWidth: 2, child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
), ),
); );
} }

View File

@@ -162,6 +162,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
onlyAll: true, onlyAll: true,
type: RequestType.common, type: RequestType.common,
); );
if (list.isEmpty) {
return;
}
AssetPathEntity albumHasAllAssets = list.first; AssetPathEntity albumHasAllAssets = list.first;
backupAlbumInfoBox.put( 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/constants/hive_box.dart';
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart'; import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart'; import 'package:immich_mobile/modules/backup/models/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/shared/services/api.service.dart';
import 'package:immich_mobile/utils/files_helper.dart'; import 'package:immich_mobile/utils/files_helper.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
@@ -24,6 +25,7 @@ final backupServiceProvider = Provider(
class BackupService { class BackupService {
final ApiService _apiService; final ApiService _apiService;
BackupService(this._apiService); BackupService(this._apiService);
Future<List<String>?> getDeviceBackupAsset() async { Future<List<String>?> getDeviceBackupAsset() async {

View File

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

View File

@@ -1,9 +1,16 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/models/home_page_state.model.dart'; import 'package:immich_mobile/modules/home/models/home_page_state.model.dart';
import 'package:immich_mobile/shared/services/share.service.dart';
import 'package:immich_mobile/shared/ui/share_dialog.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class HomePageStateNotifier extends StateNotifier<HomePageState> { class HomePageStateNotifier extends StateNotifier<HomePageState> {
HomePageStateNotifier()
final ShareService _shareService;
HomePageStateNotifier(this._shareService)
: super( : super(
HomePageState( HomePageState(
isMultiSelectEnable: false, isMultiSelectEnable: false,
@@ -64,9 +71,22 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
state = state.copyWith(selectedItems: currentList); state = state.copyWith(selectedItems: currentList);
} }
void shareAssets(List<AssetResponseDto> assets, BuildContext context) {
showDialog(
context: context,
builder: (BuildContext buildContext) {
_shareService
.shareAssets(assets)
.then((_) => Navigator.of(buildContext).pop());
return const ShareDialog();
},
barrierDismissible: false,
);
}
} }
final homePageStateProvider = final homePageStateProvider =
StateNotifierProvider<HomePageStateNotifier, HomePageState>( StateNotifierProvider<HomePageStateNotifier, HomePageState>(
((ref) => HomePageStateNotifier()), ((ref) => HomePageStateNotifier(ref.watch(shareServiceProvider))),
); );

View File

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

View File

@@ -1,12 +1,16 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart'; import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
class ControlBottomAppBar extends StatelessWidget { import '../../../shared/providers/asset.provider.dart';
import '../providers/home_page_state.provider.dart';
class ControlBottomAppBar extends ConsumerWidget {
const ControlBottomAppBar({Key? key}) : super(key: key); const ControlBottomAppBar({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
return Positioned( return Positioned(
bottom: 0, bottom: 0,
left: 0, left: 0,
@@ -25,7 +29,7 @@ class ControlBottomAppBar extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20), padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
ControlBoxButton( ControlBoxButton(
iconData: Icons.delete_forever_rounded, iconData: Icons.delete_forever_rounded,
@@ -39,6 +43,20 @@ class ControlBottomAppBar extends StatelessWidget {
); );
}, },
), ),
ControlBoxButton(
iconData: Icons.share,
label: "control_bottom_app_bar_share".tr(),
onPressed: () {
final homePageState = ref.watch(homePageStateProvider);
ref.watch(homePageStateProvider.notifier).shareAssets(
homePageState.selectedItems.toList(),
context,
);
ref
.watch(homePageStateProvider.notifier)
.disableMultiSelect();
},
),
], ],
), ),
) )
@@ -67,7 +85,7 @@ class ControlBoxButton extends StatelessWidget {
width: 60, width: 60,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
IconButton( IconButton(
onPressed: () { onPressed: () {

View File

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

View File

@@ -9,20 +9,22 @@ import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart'; import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';
class ThumbnailImage extends HookConsumerWidget { class ThumbnailImage extends HookConsumerWidget {
final AssetResponseDto asset; 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final cacheKey = useState(1); final cacheKey = useState(1);
var box = Hive.box(userInfoBox); var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = var thumbnailRequestUrl = getThumbnailUrl(asset);
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
var selectedAsset = ref.watch(homePageStateProvider).selectedItems; var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
var isMultiSelectEnable = var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable; ref.watch(homePageStateProvider).isMultiSelectEnable;
@@ -60,29 +62,16 @@ class ThumbnailImage extends HookConsumerWidget {
.watch(homePageStateProvider.notifier) .watch(homePageStateProvider.notifier)
.addSingleSelectedItem(asset); .addSingleSelectedItem(asset);
} else { } else {
if (asset.type == AssetTypeEnum.IMAGE) { AutoRouter.of(context).push(
AutoRouter.of(context).push( GalleryViewerRoute(
ImageViewerRoute( assetList: assetList,
imageUrl: asset: asset,
'${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,
),
);
}
} }
}, },
onLongPress: () { onLongPress: () {
// Enable multi selecte function // Enable multi select function
ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset}); ref.watch(homePageStateProvider.notifier).enableMultiSelect({asset});
HapticFeedback.heavyImpact(); 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/immich_sliver_appbar.dart';
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart'; import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
import 'package:immich_mobile/modules/home/ui/profile_drawer.dart'; import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:openapi/api.dart';
class HomePage extends HookConsumerWidget { class HomePage extends HookConsumerWidget {
const HomePage({Key? key}) : super(key: key); const HomePage({Key? key}) : super(key: key);
@@ -25,6 +27,13 @@ class HomePage extends HookConsumerWidget {
var isMultiSelectEnable = var isMultiSelectEnable =
ref.watch(homePageStateProvider).isMultiSelectEnable; ref.watch(homePageStateProvider).isMultiSelectEnable;
var homePageState = ref.watch(homePageStateProvider); 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( useEffect(
() { () {
@@ -67,13 +76,17 @@ class HomePage extends HookConsumerWidget {
imageGridGroup.add( imageGridGroup.add(
DailyTitleText( DailyTitleText(
key: Key('${dateGroup.toString()}title'),
isoDate: dateGroup, isoDate: dateGroup,
assetGroup: immichAssetList, assetGroup: immichAssetList,
), ),
); );
imageGridGroup.add( imageGridGroup.add(
ImageGrid(assetGroup: immichAssetList), ImageGrid(
assetGroup: immichAssetList,
sortedAssetGroup: sortedAssetList,
),
); );
lastMonth = currentMonth; 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/authentication_state.model.dart';
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart'; import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
import 'package:immich_mobile/modules/backup/services/backup.service.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/api.service.dart';
import 'package:immich_mobile/shared/services/device_info.service.dart'; import 'package:immich_mobile/shared/services/device_info.service.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';

View File

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

View File

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

View File

@@ -41,16 +41,27 @@ class _$AppRouter extends RootStackRouter {
opaque: true, opaque: true,
barrierDismissible: false); 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));
},
ImageViewerRoute.name: (routeData) { ImageViewerRoute.name: (routeData) {
final args = routeData.argsAs<ImageViewerRouteArgs>(); final args = routeData.argsAs<ImageViewerRouteArgs>();
return MaterialPageX<dynamic>( return MaterialPageX<dynamic>(
routeData: routeData, routeData: routeData,
child: ImageViewerPage( child: ImageViewerPage(
key: args.key, key: args.key,
imageUrl: args.imageUrl,
heroTag: args.heroTag, heroTag: args.heroTag,
thumbnailUrl: args.thumbnailUrl, asset: args.asset,
asset: args.asset)); authToken: args.authToken,
isZoomedFunction: args.isZoomedFunction,
isZoomedListener: args.isZoomedListener,
onLoadingCompleted: args.onLoadingCompleted,
onLoadingStart: args.onLoadingStart,
threeStageLoading: args.threeStageLoading));
}, },
VideoViewerRoute.name: (routeData) { VideoViewerRoute.name: (routeData) {
final args = routeData.argsAs<VideoViewerRouteArgs>(); final args = routeData.argsAs<VideoViewerRouteArgs>();
@@ -69,9 +80,12 @@ class _$AppRouter extends RootStackRouter {
routeData: routeData, routeData: routeData,
child: SearchResultPage(key: args.key, searchTerm: args.searchTerm)); child: SearchResultPage(key: args.key, searchTerm: args.searchTerm));
}, },
CreateSharedAlbumRoute.name: (routeData) { CreateAlbumRoute.name: (routeData) {
final args = routeData.argsAs<CreateAlbumRouteArgs>();
return MaterialPageX<dynamic>( return MaterialPageX<dynamic>(
routeData: routeData, child: const CreateSharedAlbumPage()); routeData: routeData,
child: CreateAlbumPage(
key: args.key, isSharedAlbum: args.isSharedAlbum));
}, },
AssetSelectionRoute.name: (routeData) { AssetSelectionRoute.name: (routeData) {
return CustomPage<AssetSelectionPageResult?>( return CustomPage<AssetSelectionPageResult?>(
@@ -136,6 +150,10 @@ class _$AppRouter extends RootStackRouter {
SharingRoute.name: (routeData) { SharingRoute.name: (routeData) {
return MaterialPageX<dynamic>( return MaterialPageX<dynamic>(
routeData: routeData, child: const SharingPage()); routeData: routeData, child: const SharingPage());
},
LibraryRoute.name: (routeData) {
return MaterialPageX<dynamic>(
routeData: routeData, child: const LibraryPage());
} }
}; };
@@ -161,8 +179,14 @@ class _$AppRouter extends RootStackRouter {
RouteConfig(SharingRoute.name, RouteConfig(SharingRoute.name,
path: 'sharing-page', path: 'sharing-page',
parent: TabControllerRoute.name, parent: TabControllerRoute.name,
guards: [authGuard]),
RouteConfig(LibraryRoute.name,
path: 'library-page',
parent: TabControllerRoute.name,
guards: [authGuard]) guards: [authGuard])
]), ]),
RouteConfig(GalleryViewerRoute.name,
path: '/gallery-viewer-page', guards: [authGuard]),
RouteConfig(ImageViewerRoute.name, RouteConfig(ImageViewerRoute.name,
path: '/image-viewer-page', guards: [authGuard]), path: '/image-viewer-page', guards: [authGuard]),
RouteConfig(VideoViewerRoute.name, RouteConfig(VideoViewerRoute.name,
@@ -171,8 +195,8 @@ class _$AppRouter extends RootStackRouter {
path: '/backup-controller-page', guards: [authGuard]), path: '/backup-controller-page', guards: [authGuard]),
RouteConfig(SearchResultRoute.name, RouteConfig(SearchResultRoute.name,
path: '/search-result-page', guards: [authGuard]), path: '/search-result-page', guards: [authGuard]),
RouteConfig(CreateSharedAlbumRoute.name, RouteConfig(CreateAlbumRoute.name,
path: '/create-shared-album-page', guards: [authGuard]), path: '/create-album-page', guards: [authGuard]),
RouteConfig(AssetSelectionRoute.name, RouteConfig(AssetSelectionRoute.name,
path: '/asset-selection-page', guards: [authGuard]), path: '/asset-selection-page', guards: [authGuard]),
RouteConfig(SelectUserForSharingRoute.name, RouteConfig(SelectUserForSharingRoute.name,
@@ -226,23 +250,62 @@ class TabControllerRoute extends PageRouteInfo<void> {
static const String name = 'TabControllerRoute'; static const String name = 'TabControllerRoute';
} }
/// generated route for
/// [GalleryViewerPage]
class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
GalleryViewerRoute(
{Key? key,
required List<AssetResponseDto> assetList,
required AssetResponseDto asset})
: super(GalleryViewerRoute.name,
path: '/gallery-viewer-page',
args: GalleryViewerRouteArgs(
key: key, assetList: assetList, asset: asset));
static const String name = 'GalleryViewerRoute';
}
class GalleryViewerRouteArgs {
const GalleryViewerRouteArgs(
{this.key, required this.assetList, required this.asset});
final Key? key;
final List<AssetResponseDto> assetList;
final AssetResponseDto asset;
@override
String toString() {
return 'GalleryViewerRouteArgs{key: $key, assetList: $assetList, asset: $asset}';
}
}
/// generated route for /// generated route for
/// [ImageViewerPage] /// [ImageViewerPage]
class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> { class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
ImageViewerRoute( ImageViewerRoute(
{Key? key, {Key? key,
required String imageUrl,
required String heroTag, required String heroTag,
required String thumbnailUrl, required AssetResponseDto asset,
required AssetResponseDto asset}) required String authToken,
required void Function() isZoomedFunction,
required ValueNotifier<bool> isZoomedListener,
required void Function() onLoadingCompleted,
required void Function() onLoadingStart,
required bool threeStageLoading})
: super(ImageViewerRoute.name, : super(ImageViewerRoute.name,
path: '/image-viewer-page', path: '/image-viewer-page',
args: ImageViewerRouteArgs( args: ImageViewerRouteArgs(
key: key, key: key,
imageUrl: imageUrl,
heroTag: heroTag, heroTag: heroTag,
thumbnailUrl: thumbnailUrl, asset: asset,
asset: asset)); authToken: authToken,
isZoomedFunction: isZoomedFunction,
isZoomedListener: isZoomedListener,
onLoadingCompleted: onLoadingCompleted,
onLoadingStart: onLoadingStart,
threeStageLoading: threeStageLoading));
static const String name = 'ImageViewerRoute'; static const String name = 'ImageViewerRoute';
} }
@@ -250,24 +313,36 @@ class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
class ImageViewerRouteArgs { class ImageViewerRouteArgs {
const ImageViewerRouteArgs( const ImageViewerRouteArgs(
{this.key, {this.key,
required this.imageUrl,
required this.heroTag, required this.heroTag,
required this.thumbnailUrl, required this.asset,
required this.asset}); required this.authToken,
required this.isZoomedFunction,
required this.isZoomedListener,
required this.onLoadingCompleted,
required this.onLoadingStart,
required this.threeStageLoading});
final Key? key; final Key? key;
final String imageUrl;
final String heroTag; final String heroTag;
final String thumbnailUrl;
final AssetResponseDto asset; final AssetResponseDto asset;
final String authToken;
final void Function() isZoomedFunction;
final ValueNotifier<bool> isZoomedListener;
final void Function() onLoadingCompleted;
final void Function() onLoadingStart;
final bool threeStageLoading;
@override @override
String toString() { String toString() {
return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl, asset: $asset}'; return 'ImageViewerRouteArgs{key: $key, heroTag: $heroTag, asset: $asset, authToken: $authToken, isZoomedFunction: $isZoomedFunction, isZoomedListener: $isZoomedListener, onLoadingCompleted: $onLoadingCompleted, onLoadingStart: $onLoadingStart, threeStageLoading: $threeStageLoading}';
} }
} }
@@ -334,12 +409,27 @@ class SearchResultRouteArgs {
} }
/// generated route for /// generated route for
/// [CreateSharedAlbumPage] /// [CreateAlbumPage]
class CreateSharedAlbumRoute extends PageRouteInfo<void> { class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> {
const CreateSharedAlbumRoute() CreateAlbumRoute({Key? key, required bool isSharedAlbum})
: super(CreateSharedAlbumRoute.name, path: '/create-shared-album-page'); : 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 /// generated route for
@@ -492,3 +582,11 @@ class SharingRoute extends PageRouteInfo<void> {
static const String name = 'SharingRoute'; 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:auto_route/auto_route.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/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'; import 'package:immich_mobile/shared/providers/server_info.provider.dart';
class TabNavigationObserver extends AutoRouterObserver { class TabNavigationObserver extends AutoRouterObserver {
@@ -37,6 +38,9 @@ class TabNavigationObserver extends AutoRouterObserver {
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
} }
if (route.name == 'LibraryRoute') {
ref.read(albumProvider.notifier).getAllAlbums();
}
ref.watch(serverInfoProvider.notifier).getServerVersion(); 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'; import 'package:openapi/api.dart';
final apiServiceProvider = Provider((ref) => ApiService());
class ApiService { class ApiService {
late ApiClient _apiClient; late ApiClient _apiClient;
@@ -15,7 +12,6 @@ class ApiService {
setEndpoint(String endpoint) { setEndpoint(String endpoint) {
_apiClient = ApiClient(basePath: endpoint); _apiClient = ApiClient(basePath: endpoint);
userApi = UserApi(_apiClient); userApi = UserApi(_apiClient);
authenticationApi = AuthenticationApi(_apiClient); authenticationApi = AuthenticationApi(_apiClient);
albumApi = AlbumApi(_apiClient); albumApi = AlbumApi(_apiClient);

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import 'package:image_picker/image_picker.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/shared/services/api.service.dart';
import 'package:immich_mobile/utils/files_helper.dart'; import 'package:immich_mobile/utils/files_helper.dart';
import 'package:openapi/api.dart'; import 'package:openapi/api.dart';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

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