Compare commits
55 Commits
v1.13.0_20
...
v1.18.0_27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72c334e5e0 | ||
|
|
e7f35822af | ||
|
|
bd2152d568 | ||
|
|
b1d7ef03e2 | ||
|
|
aa74417d11 | ||
|
|
229b009b7f | ||
|
|
bece6253d5 | ||
|
|
ae7e582ec8 | ||
|
|
d69470e207 | ||
|
|
c60e852226 | ||
|
|
a205478a29 | ||
|
|
22d30522e1 | ||
|
|
19b1fad274 | ||
|
|
9a6dfacf9b | ||
|
|
7f236c5b18 | ||
|
|
25985c732d | ||
|
|
9ce50b7e3d | ||
|
|
f5e93a8179 | ||
|
|
2b5cef156c | ||
|
|
f3032f74a4 | ||
|
|
58ec7553ea | ||
|
|
357f7d1c31 | ||
|
|
e6d30d72fa | ||
|
|
355038a91a | ||
|
|
97d9b80baa | ||
|
|
b6814fad57 | ||
|
|
7586c65103 | ||
|
|
633170d743 | ||
|
|
c5be7827c3 | ||
|
|
e84c705e31 | ||
|
|
36162509e0 | ||
|
|
76bf1c0379 | ||
|
|
32b847c26e | ||
|
|
a45d6fdf57 | ||
|
|
c071e64a7e | ||
|
|
663f12851e | ||
|
|
c4ef523564 | ||
|
|
992f792c0a | ||
|
|
97611fa057 | ||
|
|
32240777c3 | ||
|
|
6065ff8caa | ||
|
|
8db073941d | ||
|
|
5e281b44e9 | ||
|
|
142ede350e | ||
|
|
a2e1d4caa2 | ||
|
|
5f00d8b9c6 | ||
|
|
2e85e18020 | ||
|
|
40a8115101 | ||
|
|
d02b97e1c1 | ||
|
|
485b152beb | ||
|
|
c918f5b001 | ||
|
|
cca2f7d178 | ||
|
|
baf533de35 | ||
|
|
dfc0d6eee7 | ||
|
|
7948cb8110 |
27
.github/workflows/build_push_docker_latest.yml
vendored
27
.github/workflows/build_push_docker_latest.yml
vendored
@@ -91,3 +91,30 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-web:latest
|
altran1502/immich-web:latest
|
||||||
|
|
||||||
|
build_and_push_nginx_latest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and Push Proxy
|
||||||
|
uses: docker/build-push-action@v3.0.0
|
||||||
|
with:
|
||||||
|
context: ./nginx
|
||||||
|
file: ./nginx/Dockerfile
|
||||||
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
altran1502/immich-proxy:latest
|
||||||
|
|||||||
27
.github/workflows/build_push_docker_staging.yml
vendored
27
.github/workflows/build_push_docker_staging.yml
vendored
@@ -93,3 +93,30 @@ jobs:
|
|||||||
push: ${{ github.event_name == 'pull_request' }}
|
push: ${{ github.event_name == 'pull_request' }}
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-web:staging
|
altran1502/immich-web:staging
|
||||||
|
|
||||||
|
build_and_push_nginx_staging:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build and Push Proxy
|
||||||
|
uses: docker/build-push-action@v3.0.0
|
||||||
|
with:
|
||||||
|
context: ./nginx
|
||||||
|
file: ./nginx/Dockerfile
|
||||||
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
|
push: ${{ github.event_name == 'pull_request' }}
|
||||||
|
tags: |
|
||||||
|
altran1502/immich-proxy:staging
|
||||||
42
.github/workflows/build_push_server_release.yml
vendored
42
.github/workflows/build_push_server_release.yml
vendored
@@ -43,6 +43,7 @@ jobs:
|
|||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-server:${{ steps.previoustag.outputs.tag }}
|
altran1502/immich-server:${{ steps.previoustag.outputs.tag }}
|
||||||
|
altran1502/immich-server:release
|
||||||
|
|
||||||
build_and_push_machine_learning_release:
|
build_and_push_machine_learning_release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -75,6 +76,7 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-machine-learning:${{ steps.previoustag.outputs.tag }}
|
altran1502/immich-machine-learning:${{ steps.previoustag.outputs.tag }}
|
||||||
|
altran1502/immich-machine-learning:release
|
||||||
|
|
||||||
build_and_push_web_release:
|
build_and_push_web_release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -114,3 +116,43 @@ jobs:
|
|||||||
target: prod
|
target: prod
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-web:${{ steps.previoustag.outputs.tag }}
|
altran1502/immich-web:${{ steps.previoustag.outputs.tag }}
|
||||||
|
altran1502/immich-web:release
|
||||||
|
|
||||||
|
build_and_push_nginx_release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
ref: "main"
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: "Get Previous tag"
|
||||||
|
id: previoustag
|
||||||
|
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||||
|
with:
|
||||||
|
fallback: latest
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push immich-proxy release
|
||||||
|
uses: docker/build-push-action@v3.0.0
|
||||||
|
with:
|
||||||
|
context: ./nginx
|
||||||
|
file: ./nginx/Dockerfile
|
||||||
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: |
|
||||||
|
altran1502/immich-proxy:release
|
||||||
|
altran1502/immich-proxy:${{ steps.previoustag.outputs.tag }}
|
||||||
|
|||||||
8
Makefile
8
Makefile
@@ -1,11 +1,11 @@
|
|||||||
dev:
|
dev:
|
||||||
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-update:
|
dev-update:
|
||||||
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
|
||||||
|
|
||||||
dev-scale:
|
dev-scale:
|
||||||
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
rm -rf ./server/dist && docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
||||||
|
|
||||||
stage:
|
stage:
|
||||||
docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
|
docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
|
||||||
@@ -17,4 +17,4 @@ prod:
|
|||||||
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
|
docker-compose -f ./docker/docker-compose.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
prod-scale:
|
prod-scale:
|
||||||
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich-server=5 --scale immich-microservices=3 --remove-orphans
|
docker-compose -f ./docker/docker-compose.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||||
25
README.md
25
README.md
@@ -10,7 +10,7 @@
|
|||||||
<a href="https://actions-badge.atrox.dev/alextran1502/immich/goto?ref=main">
|
<a href="https://actions-badge.atrox.dev/alextran1502/immich/goto?ref=main">
|
||||||
<img alt="Build Status" src="https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Falextran1502%2Fimmich%2Fbadge%3Fref%3Dmain&style=for-the-badge&label=Github Action&logo=github&labelColor=ececec&logoColor=000000" />
|
<img alt="Build Status" src="https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Falextran1502%2Fimmich%2Fbadge%3Fref%3Dmain&style=for-the-badge&label=Github Action&logo=github&labelColor=ececec&logoColor=000000" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://discord.gg/rxnyVTXGbM">
|
<a href="https://discord.gg/D8JsnBEuKb">
|
||||||
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Immich%20Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Immich Discord"/>
|
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Immich%20Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Immich Discord"/>
|
||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
@@ -155,19 +155,13 @@ To **start**, run
|
|||||||
docker-compose -f ./docker/docker-compose.yml up
|
docker-compose -f ./docker/docker-compose.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
If you have a few thousand photos/videos, I suggest running docker-compose with *scaling* option for the `immich_server` container to handle high I/O load when using fast scrolling.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose -f ./docker/docker-compose.yml up --scale immich-server=5
|
|
||||||
```
|
|
||||||
|
|
||||||
To *update* docker-compose with newest image (if you have started the docker-compose previously)
|
To *update* docker-compose with newest image (if you have started the docker-compose previously)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up
|
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` through `Nginx`
|
The server will be running at `http://your-ip:2283/api`
|
||||||
|
|
||||||
## Step 3: Register User
|
## Step 3: Register User
|
||||||
|
|
||||||
@@ -185,7 +179,11 @@ Additional accounts on the server can be created by the admin account.
|
|||||||
|
|
||||||
## Step 4: Run mobile app
|
## Step 4: Run mobile app
|
||||||
|
|
||||||
The app is distributed on several platforms below.
|
Login the mobile app with your server address
|
||||||
|
|
||||||
|
<p align="left">
|
||||||
|
<img src="design/login-screen.jpeg" width="250" title="Example login screen">
|
||||||
|
<p/>
|
||||||
|
|
||||||
## 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.
|
||||||
@@ -227,6 +225,15 @@ make dev # required Makefile installed on the system.
|
|||||||
|
|
||||||
All servers and web container are hot reload for quick feedback loop.
|
All servers and web container are hot reload for quick feedback loop.
|
||||||
|
|
||||||
|
## Note for developers
|
||||||
|
### 1 - OpenAPI
|
||||||
|
OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generator-cli` can be installed [here](https://openapi-generator.tech/docs/installation/). When you add a new or modify an existing endpoint, you must run the generate command below to update the client SDK.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run api:generate # Run from server directory
|
||||||
|
```
|
||||||
|
You can find the generated client SDK in the [`web/src/api`](web/src/api) for Typescript SDK and [`mobile/openapi`](mobile/openapi) for Dart SDK.
|
||||||
|
|
||||||
# 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 [**Github Sponsor**](https://github.com/sponsors/alextran1502), or a one time donation with the Buy Me a coffee link below.
|
||||||
|
|||||||
BIN
design/login-screen.jpeg
Normal file
BIN
design/login-screen.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
@@ -7,6 +7,8 @@ DB_USERNAME=postgres
|
|||||||
DB_PASSWORD=postgres
|
DB_PASSWORD=postgres
|
||||||
DB_DATABASE_NAME=immich
|
DB_DATABASE_NAME=immich
|
||||||
|
|
||||||
|
# Optional Database settings:
|
||||||
|
# DB_PORT=5432
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -17,6 +19,12 @@ DB_DATABASE_NAME=immich
|
|||||||
|
|
||||||
REDIS_HOSTNAME=immich_redis
|
REDIS_HOSTNAME=immich_redis
|
||||||
|
|
||||||
|
# Optional Redis settings:
|
||||||
|
# REDIS_PORT=6379
|
||||||
|
# REDIS_DBINDEX=0
|
||||||
|
# REDIS_PASSWORD=
|
||||||
|
# REDIS_SOCKET=
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -51,7 +59,7 @@ MAPBOX_KEY=
|
|||||||
|
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
# WEB
|
# WEB - Required
|
||||||
###################################################################################
|
###################################################################################
|
||||||
|
|
||||||
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
||||||
@@ -61,3 +69,13 @@ MAPBOX_KEY=
|
|||||||
# !CAUTION! THERE IS NO FORWARD SLASH AT THE END
|
# !CAUTION! THERE IS NO FORWARD SLASH AT THE END
|
||||||
|
|
||||||
VITE_SERVER_ENDPOINT=
|
VITE_SERVER_ENDPOINT=
|
||||||
|
|
||||||
|
|
||||||
|
####################################################################################
|
||||||
|
# WEB - Optional
|
||||||
|
####################################################################################
|
||||||
|
|
||||||
|
# Custom message on the login page, should be written in HTML form.
|
||||||
|
# For example VITE_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
|
||||||
|
|
||||||
|
VITE_LOGIN_PAGE_MESSAGE=
|
||||||
@@ -7,8 +7,6 @@ services:
|
|||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
command: npm run start:dev immich
|
command: npm run start:dev immich
|
||||||
expose:
|
|
||||||
- "3000"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ../server:/usr/src/app
|
- ../server:/usr/src/app
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
@@ -27,8 +25,6 @@ services:
|
|||||||
context: ../machine-learning
|
context: ../machine-learning
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
command: npm run start:dev
|
command: npm run start:dev
|
||||||
expose:
|
|
||||||
- "3001"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ../machine-learning:/usr/src/app
|
- ../machine-learning:/usr/src/app
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
@@ -68,7 +64,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- 3002:3000
|
- 3000:3000
|
||||||
- 24678:24678
|
- 24678:24678
|
||||||
volumes:
|
volumes:
|
||||||
- ../web:/usr/src/app
|
- ../web:/usr/src/app
|
||||||
@@ -94,11 +90,12 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
nginx:
|
immich-proxy:
|
||||||
container_name: proxy_nginx
|
container_name: immich_proxy
|
||||||
image: nginx:latest
|
image: immich-proxy-dev:latest
|
||||||
volumes:
|
build:
|
||||||
- ./settings/nginx-conf:/etc/nginx/conf.d
|
context: ../nginx
|
||||||
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- 2283:80
|
- 2283:80
|
||||||
- 2284:443
|
- 2284:443
|
||||||
@@ -106,6 +103,7 @@ services:
|
|||||||
driver: none
|
driver: none
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich-server
|
- immich-server
|
||||||
|
restart: always
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|||||||
@@ -68,11 +68,9 @@ services:
|
|||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
nginx:
|
immich-proxy:
|
||||||
container_name: proxy_nginx
|
container_name: immich_proxy
|
||||||
image: nginx:latest
|
image: altran1502/immich-proxy:staging
|
||||||
volumes:
|
|
||||||
- ./settings/nginx-conf:/etc/nginx/conf.d
|
|
||||||
ports:
|
ports:
|
||||||
- 2283:80
|
- 2283:80
|
||||||
- 2284:443
|
- 2284:443
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ version: "3.8"
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
image: altran1502/immich-server:latest
|
image: altran1502/immich-server:release
|
||||||
entrypoint: ["/bin/sh", "./start-server.sh"]
|
entrypoint: ["/bin/sh", "./start-server.sh"]
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
@@ -16,7 +16,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
image: altran1502/immich-server:latest
|
image: altran1502/immich-server:release
|
||||||
entrypoint: ["/bin/sh", "./start-microservices.sh"]
|
entrypoint: ["/bin/sh", "./start-microservices.sh"]
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
@@ -30,7 +30,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
image: altran1502/immich-machine-learning:latest
|
image: altran1502/immich-machine-learning:release
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
@@ -43,7 +43,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
immich-web:
|
immich-web:
|
||||||
image: altran1502/immich-web:latest
|
image: altran1502/immich-web:release
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
@@ -68,14 +68,11 @@ services:
|
|||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
nginx:
|
immich-proxy:
|
||||||
container_name: proxy_nginx
|
container_name: immich_proxy
|
||||||
image: nginx:latest
|
image: altran1502/immich-proxy:release
|
||||||
volumes:
|
|
||||||
- ./settings/nginx-conf:/etc/nginx/conf.d
|
|
||||||
ports:
|
ports:
|
||||||
- 2283:80
|
- 2283:80
|
||||||
- 2284:443
|
|
||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
15
localizely.yml
Normal file
15
localizely.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
config_version: 1.0
|
||||||
|
project_id: ead34689-ec52-41d9-b675-09bc85a6cbd7
|
||||||
|
file_type: json
|
||||||
|
upload:
|
||||||
|
files:
|
||||||
|
- file: mobile/assets/i18n/en-US.json
|
||||||
|
locale_code: en
|
||||||
|
- file: mobile/assets/i18n/de-DE.json
|
||||||
|
locale_code: de
|
||||||
|
download:
|
||||||
|
files:
|
||||||
|
- file: mobile/assets/i18n/en-US.json
|
||||||
|
locale_code: en
|
||||||
|
- file: mobile/assets/i18n/de-DE.json
|
||||||
|
locale_code: de
|
||||||
@@ -9,7 +9,7 @@ COPY package.json package-lock.json ./
|
|||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
|
RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y
|
||||||
|
|
||||||
RUN npm install
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
|||||||
export const databaseConfig: TypeOrmModuleOptions = {
|
export const databaseConfig: TypeOrmModuleOptions = {
|
||||||
type: 'postgres',
|
type: 'postgres',
|
||||||
host: process.env.DB_HOSTNAME || 'immich_postgres',
|
host: process.env.DB_HOSTNAME || 'immich_postgres',
|
||||||
port: 5432,
|
port: parseInt(process.env.DB_PORT || '5432'),
|
||||||
username: process.env.DB_USERNAME,
|
username: process.env.DB_USERNAME,
|
||||||
password: process.env.DB_PASSWORD,
|
password: process.env.DB_PASSWORD,
|
||||||
database: process.env.DB_DATABASE_NAME,
|
database: process.env.DB_DATABASE_NAME,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Logger } from '@nestjs/common';
|
|||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
await app.listen(3001, () => {
|
await app.listen(3003, () => {
|
||||||
if (process.env.NODE_ENV == 'development') {
|
if (process.env.NODE_ENV == 'development') {
|
||||||
Logger.log(
|
Logger.log(
|
||||||
'Running Immich Machine Learning in DEVELOPMENT environment',
|
'Running Immich Machine Learning in DEVELOPMENT environment',
|
||||||
|
|||||||
@@ -21,10 +21,18 @@ linter:
|
|||||||
# or a specific dart file by using the `// ignore: name_of_lint` and
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
# producing the lint.
|
# producing the lint.
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
use_build_context_synchronously: false
|
use_build_context_synchronously: false
|
||||||
|
require_trailing_commas: true
|
||||||
|
unrelated_type_equality_checks: true
|
||||||
|
|
||||||
# Additional information about this file can be found at
|
# Additional information about this file can be found at
|
||||||
# https://dart.dev/guides/language/analysis-options
|
# https://dart.dev/guides/language/analysis-options
|
||||||
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- openapi/
|
||||||
|
- openapi/test/
|
||||||
|
- lib/generated_plugin_registrant.dart
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
* Fixed app does not resume back up when reopening a closed app
|
||||||
|
* Fixed wrong asset count on the upload page
|
||||||
|
* Added mechanism to change the password of new user on the first login (except Admin)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* Fixed admin is forced to change password upon logging in on mobile app
|
||||||
|
* Fixed change password form validation
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Removed thumbnail generation on mobile - the operation now will be on the server to reduce CPU load and battery usage.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Hot fix: Restore shared album functionality
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Add information for uploading asset and error indication with error message for each failed upload.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Refactored app to use OpenAPI SDK to improve performance and project structure.
|
||||||
103
mobile/assets/i18n/de-DE.json
Normal file
103
mobile/assets/i18n/de-DE.json
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"album_info_card_backup_album_excluded": "AUSGESCHLOSSEN",
|
||||||
|
"album_info_card_backup_album_included": "EINGESCHLOSSEN",
|
||||||
|
"album_viewer_appbar_share_delete": "Album löschen",
|
||||||
|
"album_viewer_appbar_share_err_delete": "Album konnte nicht gelöscht werden",
|
||||||
|
"album_viewer_appbar_share_err_leave": "Album konnte nicht verlassen werden",
|
||||||
|
"album_viewer_appbar_share_err_remove": "Beim Löschen von Elementen aus dem Album ist ein Problem aufgetreten",
|
||||||
|
"album_viewer_appbar_share_err_title": "Der Titel konnte nicht geändert werden",
|
||||||
|
"album_viewer_appbar_share_leave": "Album verlassen",
|
||||||
|
"album_viewer_appbar_share_remove": "Entferne vom Album",
|
||||||
|
"album_viewer_page_share_add_users": "Nutzer hinzufügen",
|
||||||
|
"backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})",
|
||||||
|
"backup_album_selection_page_albums_tap": "Tippen um einzuschließen, doppelt tippen um zu entfernen",
|
||||||
|
"backup_album_selection_page_assets_scatter": "Elemente können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden",
|
||||||
|
"backup_album_selection_page_select_albums": "Alben auswählen",
|
||||||
|
"backup_album_selection_page_selection_info": "Auswahl",
|
||||||
|
"backup_album_selection_page_total_assets": "Elemente",
|
||||||
|
"backup_all": "Alle",
|
||||||
|
"backup_controller_page_albums": "Gesicherte Alben",
|
||||||
|
"backup_controller_page_backup": "Sicherung",
|
||||||
|
"backup_controller_page_backup_selected": "Ausgewählt: ",
|
||||||
|
"backup_controller_page_backup_sub": "Gesicherte Fotos und Videos",
|
||||||
|
"backup_controller_page_cancel": "Abbrechen",
|
||||||
|
"backup_controller_page_created": "Erstellt: {}",
|
||||||
|
"backup_controller_page_desc_backup": "Aktiviere die Sicherung um Elemente automatisch auf den Server zu laden.",
|
||||||
|
"backup_controller_page_excluded": "Ausgeschlossen: ",
|
||||||
|
"backup_controller_page_failed": "Fehlgeschlagen ({})",
|
||||||
|
"backup_controller_page_filename": "Dateiname: {} [{}]",
|
||||||
|
"backup_controller_page_id": "ID: {}",
|
||||||
|
"backup_controller_page_info": "Informationen zur Sicherung",
|
||||||
|
"backup_controller_page_none_selected": "Keine ausgewählt",
|
||||||
|
"backup_controller_page_remainder": "Übrig",
|
||||||
|
"backup_controller_page_remainder_sub": "Noch zu sichernde Fotos und Videos",
|
||||||
|
"backup_controller_page_select": "Auswählen",
|
||||||
|
"backup_controller_page_server_storage": "Server Speicher",
|
||||||
|
"backup_controller_page_start_backup": "Sicherung starten",
|
||||||
|
"backup_controller_page_status_off": "Sicherung ist inaktiv",
|
||||||
|
"backup_controller_page_status_on": "Sicherung ist aktiv",
|
||||||
|
"backup_controller_page_storage_format": "{} von {} genutzt",
|
||||||
|
"backup_controller_page_to_backup": "Zu sichernde Alben",
|
||||||
|
"backup_controller_page_total": "Gesamt",
|
||||||
|
"backup_controller_page_total_sub": "Alle Fotos und Videos",
|
||||||
|
"backup_controller_page_turn_off": "Sicherung ausschalten",
|
||||||
|
"backup_controller_page_turn_on": "Sicherung einschalten",
|
||||||
|
"backup_controller_page_uploading_file_info": "Informationen",
|
||||||
|
"backup_err_only_album": "Das einzige Album kann nicht entfernt werden",
|
||||||
|
"backup_info_card_assets": "Elemente",
|
||||||
|
"control_bottom_app_bar_delete": "Löschen",
|
||||||
|
"create_shared_album_page_share": "Teilen",
|
||||||
|
"create_shared_album_page_share_add_assets": "ELEMENTE HINZUFÜGEN",
|
||||||
|
"create_shared_album_page_share_select_photos": "Fotos auswählen",
|
||||||
|
"daily_title_text_date": "E, dd MMM",
|
||||||
|
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
||||||
|
"date_format": "E d. LLL y • hh:mm",
|
||||||
|
"delete_dialog_alert": "Diese Elemente werden unwiderruflich von Immich und dem Gerät entfernt",
|
||||||
|
"delete_dialog_cancel": "Abbrechen",
|
||||||
|
"delete_dialog_ok": "Löschen",
|
||||||
|
"delete_dialog_title": "Für immer löschen",
|
||||||
|
"exif_bottom_sheet_description": "Beschreibung hinzufügen...",
|
||||||
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
|
"exif_bottom_sheet_location": "STANDORT",
|
||||||
|
"login_form_button_text": "Anmelden",
|
||||||
|
"login_form_email_hint": "deine@email.de",
|
||||||
|
"login_form_endpoint_hint": "http://deine-server-ip:port/api",
|
||||||
|
"login_form_endpoint_url": "Server URL",
|
||||||
|
"login_form_err_http": "Bitte gebe http:// oder https:// an",
|
||||||
|
"login_form_err_invalid_email": "Ungültige E-Mail",
|
||||||
|
"login_form_err_leading_whitespace": "Führendes Leerzichen",
|
||||||
|
"login_form_err_trailing_whitespace": "Folgendes Leerzeichen",
|
||||||
|
"login_form_label_email": "E-Mail",
|
||||||
|
"login_form_label_password": "Passwort",
|
||||||
|
"login_form_password_hint": "password",
|
||||||
|
"login_form_save_login": "Angemeldet bleiben",
|
||||||
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
|
"profile_drawer_client_server_up_to_date": "App und Server sind aktuell",
|
||||||
|
"profile_drawer_sign_out": "Abmelden",
|
||||||
|
"search_bar_hint": "Durchsuche deine Fotos",
|
||||||
|
"search_page_no_places": "Keine Informationen über Orte verfügbar",
|
||||||
|
"search_page_places": "Orte",
|
||||||
|
"search_page_things": "Dinge",
|
||||||
|
"search_result_page_new_search_hint": "Neue Suche",
|
||||||
|
"select_additional_user_for_sharing_page_suggestions": "Vorschläge",
|
||||||
|
"select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
|
||||||
|
"share_add": "Hinzufügen",
|
||||||
|
"share_add_photos": "Fotos hinzufügen",
|
||||||
|
"share_add_title": "Titel hinzufügen",
|
||||||
|
"share_create_album": "Album erstellen",
|
||||||
|
"share_invite": "Zum Album einladen",
|
||||||
|
"sharing_page_album": "Geteilte Alben",
|
||||||
|
"sharing_page_description": "Erstelle ein geteiltes Album um Fotos und Videos mit Personen in deinem Netzwerk zu teilen.",
|
||||||
|
"sharing_page_empty_list": "LEERE LISTE",
|
||||||
|
"sharing_silver_appbar_create_shared_album": "Neues geteiltes Album",
|
||||||
|
"sharing_silver_appbar_share_partner": "Teile mit Partner",
|
||||||
|
"tab_controller_nav_photos": "Fotos",
|
||||||
|
"tab_controller_nav_search": "Suche",
|
||||||
|
"tab_controller_nav_sharing": "Teilen",
|
||||||
|
"version_announcement_overlay_ack": "Ich habe verstanden",
|
||||||
|
"version_announcement_overlay_release_notes": "Änderungsprotokoll",
|
||||||
|
"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_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"
|
||||||
|
}
|
||||||
105
mobile/assets/i18n/en-US.json
Normal file
105
mobile/assets/i18n/en-US.json
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"album_info_card_backup_album_excluded": "EXCLUDED",
|
||||||
|
"album_info_card_backup_album_included": "INCLUDED",
|
||||||
|
"album_viewer_appbar_share_delete": "Delete album",
|
||||||
|
"album_viewer_appbar_share_err_delete": "Failed to delete album",
|
||||||
|
"album_viewer_appbar_share_err_leave": "Failed to leave album",
|
||||||
|
"album_viewer_appbar_share_err_remove": "There are problems in removing assets from album",
|
||||||
|
"album_viewer_appbar_share_err_title": "Failed to change album title",
|
||||||
|
"album_viewer_appbar_share_leave": "Leave album",
|
||||||
|
"album_viewer_appbar_share_remove": "Remove from album",
|
||||||
|
"album_viewer_page_share_add_users": "Add users",
|
||||||
|
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
||||||
|
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
|
||||||
|
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
||||||
|
"backup_album_selection_page_select_albums": "Select Albums",
|
||||||
|
"backup_album_selection_page_selection_info": "Selection Info",
|
||||||
|
"backup_album_selection_page_total_assets": "Total unique assets",
|
||||||
|
"backup_all": "All",
|
||||||
|
"backup_controller_page_albums": "Backup Albums",
|
||||||
|
"backup_controller_page_backup": "Backup",
|
||||||
|
"backup_controller_page_backup_selected": "Selected: ",
|
||||||
|
"backup_controller_page_backup_sub": "Backed up photos and videos",
|
||||||
|
"backup_controller_page_cancel": "Cancel",
|
||||||
|
"backup_controller_page_created": "Created on: {}",
|
||||||
|
"backup_controller_page_desc_backup": "Turn on backup to automatically upload new assets to the server.",
|
||||||
|
"backup_controller_page_excluded": "Excluded: ",
|
||||||
|
"backup_controller_page_failed": "Failed ({})",
|
||||||
|
"backup_controller_page_filename": "File name: {} [{}]",
|
||||||
|
"backup_controller_page_id": "ID: {}",
|
||||||
|
"backup_controller_page_info": "Backup Information",
|
||||||
|
"backup_controller_page_none_selected": "None selected",
|
||||||
|
"backup_controller_page_remainder": "Remainder",
|
||||||
|
"backup_controller_page_remainder_sub": "Remaining photos and albums to back up from selection",
|
||||||
|
"backup_controller_page_select": "Select",
|
||||||
|
"backup_controller_page_server_storage": "Server Storage",
|
||||||
|
"backup_controller_page_start_backup": "Start Backup",
|
||||||
|
"backup_controller_page_status_off": "Backup is off",
|
||||||
|
"backup_controller_page_status_on": "Backup is on",
|
||||||
|
"backup_controller_page_storage_format": "{} of {} used",
|
||||||
|
"backup_controller_page_to_backup": "Albums to be backup",
|
||||||
|
"backup_controller_page_total": "Total",
|
||||||
|
"backup_controller_page_total_sub": "All unique photos and videos from selected albums",
|
||||||
|
"backup_controller_page_turn_off": "Turn off Backup",
|
||||||
|
"backup_controller_page_turn_on": "Turn on Backup",
|
||||||
|
"backup_controller_page_uploading_file_info": "Uploading file info",
|
||||||
|
"backup_err_only_album": "Cannot remove the only album",
|
||||||
|
"backup_info_card_assets": "assets",
|
||||||
|
"control_bottom_app_bar_delete": "Delete",
|
||||||
|
"create_shared_album_page_share": "Share",
|
||||||
|
"create_shared_album_page_share_add_assets": "ADD ASSETS",
|
||||||
|
"create_shared_album_page_share_select_photos": "Select Photos",
|
||||||
|
"daily_title_text_date": "E, MMM dd",
|
||||||
|
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
||||||
|
"date_format": "E, LLL d, y • h:mm a",
|
||||||
|
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
|
||||||
|
"delete_dialog_cancel": "Cancel",
|
||||||
|
"delete_dialog_ok": "Delete",
|
||||||
|
"delete_dialog_title": "Delete Permanently",
|
||||||
|
"exif_bottom_sheet_description": "Add Description...",
|
||||||
|
"exif_bottom_sheet_details": "DETAILS",
|
||||||
|
"exif_bottom_sheet_location": "LOCATION",
|
||||||
|
"login_form_button_text": "Login",
|
||||||
|
"login_form_email_hint": "youremail@email.com",
|
||||||
|
"login_form_endpoint_hint": "http://your-server-ip:port/api",
|
||||||
|
"login_form_endpoint_url": "Server Endpoint URL",
|
||||||
|
"login_form_err_http": "Please specify http:// or https://",
|
||||||
|
"login_form_err_invalid_email": "Invalid Email",
|
||||||
|
"login_form_err_leading_whitespace": "Leading whitespace",
|
||||||
|
"login_form_err_trailing_whitespace": "Trailing whitespace",
|
||||||
|
"login_form_label_email": "Email",
|
||||||
|
"login_form_label_password": "Password",
|
||||||
|
"login_form_password_hint": "password",
|
||||||
|
"login_form_save_login": "Stay logged in",
|
||||||
|
"login_form_failed_login": "Error logging you in, check server url, email and password",
|
||||||
|
"monthly_title_text_date_format": "MMMM y",
|
||||||
|
"profile_drawer_client_server_up_to_date": "Client and Server are up-to-date",
|
||||||
|
"profile_drawer_sign_out": "Sign Out",
|
||||||
|
"search_bar_hint": "Search your photos",
|
||||||
|
"search_page_no_places": "No Places Info Available",
|
||||||
|
"search_page_places": "Places",
|
||||||
|
"search_page_things": "Things",
|
||||||
|
"search_result_page_new_search_hint": "New Search",
|
||||||
|
"select_additional_user_for_sharing_page_suggestions": "Suggestions",
|
||||||
|
"select_user_for_sharing_page_err_album": "Failed to create album",
|
||||||
|
"select_user_for_sharing_page_share_suggestions": "Suggestions",
|
||||||
|
"share_add": "Add",
|
||||||
|
"share_add_photos": "Add photos",
|
||||||
|
"share_add_title": "Add a title",
|
||||||
|
"share_create_album": "Create album",
|
||||||
|
"share_invite": "Invite to album",
|
||||||
|
"sharing_page_album": "Shared albums",
|
||||||
|
"sharing_page_description": "Create shared albums to share photos and videos with people in your network.",
|
||||||
|
"sharing_page_empty_list": "EMPTY LIST",
|
||||||
|
"sharing_silver_appbar_create_shared_album": "Create shared album",
|
||||||
|
"sharing_silver_appbar_share_partner": "Share with partner",
|
||||||
|
"tab_controller_nav_photos": "Photos",
|
||||||
|
"tab_controller_nav_search": "Search",
|
||||||
|
"tab_controller_nav_sharing": "Sharing",
|
||||||
|
"version_announcement_overlay_ack": "Acknowledge",
|
||||||
|
"version_announcement_overlay_release_notes": "release notes",
|
||||||
|
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
|
||||||
|
"version_announcement_overlay_text_2": "please take your time to visit the ",
|
||||||
|
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
||||||
|
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
|
||||||
|
}
|
||||||
@@ -82,5 +82,11 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>https</string>
|
<string>https</string>
|
||||||
</array>
|
</array>
|
||||||
|
|
||||||
|
<key>CFBundleLocalizations</key>
|
||||||
|
<array>
|
||||||
|
<string>en</string>
|
||||||
|
<string>de</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -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.13.0"
|
version_number: "1.18.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
@@ -36,7 +37,24 @@ void main() async {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
runApp(const ProviderScope(child: ImmichApp()));
|
await EasyLocalization.ensureInitialized();
|
||||||
|
|
||||||
|
var locales = const [
|
||||||
|
// Default locale
|
||||||
|
Locale('en', 'US'),
|
||||||
|
// Additional locales
|
||||||
|
Locale('de', 'DE')
|
||||||
|
];
|
||||||
|
|
||||||
|
runApp(
|
||||||
|
EasyLocalization(
|
||||||
|
supportedLocales: locales,
|
||||||
|
path: 'assets/i18n',
|
||||||
|
useFallbackTranslations: true,
|
||||||
|
fallbackLocale: locales.first,
|
||||||
|
child: const ProviderScope(child: ImmichApp()),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImmichApp extends ConsumerStatefulWidget {
|
class ImmichApp extends ConsumerStatefulWidget {
|
||||||
@@ -96,6 +114,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
initApp().then((_) => debugPrint("App Init Completed"));
|
initApp().then((_) => debugPrint("App Init Completed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,13 +124,15 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
final _immichRouter = AppRouter();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
var router = ref.watch(appRouterProvider);
|
||||||
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
||||||
|
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
|
localizationsDelegates: context.localizationDelegates,
|
||||||
|
supportedLocales: context.supportedLocales,
|
||||||
|
locale: context.locale,
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
home: Stack(
|
home: Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -124,7 +145,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
primarySwatch: Colors.indigo,
|
primarySwatch: Colors.indigo,
|
||||||
fontFamily: 'WorkSans',
|
fontFamily: 'WorkSans',
|
||||||
snackBarTheme: const SnackBarThemeData(
|
snackBarTheme: const SnackBarThemeData(
|
||||||
contentTextStyle: TextStyle(fontFamily: 'WorkSans')),
|
contentTextStyle: TextStyle(fontFamily: 'WorkSans'),
|
||||||
|
),
|
||||||
scaffoldBackgroundColor: immichBackgroundColor,
|
scaffoldBackgroundColor: immichBackgroundColor,
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
backgroundColor: immichBackgroundColor,
|
backgroundColor: immichBackgroundColor,
|
||||||
@@ -134,9 +156,10 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
routeInformationParser: _immichRouter.defaultRouteParser(),
|
routeInformationParser: router.defaultRouteParser(),
|
||||||
routerDelegate: _immichRouter.delegate(
|
routerDelegate: router.delegate(
|
||||||
navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
|
navigatorObservers: () => [TabNavigationObserver(ref: ref)],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const ImmichLoadingOverlay(),
|
const ImmichLoadingOverlay(),
|
||||||
const VersionAnnouncementOverlay(),
|
const VersionAnnouncementOverlay(),
|
||||||
|
|||||||
@@ -28,22 +28,26 @@ class ImageViewerPageState {
|
|||||||
|
|
||||||
factory ImageViewerPageState.fromMap(Map<String, dynamic> map) {
|
factory ImageViewerPageState.fromMap(Map<String, dynamic> map) {
|
||||||
return ImageViewerPageState(
|
return ImageViewerPageState(
|
||||||
downloadAssetStatus: DownloadAssetStatus.values[map['downloadAssetStatus'] ?? 0],
|
downloadAssetStatus:
|
||||||
|
DownloadAssetStatus.values[map['downloadAssetStatus'] ?? 0],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
factory ImageViewerPageState.fromJson(String source) => ImageViewerPageState.fromMap(json.decode(source));
|
factory ImageViewerPageState.fromJson(String source) =>
|
||||||
|
ImageViewerPageState.fromMap(json.decode(source));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)';
|
String toString() =>
|
||||||
|
'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
return other is ImageViewerPageState && other.downloadAssetStatus == downloadAssetStatus;
|
return other is ImageViewerPageState &&
|
||||||
|
other.downloadAssetStatus == downloadAssetStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -3,15 +3,20 @@ 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/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
||||||
final ImageViewerService _imageViewerService = ImageViewerService();
|
final ImageViewerService _imageViewerService;
|
||||||
|
|
||||||
ImageViewerStateNotifier() : super(ImageViewerPageState(downloadAssetStatus: DownloadAssetStatus.idle));
|
ImageViewerStateNotifier(this._imageViewerService)
|
||||||
|
: super(
|
||||||
|
ImageViewerPageState(
|
||||||
|
downloadAssetStatus: DownloadAssetStatus.idle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
void downloadAsset(ImmichAsset asset, BuildContext context) async {
|
void downloadAsset(AssetResponseDto asset, BuildContext context) async {
|
||||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
|
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading);
|
||||||
|
|
||||||
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
|
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
|
||||||
@@ -40,4 +45,6 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final imageViewerStateProvider =
|
final imageViewerStateProvider =
|
||||||
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(((ref) => ImageViewerStateNotifier()));
|
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
|
||||||
|
((ref) => ImageViewerStateNotifier(ref.watch(imageViewerServiceProvider))),
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
|
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
final imageViewerServiceProvider =
|
||||||
|
Provider((ref) => ImageViewerService(ref.watch(apiServiceProvider)));
|
||||||
|
|
||||||
class ImageViewerService {
|
class ImageViewerService {
|
||||||
Future<bool> downloadAssetToDevice(ImmichAsset asset) async {
|
final ApiService _apiService;
|
||||||
|
ImageViewerService(this._apiService);
|
||||||
|
|
||||||
|
Future<bool> downloadAssetToDevice(AssetResponseDto asset) async {
|
||||||
try {
|
try {
|
||||||
String fileName = p.basename(asset.originalPath);
|
String fileName = p.basename(asset.originalPath);
|
||||||
var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
|
||||||
Uri filePath =
|
|
||||||
Uri.parse("$savedEndpoint/asset/download?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false");
|
|
||||||
|
|
||||||
var res = await http.get(
|
var res = await _apiService.assetApi.downloadFileWithHttpInfo(
|
||||||
filePath,
|
asset.deviceAssetId,
|
||||||
headers: {"Authorization": "Bearer ${Hive.box(userInfoBox).get(accessTokenKey)}"},
|
asset.deviceId,
|
||||||
|
isThumb: false,
|
||||||
|
isWeb: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
final AssetEntity? entity;
|
final AssetEntity? entity;
|
||||||
|
|
||||||
if (asset.type == 'IMAGE') {
|
if (asset.type == AssetTypeEnum.IMAGE) {
|
||||||
entity = await PhotoManager.editor.saveImage(
|
entity = await PhotoManager.editor.saveImage(
|
||||||
res.bodyBytes,
|
res.bodyBytes,
|
||||||
title: p.basename(asset.originalPath),
|
title: p.basename(asset.originalPath),
|
||||||
@@ -37,14 +41,10 @@ class ImageViewerService {
|
|||||||
entity = await PhotoManager.editor.saveVideo(tempFile, title: fileName);
|
entity = await PhotoManager.editor.saveVideo(tempFile, title: fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entity != null) {
|
return entity != null;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error saving file $e");
|
debugPrint("Error saving file $e");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
class ExifBottomSheet extends ConsumerWidget {
|
class ExifBottomSheet extends ConsumerWidget {
|
||||||
final ImmichAssetWithExif assetDetail;
|
final AssetResponseDto assetDetail;
|
||||||
|
|
||||||
const ExifBottomSheet({Key? key, required this.assetDetail})
|
const ExifBottomSheet({Key? key, required this.assetDetail})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
@@ -15,171 +15,171 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
_buildMap() {
|
_buildMap() {
|
||||||
return (assetDetail.exifInfo!.latitude != null &&
|
return Padding(
|
||||||
assetDetail.exifInfo!.longitude != null)
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
? Padding(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
height: 150,
|
||||||
child: Container(
|
width: MediaQuery.of(context).size.width,
|
||||||
height: 150,
|
decoration: const BoxDecoration(
|
||||||
width: MediaQuery.of(context).size.width,
|
borderRadius: BorderRadius.all(Radius.circular(15)),
|
||||||
decoration: const BoxDecoration(
|
),
|
||||||
borderRadius: BorderRadius.all(Radius.circular(15)),
|
child: FlutterMap(
|
||||||
),
|
options: MapOptions(
|
||||||
child: FlutterMap(
|
center: LatLng(
|
||||||
options: MapOptions(
|
assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
|
||||||
center: LatLng(assetDetail.exifInfo!.latitude!,
|
assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
|
||||||
assetDetail.exifInfo!.longitude!),
|
|
||||||
zoom: 16.0,
|
|
||||||
),
|
|
||||||
layers: [
|
|
||||||
TileLayerOptions(
|
|
||||||
urlTemplate:
|
|
||||||
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
|
||||||
subdomains: ['a', 'b', 'c'],
|
|
||||||
attributionBuilder: (_) {
|
|
||||||
return const Text(
|
|
||||||
"© OpenStreetMap",
|
|
||||||
style: TextStyle(fontSize: 10),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
MarkerLayerOptions(
|
|
||||||
markers: [
|
|
||||||
Marker(
|
|
||||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
|
||||||
point: LatLng(assetDetail.exifInfo!.latitude!,
|
|
||||||
assetDetail.exifInfo!.longitude!),
|
|
||||||
builder: (ctx) => const Image(
|
|
||||||
image: AssetImage('assets/location-pin.png')),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
zoom: 16.0,
|
||||||
: Container();
|
),
|
||||||
|
layers: [
|
||||||
|
TileLayerOptions(
|
||||||
|
urlTemplate:
|
||||||
|
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||||
|
subdomains: ['a', 'b', 'c'],
|
||||||
|
attributionBuilder: (_) {
|
||||||
|
return const Text(
|
||||||
|
"© OpenStreetMap",
|
||||||
|
style: TextStyle(fontSize: 10),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MarkerLayerOptions(
|
||||||
|
markers: [
|
||||||
|
Marker(
|
||||||
|
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||||
|
point: LatLng(
|
||||||
|
assetDetail.exifInfo?.latitude?.toDouble() ?? 0,
|
||||||
|
assetDetail.exifInfo?.longitude?.toDouble() ?? 0,
|
||||||
|
),
|
||||||
|
builder: (ctx) => const Image(
|
||||||
|
image: AssetImage('assets/location-pin.png'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildLocationText() {
|
_buildLocationText() {
|
||||||
return (assetDetail.exifInfo!.city != null &&
|
return Text(
|
||||||
assetDetail.exifInfo!.state != null)
|
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
|
||||||
? Text(
|
style: TextStyle(
|
||||||
"${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
|
fontSize: 12,
|
||||||
style: TextStyle(
|
color: Colors.grey[200],
|
||||||
fontSize: 12,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.grey[200],
|
),
|
||||||
fontWeight: FontWeight.bold),
|
);
|
||||||
)
|
|
||||||
: Container();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
assetDetail.exifInfo?.dateTimeOriginal != null
|
if (assetDetail.exifInfo?.dateTimeOriginal != null)
|
||||||
? Text(
|
Text(
|
||||||
DateFormat('E, LLL d, y • h:mm a').format(
|
DateFormat('date_format'.tr()).format(
|
||||||
DateTime.parse(assetDetail.exifInfo!.dateTimeOriginal!),
|
assetDetail.exifInfo!.dateTimeOriginal!,
|
||||||
),
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey[400],
|
color: Colors.grey[400],
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: Container(),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Add Description...",
|
"exif_bottom_sheet_description",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey[500],
|
color: Colors.grey[500],
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
),
|
),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Location
|
// Location
|
||||||
assetDetail.exifInfo?.latitude != null
|
if (assetDetail.exifInfo?.latitude != null)
|
||||||
? Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 32.0),
|
padding: const EdgeInsets.only(top: 32.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Divider(
|
Divider(
|
||||||
thickness: 1,
|
thickness: 1,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"LOCATION",
|
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
|
||||||
),
|
|
||||||
_buildMap(),
|
|
||||||
_buildLocationText(),
|
|
||||||
Text(
|
|
||||||
"${assetDetail.exifInfo!.latitude!.toStringAsFixed(4)}, ${assetDetail.exifInfo!.longitude!.toStringAsFixed(4)}",
|
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
)
|
Text(
|
||||||
: Container(),
|
"exif_bottom_sheet_location",
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
||||||
|
).tr(),
|
||||||
|
if (assetDetail.exifInfo?.latitude != null &&
|
||||||
|
assetDetail.exifInfo?.longitude != null)
|
||||||
|
_buildMap(),
|
||||||
|
if (assetDetail.exifInfo?.city != null &&
|
||||||
|
assetDetail.exifInfo?.state != null)
|
||||||
|
_buildLocationText(),
|
||||||
|
Text(
|
||||||
|
"${assetDetail.exifInfo?.latitude?.toStringAsFixed(4)}, ${assetDetail.exifInfo?.longitude?.toStringAsFixed(4)}",
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
// Detail
|
// Detail
|
||||||
assetDetail.exifInfo != null
|
if (assetDetail.exifInfo != null)
|
||||||
? Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 32.0),
|
padding: const EdgeInsets.only(top: 32.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Divider(
|
Divider(
|
||||||
thickness: 1,
|
thickness: 1,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
|
||||||
child: Text(
|
|
||||||
"DETAILS",
|
|
||||||
style:
|
|
||||||
TextStyle(fontSize: 11, color: Colors.grey[400]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
contentPadding: const EdgeInsets.all(0),
|
|
||||||
dense: true,
|
|
||||||
textColor: Colors.grey[300],
|
|
||||||
iconColor: Colors.grey[300],
|
|
||||||
leading: const Icon(Icons.image),
|
|
||||||
title: Text(
|
|
||||||
"${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
subtitle: assetDetail.exifInfo?.exifImageHeight != null
|
|
||||||
? Text(
|
|
||||||
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ")
|
|
||||||
: Container(),
|
|
||||||
),
|
|
||||||
assetDetail.exifInfo?.make != null
|
|
||||||
? ListTile(
|
|
||||||
contentPadding: const EdgeInsets.all(0),
|
|
||||||
dense: true,
|
|
||||||
textColor: Colors.grey[300],
|
|
||||||
iconColor: Colors.grey[300],
|
|
||||||
leading: const Icon(Icons.camera),
|
|
||||||
title: Text(
|
|
||||||
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / assetDetail.exifInfo!.exposureTime!).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} "),
|
|
||||||
)
|
|
||||||
: Container()
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
)
|
Padding(
|
||||||
: Container()
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
|
child: Text(
|
||||||
|
"exif_bottom_sheet_details",
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.grey[400]),
|
||||||
|
).tr(),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
dense: true,
|
||||||
|
textColor: Colors.grey[300],
|
||||||
|
iconColor: Colors.grey[300],
|
||||||
|
leading: const Icon(Icons.image),
|
||||||
|
title: Text(
|
||||||
|
"${assetDetail.exifInfo?.imageName!}${p.extension(assetDetail.originalPath)}",
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
subtitle: assetDetail.exifInfo?.exifImageHeight != null
|
||||||
|
? Text(
|
||||||
|
"${assetDetail.exifInfo?.exifImageHeight} x ${assetDetail.exifInfo?.exifImageWidth} ${assetDetail.exifInfo?.fileSizeInByte!}B ",
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
if (assetDetail.exifInfo?.make != null)
|
||||||
|
ListTile(
|
||||||
|
contentPadding: const EdgeInsets.all(0),
|
||||||
|
dense: true,
|
||||||
|
textColor: Colors.grey[300],
|
||||||
|
iconColor: Colors.grey[300],
|
||||||
|
leading: const Icon(Icons.camera),
|
||||||
|
title: Text(
|
||||||
|
"${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"ƒ/${assetDetail.exifInfo?.fNumber} 1/${(1 / (assetDetail.exifInfo?.exposureTime ?? 1)).toStringAsFixed(0)} ${assetDetail.exifInfo?.focalLength}mm ISO${assetDetail.exifInfo?.iso} ",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,16 +17,20 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
bool allowMoving = _status == _RemoteImageStatus.full;
|
bool allowMoving = _status == _RemoteImageStatus.full;
|
||||||
|
|
||||||
return PhotoView(
|
return PhotoView(
|
||||||
imageProvider: _imageProvider,
|
imageProvider: _imageProvider,
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained,
|
maxScale: allowMoving ? 1.0 : PhotoViewComputedScale.contained,
|
||||||
enablePanAlways: true,
|
enablePanAlways: true,
|
||||||
scaleStateChangedCallback: _scaleStateChanged,
|
scaleStateChangedCallback: _scaleStateChanged,
|
||||||
onScaleEnd: _onScaleListener);
|
onScaleEnd: _onScaleListener,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onScaleListener(BuildContext context, ScaleEndDetails details,
|
void _onScaleListener(
|
||||||
PhotoViewControllerValue controllerValue) {
|
BuildContext context,
|
||||||
|
ScaleEndDetails details,
|
||||||
|
PhotoViewControllerValue controllerValue,
|
||||||
|
) {
|
||||||
// Disable swipe events when zoomed in
|
// Disable swipe events when zoomed in
|
||||||
if (_zoomedIn) return;
|
if (_zoomedIn) return;
|
||||||
|
|
||||||
@@ -42,12 +46,17 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CachedNetworkImageProvider _authorizedImageProvider(String url) {
|
CachedNetworkImageProvider _authorizedImageProvider(String url) {
|
||||||
return CachedNetworkImageProvider(url,
|
return CachedNetworkImageProvider(
|
||||||
headers: {"Authorization": widget.authToken}, cacheKey: url);
|
url,
|
||||||
|
headers: {"Authorization": widget.authToken},
|
||||||
|
cacheKey: url,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _performStateTransition(
|
void _performStateTransition(
|
||||||
_RemoteImageStatus newStatus, CachedNetworkImageProvider provider) {
|
_RemoteImageStatus newStatus,
|
||||||
|
CachedNetworkImageProvider provider,
|
||||||
|
) {
|
||||||
// Transition to same status is forbidden
|
// Transition to same status is forbidden
|
||||||
if (_status == newStatus) return;
|
if (_status == newStatus) return;
|
||||||
// Transition full -> thumbnail is forbidden
|
// Transition full -> thumbnail is forbidden
|
||||||
@@ -67,19 +76,22 @@ class _RemotePhotoViewState extends State<RemotePhotoView> {
|
|||||||
_authorizedImageProvider(widget.thumbnailUrl);
|
_authorizedImageProvider(widget.thumbnailUrl);
|
||||||
_imageProvider = thumbnailProvider;
|
_imageProvider = thumbnailProvider;
|
||||||
|
|
||||||
thumbnailProvider
|
thumbnailProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
.resolve(const ImageConfiguration())
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
.addListener(ImageStreamListener((ImageInfo imageInfo, _) {
|
_performStateTransition(
|
||||||
_performStateTransition(_RemoteImageStatus.thumbnail, thumbnailProvider);
|
_RemoteImageStatus.thumbnail,
|
||||||
}));
|
thumbnailProvider,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
CachedNetworkImageProvider fullProvider =
|
CachedNetworkImageProvider fullProvider =
|
||||||
_authorizedImageProvider(widget.imageUrl);
|
_authorizedImageProvider(widget.imageUrl);
|
||||||
fullProvider
|
fullProvider.resolve(const ImageConfiguration()).addListener(
|
||||||
.resolve(const ImageConfiguration())
|
ImageStreamListener((ImageInfo imageInfo, _) {
|
||||||
.addListener(ImageStreamListener((ImageInfo imageInfo, _) {
|
_performStateTransition(_RemoteImageStatus.full, fullProvider);
|
||||||
_performStateTransition(_RemoteImageStatus.full, fullProvider);
|
}),
|
||||||
}));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -90,14 +102,14 @@ 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.onSwipeDown,
|
||||||
required this.onSwipeUp})
|
required this.onSwipeUp,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String thumbnailUrl;
|
final String thumbnailUrl;
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import 'dart:developer';
|
|||||||
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/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
||||||
const TopControlAppBar(
|
const TopControlAppBar({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.asset,
|
required this.asset,
|
||||||
required this.onMoreInfoPressed,
|
required this.onMoreInfoPressed,
|
||||||
required this.onDownloadPressed})
|
required this.onDownloadPressed,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
final Function onMoreInfoPressed;
|
final Function onMoreInfoPressed;
|
||||||
final Function onDownloadPressed;
|
final Function onDownloadPressed;
|
||||||
|
|
||||||
@@ -54,12 +54,13 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
|
|||||||
: const Icon(Icons.favorite_border_rounded),
|
: const Icon(Icons.favorite_border_rounded),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
iconSize: iconSize,
|
iconSize: iconSize,
|
||||||
splashRadius: iconSize,
|
splashRadius: iconSize,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onMoreInfoPressed();
|
onMoreInfoPressed();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.more_horiz_rounded))
|
icon: const Icon(Icons.more_horiz_rounded),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,29 +11,34 @@ 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/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/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class ImageViewerPage extends HookConsumerWidget {
|
class ImageViewerPage extends HookConsumerWidget {
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
final String heroTag;
|
final String heroTag;
|
||||||
final String thumbnailUrl;
|
final String thumbnailUrl;
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
final AssetService _assetService = AssetService();
|
|
||||||
ImmichAssetWithExif? assetDetail;
|
|
||||||
|
|
||||||
ImageViewerPage(
|
AssetResponseDto? assetDetail;
|
||||||
{Key? key, required this.imageUrl, required this.heroTag, required this.thumbnailUrl, required this.asset})
|
|
||||||
: super(key: key);
|
ImageViewerPage({
|
||||||
|
Key? key,
|
||||||
|
required this.imageUrl,
|
||||||
|
required this.heroTag,
|
||||||
|
required this.thumbnailUrl,
|
||||||
|
required this.asset,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
final downloadAssetStatus =
|
||||||
|
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
|
|
||||||
getAssetExif() async {
|
getAssetExif() async {
|
||||||
assetDetail = await _assetService.getAssetById(asset.id);
|
assetDetail =
|
||||||
|
await ref.watch(assetServiceProvider).getAssetById(asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
showInfo() {
|
showInfo() {
|
||||||
@@ -48,10 +53,13 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
getAssetExif();
|
() {
|
||||||
return null;
|
getAssetExif();
|
||||||
}, []);
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
@@ -59,31 +67,33 @@ class ImageViewerPage extends HookConsumerWidget {
|
|||||||
asset: asset,
|
asset: asset,
|
||||||
onMoreInfoPressed: showInfo,
|
onMoreInfoPressed: showInfo,
|
||||||
onDownloadPressed: () {
|
onDownloadPressed: () {
|
||||||
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
|
ref
|
||||||
|
.watch(imageViewerStateProvider.notifier)
|
||||||
|
.downloadAsset(asset, context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: heroTag,
|
tag: heroTag,
|
||||||
child: RemotePhotoView(
|
child: RemotePhotoView(
|
||||||
thumbnailUrl: thumbnailUrl,
|
thumbnailUrl: thumbnailUrl,
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
authToken: "Bearer ${box.get(accessTokenKey)}",
|
authToken: "Bearer ${box.get(accessTokenKey)}",
|
||||||
onSwipeDown: () => AutoRouter.of(context).pop(),
|
onSwipeDown: () => AutoRouter.of(context).pop(),
|
||||||
onSwipeUp: () => showInfo(),
|
onSwipeUp: () => showInfo(),
|
||||||
)
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
),
|
||||||
const Center(
|
if (downloadAssetStatus == DownloadAssetStatus.loading)
|
||||||
child: DownloadLoadingIndicator(),
|
const Center(
|
||||||
),
|
child: DownloadLoadingIndicator(),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,22 +12,22 @@ import 'package:immich_mobile/modules/asset_viewer/ui/download_loading_indicator
|
|||||||
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.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/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class VideoViewerPage extends HookConsumerWidget {
|
class VideoViewerPage extends HookConsumerWidget {
|
||||||
final String videoUrl;
|
final String videoUrl;
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
ImmichAssetWithExif? assetDetail;
|
AssetResponseDto? assetDetail;
|
||||||
final AssetService _assetService = AssetService();
|
|
||||||
|
|
||||||
VideoViewerPage({Key? key, required this.videoUrl, required this.asset}) : super(key: key);
|
VideoViewerPage({Key? key, required this.videoUrl, required this.asset})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final downloadAssetStatus = ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
final downloadAssetStatus =
|
||||||
|
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||||
|
|
||||||
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
|
String jwtToken = Hive.box(userInfoBox).get(accessTokenKey);
|
||||||
|
|
||||||
@@ -44,13 +44,17 @@ class VideoViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAssetExif() async {
|
getAssetExif() async {
|
||||||
assetDetail = await _assetService.getAssetById(asset.id);
|
assetDetail =
|
||||||
|
await ref.watch(assetServiceProvider).getAssetById(asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
getAssetExif();
|
() {
|
||||||
return null;
|
getAssetExif();
|
||||||
}, []);
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
@@ -60,7 +64,9 @@ class VideoViewerPage extends HookConsumerWidget {
|
|||||||
showInfo();
|
showInfo();
|
||||||
},
|
},
|
||||||
onDownloadPressed: () {
|
onDownloadPressed: () {
|
||||||
ref.watch(imageViewerStateProvider.notifier).downloadAsset(asset, context);
|
ref
|
||||||
|
.watch(imageViewerStateProvider.notifier)
|
||||||
|
.downloadAsset(asset, context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
body: SwipeDetector(
|
body: SwipeDetector(
|
||||||
@@ -93,7 +99,8 @@ class VideoThumbnailPlayer extends StatefulWidget {
|
|||||||
final String url;
|
final String url;
|
||||||
final String? jwtToken;
|
final String? jwtToken;
|
||||||
|
|
||||||
const VideoThumbnailPlayer({Key? key, required this.url, this.jwtToken}) : super(key: key);
|
const VideoThumbnailPlayer({Key? key, required this.url, this.jwtToken})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<VideoThumbnailPlayer> createState() => _VideoThumbnailPlayerState();
|
State<VideoThumbnailPlayer> createState() => _VideoThumbnailPlayerState();
|
||||||
@@ -111,8 +118,10 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
|
|||||||
|
|
||||||
Future<void> initializePlayer() async {
|
Future<void> initializePlayer() async {
|
||||||
try {
|
try {
|
||||||
videoPlayerController =
|
videoPlayerController = VideoPlayerController.network(
|
||||||
VideoPlayerController.network(widget.url, httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"});
|
widget.url,
|
||||||
|
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
|
||||||
|
);
|
||||||
|
|
||||||
await videoPlayerController.initialize();
|
await videoPlayerController.initialize();
|
||||||
_createChewieController();
|
_createChewieController();
|
||||||
@@ -142,7 +151,7 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return chewieController != null && chewieController!.videoPlayerController.value.isInitialized
|
return chewieController?.videoPlayerController.value.isInitialized == true
|
||||||
? SizedBox(
|
? SizedBox(
|
||||||
child: Chewie(
|
child: Chewie(
|
||||||
controller: chewieController!,
|
controller: chewieController!,
|
||||||
|
|||||||
@@ -21,13 +21,16 @@ class AvailableAlbum {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AvailableAlbum(albumEntity: $albumEntity, thumbnailData: $thumbnailData)';
|
String toString() =>
|
||||||
|
'AvailableAlbum(albumEntity: $albumEntity, thumbnailData: $thumbnailData)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
return other is AvailableAlbum && other.albumEntity == albumEntity && other.thumbnailData == thumbnailData;
|
return other is AvailableAlbum &&
|
||||||
|
other.albumEntity == albumEntity &&
|
||||||
|
other.thumbnailData == thumbnailData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import 'package:cancellation_token_http/http.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
|
|
||||||
enum BackUpProgressEnum { idle, inProgress, done }
|
enum BackUpProgressEnum { idle, inProgress, done }
|
||||||
|
|
||||||
class BackUpState extends Equatable {
|
class BackUpState {
|
||||||
// enum
|
// enum
|
||||||
final BackUpProgressEnum backupProgress;
|
final BackUpProgressEnum backupProgress;
|
||||||
final List<String> allAssetOnDatabase;
|
final List<String> allAssetsInDatabase;
|
||||||
final double progressInPercentage;
|
final double progressInPercentage;
|
||||||
final CancellationToken cancelToken;
|
final CancellationToken cancelToken;
|
||||||
final ServerInfo serverInfo;
|
final ServerInfoResponseDto serverInfo;
|
||||||
|
|
||||||
/// All available albums on the device
|
/// All available albums on the device
|
||||||
final List<AvailableAlbum> availableAlbums;
|
final List<AvailableAlbum> availableAlbums;
|
||||||
@@ -26,9 +27,12 @@ class BackUpState extends Equatable {
|
|||||||
/// All assets from the selected albums that have been backup
|
/// All assets from the selected albums that have been backup
|
||||||
final Set<String> selectedAlbumsBackupAssetsIds;
|
final Set<String> selectedAlbumsBackupAssetsIds;
|
||||||
|
|
||||||
|
// Current Backup Asset
|
||||||
|
final CurrentUploadAsset currentUploadAsset;
|
||||||
|
|
||||||
const BackUpState({
|
const BackUpState({
|
||||||
required this.backupProgress,
|
required this.backupProgress,
|
||||||
required this.allAssetOnDatabase,
|
required this.allAssetsInDatabase,
|
||||||
required this.progressInPercentage,
|
required this.progressInPercentage,
|
||||||
required this.cancelToken,
|
required this.cancelToken,
|
||||||
required this.serverInfo,
|
required this.serverInfo,
|
||||||
@@ -37,23 +41,25 @@ class BackUpState extends Equatable {
|
|||||||
required this.excludedBackupAlbums,
|
required this.excludedBackupAlbums,
|
||||||
required this.allUniqueAssets,
|
required this.allUniqueAssets,
|
||||||
required this.selectedAlbumsBackupAssetsIds,
|
required this.selectedAlbumsBackupAssetsIds,
|
||||||
|
required this.currentUploadAsset,
|
||||||
});
|
});
|
||||||
|
|
||||||
BackUpState copyWith({
|
BackUpState copyWith({
|
||||||
BackUpProgressEnum? backupProgress,
|
BackUpProgressEnum? backupProgress,
|
||||||
List<String>? allAssetOnDatabase,
|
List<String>? allAssetsInDatabase,
|
||||||
double? progressInPercentage,
|
double? progressInPercentage,
|
||||||
CancellationToken? cancelToken,
|
CancellationToken? cancelToken,
|
||||||
ServerInfo? serverInfo,
|
ServerInfoResponseDto? serverInfo,
|
||||||
List<AvailableAlbum>? availableAlbums,
|
List<AvailableAlbum>? availableAlbums,
|
||||||
Set<AssetPathEntity>? selectedBackupAlbums,
|
Set<AssetPathEntity>? selectedBackupAlbums,
|
||||||
Set<AssetPathEntity>? excludedBackupAlbums,
|
Set<AssetPathEntity>? excludedBackupAlbums,
|
||||||
Set<AssetEntity>? allUniqueAssets,
|
Set<AssetEntity>? allUniqueAssets,
|
||||||
Set<String>? selectedAlbumsBackupAssetsIds,
|
Set<String>? selectedAlbumsBackupAssetsIds,
|
||||||
|
CurrentUploadAsset? currentUploadAsset,
|
||||||
}) {
|
}) {
|
||||||
return BackUpState(
|
return BackUpState(
|
||||||
backupProgress: backupProgress ?? this.backupProgress,
|
backupProgress: backupProgress ?? this.backupProgress,
|
||||||
allAssetOnDatabase: allAssetOnDatabase ?? this.allAssetOnDatabase,
|
allAssetsInDatabase: allAssetsInDatabase ?? this.allAssetsInDatabase,
|
||||||
progressInPercentage: progressInPercentage ?? this.progressInPercentage,
|
progressInPercentage: progressInPercentage ?? this.progressInPercentage,
|
||||||
cancelToken: cancelToken ?? this.cancelToken,
|
cancelToken: cancelToken ?? this.cancelToken,
|
||||||
serverInfo: serverInfo ?? this.serverInfo,
|
serverInfo: serverInfo ?? this.serverInfo,
|
||||||
@@ -61,28 +67,51 @@ class BackUpState extends Equatable {
|
|||||||
selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
|
selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
|
||||||
excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
|
excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
|
||||||
allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
|
allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
|
||||||
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
|
selectedAlbumsBackupAssetsIds:
|
||||||
|
selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
|
||||||
|
currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'BackUpState(backupProgress: $backupProgress, allAssetOnDatabase: $allAssetOnDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds)';
|
return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props {
|
bool operator ==(Object other) {
|
||||||
return [
|
if (identical(this, other)) return true;
|
||||||
backupProgress,
|
final collectionEquals = const DeepCollectionEquality().equals;
|
||||||
allAssetOnDatabase,
|
|
||||||
progressInPercentage,
|
return other is BackUpState &&
|
||||||
cancelToken,
|
other.backupProgress == backupProgress &&
|
||||||
serverInfo,
|
collectionEquals(other.allAssetsInDatabase, allAssetsInDatabase) &&
|
||||||
availableAlbums,
|
other.progressInPercentage == progressInPercentage &&
|
||||||
selectedBackupAlbums,
|
other.cancelToken == cancelToken &&
|
||||||
excludedBackupAlbums,
|
other.serverInfo == serverInfo &&
|
||||||
allUniqueAssets,
|
collectionEquals(other.availableAlbums, availableAlbums) &&
|
||||||
selectedAlbumsBackupAssetsIds,
|
collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) &&
|
||||||
];
|
collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) &&
|
||||||
|
collectionEquals(other.allUniqueAssets, allUniqueAssets) &&
|
||||||
|
collectionEquals(
|
||||||
|
other.selectedAlbumsBackupAssetsIds,
|
||||||
|
selectedAlbumsBackupAssetsIds,
|
||||||
|
) &&
|
||||||
|
other.currentUploadAsset == currentUploadAsset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return backupProgress.hashCode ^
|
||||||
|
allAssetsInDatabase.hashCode ^
|
||||||
|
progressInPercentage.hashCode ^
|
||||||
|
cancelToken.hashCode ^
|
||||||
|
serverInfo.hashCode ^
|
||||||
|
availableAlbums.hashCode ^
|
||||||
|
selectedBackupAlbums.hashCode ^
|
||||||
|
excludedBackupAlbums.hashCode ^
|
||||||
|
allUniqueAssets.hashCode ^
|
||||||
|
selectedAlbumsBackupAssetsIds.hashCode ^
|
||||||
|
currentUploadAsset.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class CurrentUploadAsset {
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final String fileName;
|
||||||
|
final String fileType;
|
||||||
|
|
||||||
|
CurrentUploadAsset({
|
||||||
|
required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.fileName,
|
||||||
|
required this.fileType,
|
||||||
|
});
|
||||||
|
|
||||||
|
CurrentUploadAsset copyWith({
|
||||||
|
String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
String? fileName,
|
||||||
|
String? fileType,
|
||||||
|
}) {
|
||||||
|
return CurrentUploadAsset(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
fileName: fileName ?? this.fileName,
|
||||||
|
fileType: fileType ?? this.fileType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
|
result.addAll({'id': id});
|
||||||
|
result.addAll({'createdAt': createdAt.millisecondsSinceEpoch});
|
||||||
|
result.addAll({'fileName': fileName});
|
||||||
|
result.addAll({'fileType': fileType});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory CurrentUploadAsset.fromMap(Map<String, dynamic> map) {
|
||||||
|
return CurrentUploadAsset(
|
||||||
|
id: map['id'] ?? '',
|
||||||
|
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']),
|
||||||
|
fileName: map['fileName'] ?? '',
|
||||||
|
fileType: map['fileType'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory CurrentUploadAsset.fromJson(String source) =>
|
||||||
|
CurrentUploadAsset.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'CurrentUploadAsset(id: $id, createdAt: $createdAt, fileName: $fileName, fileType: $fileType)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is CurrentUploadAsset &&
|
||||||
|
other.id == id &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.fileName == fileName &&
|
||||||
|
other.fileType == fileType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^
|
||||||
|
createdAt.hashCode ^
|
||||||
|
fileName.hashCode ^
|
||||||
|
fileType.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
class ErrorUploadAsset extends Equatable {
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final String fileName;
|
||||||
|
final String fileType;
|
||||||
|
final AssetEntity asset;
|
||||||
|
final String errorMessage;
|
||||||
|
|
||||||
|
const ErrorUploadAsset({
|
||||||
|
required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.fileName,
|
||||||
|
required this.fileType,
|
||||||
|
required this.asset,
|
||||||
|
required this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
ErrorUploadAsset copyWith({
|
||||||
|
String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
String? fileName,
|
||||||
|
String? fileType,
|
||||||
|
AssetEntity? asset,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return ErrorUploadAsset(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
fileName: fileName ?? this.fileName,
|
||||||
|
fileType: fileType ?? this.fileType,
|
||||||
|
asset: asset ?? this.asset,
|
||||||
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'ErrorUploadAsset(id: $id, createdAt: $createdAt, fileName: $fileName, fileType: $fileType, asset: $asset, errorMessage: $errorMessage)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props {
|
||||||
|
return [
|
||||||
|
id,
|
||||||
|
fileName,
|
||||||
|
fileType,
|
||||||
|
errorMessage,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,8 @@ class HiveBackupAlbums {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'HiveBackupAlbums(selectedAlbumIds: $selectedAlbumIds, excludedAlbumsIds: $excludedAlbumsIds)';
|
String toString() =>
|
||||||
|
'HiveBackupAlbums(selectedAlbumIds: $selectedAlbumIds, excludedAlbumsIds: $excludedAlbumsIds)';
|
||||||
|
|
||||||
HiveBackupAlbums copyWith({
|
HiveBackupAlbums copyWith({
|
||||||
List<String>? selectedAlbumIds,
|
List<String>? selectedAlbumIds,
|
||||||
@@ -49,7 +50,8 @@ class HiveBackupAlbums {
|
|||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
factory HiveBackupAlbums.fromJson(String source) => HiveBackupAlbums.fromMap(json.decode(source));
|
factory HiveBackupAlbums.fromJson(String source) =>
|
||||||
|
HiveBackupAlbums.fromMap(json.decode(source));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
|
|||||||
@@ -5,27 +5,35 @@ 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/available_album.model.dart';
|
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
import 'package:immich_mobile/modules/backup/models/backup_state.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/hive_backup_albums.model.dart';
|
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.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/modules/login/models/authentication_state.model.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/shared/models/server_info.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
class BackupNotifier extends StateNotifier<BackUpState> {
|
class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
BackupNotifier({this.ref})
|
BackupNotifier(
|
||||||
: super(
|
this._backupService,
|
||||||
|
this._serverInfoService,
|
||||||
|
this._authState,
|
||||||
|
this.ref,
|
||||||
|
) : super(
|
||||||
BackUpState(
|
BackUpState(
|
||||||
backupProgress: BackUpProgressEnum.idle,
|
backupProgress: BackUpProgressEnum.idle,
|
||||||
allAssetOnDatabase: const [],
|
allAssetsInDatabase: const [],
|
||||||
progressInPercentage: 0,
|
progressInPercentage: 0,
|
||||||
cancelToken: CancellationToken(),
|
cancelToken: CancellationToken(),
|
||||||
serverInfo: ServerInfo(
|
serverInfo: ServerInfoResponseDto(
|
||||||
diskAvailable: "0",
|
diskAvailable: "0",
|
||||||
diskAvailableRaw: 0,
|
diskAvailableRaw: 0,
|
||||||
diskSize: "0",
|
diskSize: "0",
|
||||||
diskSizeRaw: 0,
|
diskSizeRaw: 0,
|
||||||
diskUsagePercentage: 0.0,
|
diskUsagePercentage: 0,
|
||||||
diskUse: "0",
|
diskUse: "0",
|
||||||
diskUseRaw: 0,
|
diskUseRaw: 0,
|
||||||
),
|
),
|
||||||
@@ -34,12 +42,21 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
excludedBackupAlbums: const {},
|
excludedBackupAlbums: const {},
|
||||||
allUniqueAssets: const {},
|
allUniqueAssets: const {},
|
||||||
selectedAlbumsBackupAssetsIds: const {},
|
selectedAlbumsBackupAssetsIds: const {},
|
||||||
|
currentUploadAsset: CurrentUploadAsset(
|
||||||
|
id: '...',
|
||||||
|
createdAt: DateTime.parse('2020-10-04'),
|
||||||
|
fileName: '...',
|
||||||
|
fileType: '...',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
) {
|
||||||
|
getBackupInfo();
|
||||||
|
}
|
||||||
|
|
||||||
Ref? ref;
|
final BackupService _backupService;
|
||||||
final BackupService _backupService = BackupService();
|
final ServerInfoService _serverInfoService;
|
||||||
final ServerInfoService _serverInfoService = ServerInfoService();
|
final AuthenticationState _authState;
|
||||||
|
final Ref ref;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// UI INTERACTION
|
/// UI INTERACTION
|
||||||
@@ -92,11 +109,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
/// If this is the first time performing backup - set the default selected album to be
|
/// If this is the first time performing backup - set the default selected album to be
|
||||||
/// the one that has all assets (Recent on Android, Recents on iOS)
|
/// the one that has all assets (Recent on Android, Recents on iOS)
|
||||||
///
|
///
|
||||||
Future<void> getBackupAlbumsInfo() async {
|
Future<void> _getBackupAlbumsInfo() async {
|
||||||
// Get all albums on the device
|
// Get all albums on the device
|
||||||
List<AvailableAlbum> availableAlbums = [];
|
List<AvailableAlbum> availableAlbums = [];
|
||||||
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
|
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
|
||||||
hasAll: true, type: RequestType.common);
|
hasAll: true,
|
||||||
|
type: RequestType.common,
|
||||||
|
);
|
||||||
|
|
||||||
for (AssetPathEntity album in albums) {
|
for (AssetPathEntity album in albums) {
|
||||||
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
||||||
@@ -139,7 +158,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
// Get album that contains all assets
|
// Get album that contains all assets
|
||||||
var list = await PhotoManager.getAssetPathList(
|
var list = await PhotoManager.getAssetPathList(
|
||||||
hasAll: true, onlyAll: true, type: RequestType.common);
|
hasAll: true,
|
||||||
|
onlyAll: true,
|
||||||
|
type: RequestType.common,
|
||||||
|
);
|
||||||
AssetPathEntity albumHasAllAssets = list.first;
|
AssetPathEntity albumHasAllAssets = list.first;
|
||||||
|
|
||||||
backupAlbumInfoBox.put(
|
backupAlbumInfoBox.put(
|
||||||
@@ -158,13 +180,15 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
|
for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
|
||||||
var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
|
var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset});
|
selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
|
for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
|
||||||
var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
|
var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset});
|
excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] Failed to generate album from id $e");
|
debugPrint("[ERROR] Failed to generate album from id $e");
|
||||||
@@ -176,7 +200,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
/// Find the assets that are not overlapping between the two sets
|
/// Find the assets that are not overlapping between the two sets
|
||||||
/// Those assets are unique and are used as the total assets
|
/// Those assets are unique and are used as the total assets
|
||||||
///
|
///
|
||||||
void _updateBackupAssetCount() async {
|
Future<void> _updateBackupAssetCount() async {
|
||||||
Set<AssetEntity> assetsFromSelectedAlbums = {};
|
Set<AssetEntity> assetsFromSelectedAlbums = {};
|
||||||
Set<AssetEntity> assetsFromExcludedAlbums = {};
|
Set<AssetEntity> assetsFromExcludedAlbums = {};
|
||||||
|
|
||||||
@@ -194,27 +218,30 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
Set<AssetEntity> allUniqueAssets =
|
Set<AssetEntity> allUniqueAssets =
|
||||||
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
|
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
|
||||||
List<String> allAssetOnDatabase =
|
var allAssetsInDatabase = await _backupService.getDeviceBackupAsset();
|
||||||
await _backupService.getDeviceBackupAsset();
|
|
||||||
|
if (allAssetsInDatabase == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Find asset that were backup from selected albums
|
// Find asset that were backup from selected albums
|
||||||
Set<String> selectedAlbumsBackupAssets =
|
Set<String> selectedAlbumsBackupAssets =
|
||||||
Set.from(allUniqueAssets.map((e) => e.id));
|
Set.from(allUniqueAssets.map((e) => e.id));
|
||||||
selectedAlbumsBackupAssets
|
selectedAlbumsBackupAssets
|
||||||
.removeWhere((assetId) => !allAssetOnDatabase.contains(assetId));
|
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
|
||||||
|
|
||||||
if (allUniqueAssets.isEmpty) {
|
if (allUniqueAssets.isEmpty) {
|
||||||
debugPrint("No Asset On Device");
|
debugPrint("No Asset On Device");
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
backupProgress: BackUpProgressEnum.idle,
|
backupProgress: BackUpProgressEnum.idle,
|
||||||
allAssetOnDatabase: allAssetOnDatabase,
|
allAssetsInDatabase: allAssetsInDatabase,
|
||||||
allUniqueAssets: {},
|
allUniqueAssets: {},
|
||||||
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
|
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
allAssetOnDatabase: allAssetOnDatabase,
|
allAssetsInDatabase: allAssetsInDatabase,
|
||||||
allUniqueAssets: allUniqueAssets,
|
allUniqueAssets: allUniqueAssets,
|
||||||
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
|
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
|
||||||
);
|
);
|
||||||
@@ -222,6 +249,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
// Save to persistent storage
|
// Save to persistent storage
|
||||||
_updatePersistentAlbumsSelection();
|
_updatePersistentAlbumsSelection();
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -229,10 +258,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
/// which albums are selected or excluded
|
/// which albums are selected or excluded
|
||||||
/// and then update the UI according to those information
|
/// and then update the UI according to those information
|
||||||
///
|
///
|
||||||
void getBackupInfo() async {
|
Future<void> getBackupInfo() async {
|
||||||
await getBackupAlbumsInfo();
|
await Future.wait([
|
||||||
_updateServerInfo();
|
_getBackupAlbumsInfo(),
|
||||||
_updateBackupAssetCount();
|
_updateServerInfo(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await _updateBackupAssetCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -255,11 +287,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
/// Invoke backup process
|
/// Invoke backup process
|
||||||
///
|
///
|
||||||
void startBackupProcess() async {
|
void startBackupProcess() async {
|
||||||
_updateServerInfo();
|
|
||||||
_updateBackupAssetCount();
|
|
||||||
|
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
|
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
|
||||||
|
|
||||||
|
await getBackupInfo();
|
||||||
|
|
||||||
var authResult = await PhotoManager.requestPermissionExtend();
|
var authResult = await PhotoManager.requestPermissionExtend();
|
||||||
if (authResult.isAuth) {
|
if (authResult.isAuth) {
|
||||||
await PhotoManager.clearFileCache();
|
await PhotoManager.clearFileCache();
|
||||||
@@ -270,10 +301,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<AssetEntity> assetsWillBeBackup = state.allUniqueAssets;
|
Set<AssetEntity> assetsWillBeBackup = Set.from(state.allUniqueAssets);
|
||||||
|
|
||||||
// Remove item that has already been backed up
|
// Remove item that has already been backed up
|
||||||
for (var assetId in state.allAssetOnDatabase) {
|
for (var assetId in state.allAssetsInDatabase) {
|
||||||
assetsWillBeBackup.removeWhere((e) => e.id == assetId);
|
assetsWillBeBackup.removeWhere((e) => e.id == assetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,33 +314,51 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
// Perform Backup
|
// Perform Backup
|
||||||
state = state.copyWith(cancelToken: CancellationToken());
|
state = state.copyWith(cancelToken: CancellationToken());
|
||||||
_backupService.backupAsset(assetsWillBeBackup, state.cancelToken,
|
_backupService.backupAsset(
|
||||||
_onAssetUploaded, _onUploadProgress);
|
assetsWillBeBackup,
|
||||||
|
state.cancelToken,
|
||||||
|
_onAssetUploaded,
|
||||||
|
_onUploadProgress,
|
||||||
|
_onSetCurrentBackupAsset,
|
||||||
|
_onBackupError,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
PhotoManager.openSetting();
|
PhotoManager.openSetting();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onBackupError(ErrorUploadAsset errorAssetInfo) {
|
||||||
|
ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
|
||||||
|
state = state.copyWith(currentUploadAsset: currentUploadAsset);
|
||||||
|
}
|
||||||
|
|
||||||
void cancelBackup() {
|
void cancelBackup() {
|
||||||
state.cancelToken.cancel();
|
state.cancelToken.cancel();
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
|
backupProgress: BackUpProgressEnum.idle,
|
||||||
|
progressInPercentage: 0.0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
||||||
state = state.copyWith(selectedAlbumsBackupAssetsIds: {
|
state = state.copyWith(
|
||||||
...state.selectedAlbumsBackupAssetsIds,
|
selectedAlbumsBackupAssetsIds: {
|
||||||
deviceAssetId
|
...state.selectedAlbumsBackupAssetsIds,
|
||||||
}, allAssetOnDatabase: [
|
deviceAssetId
|
||||||
...state.allAssetOnDatabase,
|
},
|
||||||
deviceAssetId
|
allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId],
|
||||||
]);
|
);
|
||||||
|
|
||||||
if (state.allUniqueAssets.length -
|
if (state.allUniqueAssets.length -
|
||||||
state.selectedAlbumsBackupAssetsIds.length ==
|
state.selectedAlbumsBackupAssetsIds.length ==
|
||||||
0) {
|
0) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
|
backupProgress: BackUpProgressEnum.done,
|
||||||
|
progressInPercentage: 0.0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateServerInfo();
|
_updateServerInfo();
|
||||||
@@ -317,59 +366,55 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
|
|
||||||
void _onUploadProgress(int sent, int total) {
|
void _onUploadProgress(int sent, int total) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
|
progressInPercentage: (sent.toDouble() / total.toDouble() * 100),
|
||||||
}
|
|
||||||
|
|
||||||
void _updateServerInfo() async {
|
|
||||||
var serverInfo = await _serverInfoService.getServerInfo();
|
|
||||||
|
|
||||||
// Update server info
|
|
||||||
state = state.copyWith(
|
|
||||||
serverInfo: ServerInfo(
|
|
||||||
diskSize: serverInfo.diskSize,
|
|
||||||
diskUse: serverInfo.diskUse,
|
|
||||||
diskAvailable: serverInfo.diskAvailable,
|
|
||||||
diskSizeRaw: serverInfo.diskSizeRaw,
|
|
||||||
diskUseRaw: serverInfo.diskUseRaw,
|
|
||||||
diskAvailableRaw: serverInfo.diskAvailableRaw,
|
|
||||||
diskUsagePercentage: serverInfo.diskUsagePercentage,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void resumeBackup() {
|
Future<void> _updateServerInfo() async {
|
||||||
var authState = ref?.read(authenticationProvider);
|
var serverInfo = await _serverInfoService.getServerInfo();
|
||||||
|
|
||||||
|
// Update server info
|
||||||
|
if (serverInfo != null) {
|
||||||
|
state = state.copyWith(
|
||||||
|
serverInfo: serverInfo,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void resumeBackup() {
|
||||||
// Check if user is login
|
// Check if user is login
|
||||||
var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
|
var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
|
||||||
|
|
||||||
// User has been logged out return
|
// User has been logged out return
|
||||||
if (authState != null) {
|
if (accessKey == null || !_authState.isAuthenticated) {
|
||||||
if (accessKey == null || !authState.isAuthenticated) {
|
debugPrint("[resumeBackup] not authenticated - abort");
|
||||||
debugPrint("[resumeBackup] not authenticated - abort");
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this device is enable backup by the user
|
||||||
|
if ((_authState.deviceInfo.deviceId == _authState.deviceId) &&
|
||||||
|
_authState.deviceInfo.isAutoBackup) {
|
||||||
|
// check if backup is alreayd in process - then return
|
||||||
|
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
||||||
|
debugPrint("[resumeBackup] Backup is already in progress - abort");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this device is enable backup by the user
|
// Run backup
|
||||||
if ((authState.deviceInfo.deviceId == authState.deviceId) &&
|
debugPrint("[resumeBackup] Start back up");
|
||||||
authState.deviceInfo.isAutoBackup) {
|
startBackupProcess();
|
||||||
// check if backup is alreayd in process - then return
|
|
||||||
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
|
||||||
debugPrint("[resumeBackup] Backup is already in progress - abort");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run backup
|
|
||||||
debugPrint("[resumeBackup] Start back up");
|
|
||||||
startBackupProcess();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final backupProvider =
|
final backupProvider =
|
||||||
StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
||||||
return BackupNotifier(ref: ref);
|
return BackupNotifier(
|
||||||
|
ref.watch(backupServiceProvider),
|
||||||
|
ref.watch(serverInfoServiceProvider),
|
||||||
|
ref.watch(authenticationProvider),
|
||||||
|
ref,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
|
||||||
|
|
||||||
|
class ErrorBackupListNotifier extends StateNotifier<Set<ErrorUploadAsset>> {
|
||||||
|
ErrorBackupListNotifier() : super({});
|
||||||
|
|
||||||
|
add(ErrorUploadAsset errorAsset) {
|
||||||
|
state = state.union({errorAsset});
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(ErrorUploadAsset errorAsset) {
|
||||||
|
state = state.difference({errorAsset});
|
||||||
|
}
|
||||||
|
|
||||||
|
empty() {
|
||||||
|
state = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final errorBackupListProvider =
|
||||||
|
StateNotifierProvider<ErrorBackupListNotifier, Set<ErrorUploadAsset>>(
|
||||||
|
(ref) => ErrorBackupListNotifier(),
|
||||||
|
);
|
||||||
@@ -2,38 +2,53 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive/hive.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/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/device_info.model.dart';
|
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.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:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:http_parser/http_parser.dart';
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:cancellation_token_http/http.dart' as http;
|
import 'package:cancellation_token_http/http.dart' as http;
|
||||||
|
|
||||||
class BackupService {
|
final backupServiceProvider = Provider(
|
||||||
final NetworkService _networkService = NetworkService();
|
(ref) => BackupService(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Future<List<String>> getDeviceBackupAsset() async {
|
class BackupService {
|
||||||
|
final ApiService _apiService;
|
||||||
|
BackupService(this._apiService);
|
||||||
|
|
||||||
|
Future<List<String>?> getDeviceBackupAsset() async {
|
||||||
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
|
|
||||||
Response response = await _networkService.getRequest(url: "asset/$deviceId");
|
try {
|
||||||
List<dynamic> result = jsonDecode(response.toString());
|
return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId);
|
||||||
|
} catch (e) {
|
||||||
return result.cast<String>();
|
debugPrint('Error [getDeviceBackupAsset] ${e.toString()}');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
backupAsset(Set<AssetEntity> assetList, http.CancellationToken cancelToken,
|
backupAsset(
|
||||||
Function(String, String) singleAssetDoneCb, Function(int, int) uploadProgress) async {
|
Set<AssetEntity> assetList,
|
||||||
|
http.CancellationToken cancelToken,
|
||||||
|
Function(String, String) singleAssetDoneCb,
|
||||||
|
Function(int, int) uploadProgressCb,
|
||||||
|
Function(CurrentUploadAsset) setCurrentUploadAssetCb,
|
||||||
|
Function(ErrorUploadAsset) errorCb,
|
||||||
|
) async {
|
||||||
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||||
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
File? file;
|
File? file;
|
||||||
|
|
||||||
http.MultipartFile? thumbnailUploadData;
|
|
||||||
|
|
||||||
for (var entity in assetList) {
|
for (var entity in assetList) {
|
||||||
try {
|
try {
|
||||||
if (entity.type == AssetType.video) {
|
if (entity.type == AssetType.video) {
|
||||||
@@ -44,7 +59,8 @@ class BackupService {
|
|||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
String originalFileName = await entity.titleAsync;
|
String originalFileName = await entity.titleAsync;
|
||||||
String fileNameWithoutPath = originalFileName.toString().split(".")[0];
|
String fileNameWithoutPath =
|
||||||
|
originalFileName.toString().split(".")[0];
|
||||||
var fileExtension = p.extension(file.path);
|
var fileExtension = p.extension(file.path);
|
||||||
var mimeType = FileHelper.getMimeType(file.path);
|
var mimeType = FileHelper.getMimeType(file.path);
|
||||||
var fileStream = file.openRead();
|
var fileStream = file.openRead();
|
||||||
@@ -59,24 +75,14 @@ class BackupService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build thumbnail multipart data
|
|
||||||
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(1440, 2560));
|
|
||||||
if (thumbnailData != null) {
|
|
||||||
thumbnailUploadData = http.MultipartFile.fromBytes(
|
|
||||||
"thumbnailData",
|
|
||||||
List.from(thumbnailData),
|
|
||||||
filename: fileNameWithoutPath,
|
|
||||||
contentType: MediaType(
|
|
||||||
"image",
|
|
||||||
"jpeg",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
|
|
||||||
var req = MultipartRequest('POST', Uri.parse('$savedEndpoint/asset/upload'),
|
var req = MultipartRequest(
|
||||||
onProgress: ((bytes, totalBytes) => uploadProgress(bytes, totalBytes)));
|
'POST',
|
||||||
|
Uri.parse('$savedEndpoint/asset/upload'),
|
||||||
|
onProgress: ((bytes, totalBytes) =>
|
||||||
|
uploadProgressCb(bytes, totalBytes)),
|
||||||
|
);
|
||||||
req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}";
|
req.headers["Authorization"] = "Bearer ${box.get(accessTokenKey)}";
|
||||||
|
|
||||||
req.fields['deviceAssetId'] = entity.id;
|
req.fields['deviceAssetId'] = entity.id;
|
||||||
@@ -88,15 +94,40 @@ class BackupService {
|
|||||||
req.fields['fileExtension'] = fileExtension;
|
req.fields['fileExtension'] = fileExtension;
|
||||||
req.fields['duration'] = entity.videoDuration.toString();
|
req.fields['duration'] = entity.videoDuration.toString();
|
||||||
|
|
||||||
if (thumbnailUploadData != null) {
|
|
||||||
req.files.add(thumbnailUploadData);
|
|
||||||
}
|
|
||||||
req.files.add(assetRawUploadData);
|
req.files.add(assetRawUploadData);
|
||||||
|
|
||||||
var res = await req.send(cancellationToken: cancelToken);
|
setCurrentUploadAssetCb(
|
||||||
|
CurrentUploadAsset(
|
||||||
|
id: entity.id,
|
||||||
|
createdAt: entity.createDateTime,
|
||||||
|
fileName: originalFileName,
|
||||||
|
fileType: _getAssetType(entity.type),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (res.statusCode == 201) {
|
var response = await req.send(cancellationToken: cancelToken);
|
||||||
|
|
||||||
|
if (response.statusCode == 201) {
|
||||||
singleAssetDoneCb(entity.id, deviceId);
|
singleAssetDoneCb(entity.id, deviceId);
|
||||||
|
} else {
|
||||||
|
var data = await response.stream.bytesToString();
|
||||||
|
var error = jsonDecode(data);
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
"Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}",
|
||||||
|
);
|
||||||
|
|
||||||
|
errorCb(
|
||||||
|
ErrorUploadAsset(
|
||||||
|
asset: entity,
|
||||||
|
id: entity.id,
|
||||||
|
createdAt: entity.createDateTime,
|
||||||
|
fileName: originalFileName,
|
||||||
|
fileType: _getAssetType(entity.type),
|
||||||
|
errorMessage: error['error'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} on http.CancelledException {
|
} on http.CancelledException {
|
||||||
@@ -126,14 +157,29 @@ class BackupService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DeviceInfoRemote> setAutoBackup(bool status, String deviceId, String deviceType) async {
|
Future<DeviceInfoResponseDto> setAutoBackup(
|
||||||
var res = await _networkService.patchRequest(url: 'device-info', data: {
|
bool status,
|
||||||
"isAutoBackup": status,
|
String deviceId,
|
||||||
"deviceId": deviceId,
|
DeviceTypeEnum deviceType,
|
||||||
"deviceType": deviceType,
|
) async {
|
||||||
});
|
try {
|
||||||
|
var updatedDeviceInfo = await _apiService.deviceInfoApi.updateDeviceInfo(
|
||||||
|
UpdateDeviceInfoDto(
|
||||||
|
deviceId: deviceId,
|
||||||
|
deviceType: deviceType,
|
||||||
|
isAutoBackup: status,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return DeviceInfoRemote.fromJson(res.toString());
|
if (updatedDeviceInfo == null) {
|
||||||
|
throw Exception("Error updating device info");
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedDeviceInfo;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error setAutoBackup: ${e.toString()}");
|
||||||
|
throw Error();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
@@ -14,16 +15,24 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
final Uint8List? imageData;
|
final Uint8List? imageData;
|
||||||
final AssetPathEntity albumInfo;
|
final AssetPathEntity albumInfo;
|
||||||
|
|
||||||
const AlbumInfoCard({Key? key, this.imageData, required this.albumInfo}) : super(key: key);
|
const AlbumInfoCard({Key? key, this.imageData, required this.albumInfo})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
|
final bool isSelected =
|
||||||
final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
|
||||||
|
final bool isExcluded =
|
||||||
|
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
||||||
|
|
||||||
ColorFilter selectedFilter = ColorFilter.mode(Theme.of(context).primaryColor.withAlpha(100), BlendMode.darken);
|
ColorFilter selectedFilter = ColorFilter.mode(
|
||||||
ColorFilter excludedFilter = ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
|
Theme.of(context).primaryColor.withAlpha(100),
|
||||||
ColorFilter unselectedFilter = const ColorFilter.mode(Colors.black, BlendMode.color);
|
BlendMode.darken,
|
||||||
|
);
|
||||||
|
ColorFilter excludedFilter =
|
||||||
|
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
|
||||||
|
ColorFilter unselectedFilter =
|
||||||
|
const ColorFilter.mode(Colors.black, BlendMode.color);
|
||||||
|
|
||||||
_buildSelectedTextBox() {
|
_buildSelectedTextBox() {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
@@ -31,9 +40,13 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
"INCLUDED",
|
"album_info_card_backup_album_included",
|
||||||
style: TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
),
|
fontSize: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
);
|
);
|
||||||
} else if (isExcluded) {
|
} else if (isExcluded) {
|
||||||
@@ -41,14 +54,18 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
"EXCLUDED",
|
"album_info_card_backup_album_excluded",
|
||||||
style: TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
),
|
fontSize: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
backgroundColor: Colors.red[300],
|
backgroundColor: Colors.red[300],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildImageFilter() {
|
_buildImageFilter() {
|
||||||
@@ -69,7 +86,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
|
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "Cannot remove the only album",
|
msg: "backup_err_only_album".tr(),
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
@@ -85,20 +102,27 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
|
|
||||||
if (isExcluded) {
|
if (isExcluded) {
|
||||||
ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(albumInfo);
|
ref
|
||||||
|
.watch(backupProvider.notifier)
|
||||||
|
.removeExcludedAlbumForBackup(albumInfo);
|
||||||
} else {
|
} else {
|
||||||
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
|
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
|
||||||
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo)) {
|
ref
|
||||||
|
.watch(backupProvider)
|
||||||
|
.selectedBackupAlbums
|
||||||
|
.contains(albumInfo)) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "Cannot exclude the only album",
|
msg: "backup_err_only_album".tr(),
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.watch(backupProvider.notifier).addExcludedAlbumForBackup(albumInfo);
|
ref
|
||||||
|
.watch(backupProvider.notifier)
|
||||||
|
.addExcludedAlbumForBackup(albumInfo);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Card(
|
child: Card(
|
||||||
@@ -121,18 +145,27 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
width: 200,
|
width: 200,
|
||||||
height: 200,
|
height: 200,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)),
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(12),
|
||||||
|
topRight: Radius.circular(12),
|
||||||
|
),
|
||||||
image: DecorationImage(
|
image: DecorationImage(
|
||||||
colorFilter: _buildImageFilter(),
|
colorFilter: _buildImageFilter(),
|
||||||
image: imageData != null
|
image: imageData != null
|
||||||
? MemoryImage(imageData!)
|
? MemoryImage(imageData!)
|
||||||
: const AssetImage('assets/immich-logo-no-outline.png') as ImageProvider,
|
: const AssetImage(
|
||||||
|
'assets/immich-logo-no-outline.png',
|
||||||
|
) as ImageProvider,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: null,
|
child: null,
|
||||||
),
|
),
|
||||||
Positioned(bottom: 10, left: 25, child: _buildSelectedTextBox())
|
Positioned(
|
||||||
|
bottom: 10,
|
||||||
|
left: 25,
|
||||||
|
child: _buildSelectedTextBox(),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
@@ -150,13 +183,22 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
Text(
|
Text(
|
||||||
albumInfo.name,
|
albumInfo.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14, color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
|
fontSize: 14,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 2.0),
|
padding: const EdgeInsets.only(top: 2.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
albumInfo.assetCount.toString() + (albumInfo.isAll ? " (ALL)" : ""),
|
albumInfo.assetCount.toString() +
|
||||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
(albumInfo.isAll
|
||||||
|
? " (${'backup_all'.tr()})"
|
||||||
|
: ""),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -165,7 +207,8 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).push(AlbumPreviewRoute(album: albumInfo));
|
AutoRouter.of(context)
|
||||||
|
.push(AlbumPreviewRoute(album: albumInfo));
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.image_outlined,
|
Icons.image_outlined,
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class BackupInfoCard extends StatelessWidget {
|
class BackupInfoCard extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
final String info;
|
final String info;
|
||||||
const BackupInfoCard({Key? key, required this.title, required this.subtitle, required this.info}) : super(key: key);
|
const BackupInfoCard({
|
||||||
|
Key? key,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.info,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -39,7 +45,7 @@ class BackupInfoCard extends StatelessWidget {
|
|||||||
info,
|
info,
|
||||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const Text("assets"),
|
const Text("backup_info_card_assets").tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -16,13 +16,17 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
|||||||
final assets = useState<List<AssetEntity>>([]);
|
final assets = useState<List<AssetEntity>>([]);
|
||||||
|
|
||||||
_getAssetsInAlbum() async {
|
_getAssetsInAlbum() async {
|
||||||
assets.value = await album.getAssetListRange(start: 0, end: album.assetCount);
|
assets.value =
|
||||||
|
await album.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
_getAssetsInAlbum();
|
() {
|
||||||
return null;
|
_getAssetsInAlbum();
|
||||||
}, []);
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -37,7 +41,11 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(top: 4.0),
|
padding: const EdgeInsets.only(top: 4.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"ID ${album.id}",
|
"ID ${album.id}",
|
||||||
style: TextStyle(fontSize: 10, color: Colors.grey[600], fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -56,7 +64,10 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
|||||||
itemCount: assets.value.length,
|
itemCount: assets.value.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
Future<Uint8List?> thumbData =
|
Future<Uint8List?> thumbData =
|
||||||
assets.value[index].thumbnailDataWithSize(const ThumbnailSize(200, 200), quality: 50);
|
assets.value[index].thumbnailDataWithSize(
|
||||||
|
const ThumbnailSize(200, 200),
|
||||||
|
quality: 50,
|
||||||
|
);
|
||||||
|
|
||||||
return FutureBuilder<Uint8List?>(
|
return FutureBuilder<Uint8List?>(
|
||||||
future: thumbData,
|
future: thumbData,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
@@ -16,10 +17,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
|
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
|
||||||
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
|
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
ref.read(backupProvider.notifier).getBackupAlbumsInfo();
|
() {
|
||||||
return null;
|
ref.read(backupProvider.notifier).getBackupInfo();
|
||||||
}, []);
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
_buildAlbumSelectionList() {
|
_buildAlbumSelectionList() {
|
||||||
if (availableAlbums.isEmpty) {
|
if (availableAlbums.isEmpty) {
|
||||||
@@ -37,8 +41,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
itemBuilder: ((context, index) {
|
itemBuilder: ((context, index) {
|
||||||
var thumbnailData = availableAlbums[index].thumbnailData;
|
var thumbnailData = availableAlbums[index].thumbnailData;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: index == 0 ? const EdgeInsets.only(left: 16.00) : const EdgeInsets.all(0),
|
padding: index == 0
|
||||||
child: AlbumInfoCard(imageData: thumbnailData, albumInfo: availableAlbums[index].albumEntity),
|
? const EdgeInsets.only(left: 16.00)
|
||||||
|
: const EdgeInsets.all(0),
|
||||||
|
child: AlbumInfoCard(
|
||||||
|
imageData: thumbnailData,
|
||||||
|
albumInfo: availableAlbums[index].albumEntity,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -51,7 +60,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
|
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "Cannot remove the only album",
|
msg: "backup_err_only_album".tr(),
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
@@ -67,10 +76,16 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
onTap: removeSelection,
|
onTap: removeSelection,
|
||||||
child: Chip(
|
child: Chip(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
album.name,
|
album.name,
|
||||||
style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
deleteIconColor: Colors.white,
|
deleteIconColor: Colors.white,
|
||||||
@@ -88,7 +103,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
_buildExcludedAlbumNameChip() {
|
_buildExcludedAlbumNameChip() {
|
||||||
return excludedBackupAlbums.map((album) {
|
return excludedBackupAlbums.map((album) {
|
||||||
void removeSelection() {
|
void removeSelection() {
|
||||||
ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(album);
|
ref
|
||||||
|
.watch(backupProvider.notifier)
|
||||||
|
.removeExcludedAlbumForBackup(album);
|
||||||
}
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
@@ -97,10 +114,16 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.only(right: 8.0),
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
child: Chip(
|
child: Chip(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
album.name,
|
album.name,
|
||||||
style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.red[300],
|
backgroundColor: Colors.red[300],
|
||||||
deleteIconColor: Colors.white,
|
deleteIconColor: Colors.white,
|
||||||
@@ -122,27 +145,31 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Select Albums",
|
"backup_album_selection_page_select_albums",
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
physics: const ClampingScrollPhysics(),
|
physics: const ClampingScrollPhysics(),
|
||||||
children: [
|
children: [
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
padding:
|
||||||
child: Text(
|
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||||
"Selection Info",
|
child: const Text(
|
||||||
|
"backup_album_selection_page_selection_info",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
// Selected Album Chips
|
// Selected Album Chips
|
||||||
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
children: [..._buildSelectedAlbumNameChip(), ..._buildExcludedAlbumNameChip()],
|
children: [
|
||||||
|
..._buildSelectedAlbumNameChip(),
|
||||||
|
..._buildExcludedAlbumNameChip()
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -164,11 +191,19 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
ListTile(
|
ListTile(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
title: Text(
|
title: Text(
|
||||||
"Total unique assets",
|
"backup_album_selection_page_total_assets",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.grey[700]),
|
style: TextStyle(
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
ref.watch(backupProvider).allUniqueAssets.length.toString(),
|
ref
|
||||||
|
.watch(backupProvider)
|
||||||
|
.allUniqueAssets
|
||||||
|
.length
|
||||||
|
.toString(),
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -179,19 +214,20 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
"Albums on device (${availableAlbums.length.toString()})",
|
"backup_album_selection_page_albums_device"
|
||||||
|
.tr(args: [availableAlbums.length.toString()]),
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
),
|
),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Tap to include, double tap to exclude",
|
"backup_album_selection_page_albums_tap",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
splashRadius: 16,
|
splashRadius: 16,
|
||||||
@@ -206,23 +242,28 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
title: Text(
|
title: Text(
|
||||||
'Selection Info',
|
'backup_album_selection_page_selection_info',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
),
|
).tr(),
|
||||||
content: SingleChildScrollView(
|
content: SingleChildScrollView(
|
||||||
child: ListBody(
|
child: ListBody(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.',
|
'backup_album_selection_page_assets_scatter',
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
style: TextStyle(
|
||||||
),
|
fontSize: 14,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.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/backup/models/backup_state.model.dart';
|
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
@@ -24,16 +26,19 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
? false
|
? false
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
|
() {
|
||||||
ref.read(backupProvider.notifier).getBackupInfo();
|
if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
|
||||||
}
|
ref.watch(backupProvider.notifier).getBackupInfo();
|
||||||
|
}
|
||||||
|
|
||||||
ref
|
ref
|
||||||
.watch(websocketProvider.notifier)
|
.watch(websocketProvider.notifier)
|
||||||
.stopListenToEvent('on_upload_success');
|
.stopListenToEvent('on_upload_success');
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildStorageInformation() {
|
Widget _buildStorageInformation() {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
@@ -42,9 +47,9 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Server Storage",
|
"backup_controller_page_server_storage",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
),
|
).tr(),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -56,7 +61,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||||
barRadius: const Radius.circular(2),
|
barRadius: const Radius.circular(2),
|
||||||
lineHeight: 6.0,
|
lineHeight: 10.0,
|
||||||
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
|
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
|
||||||
backgroundColor: Colors.grey,
|
backgroundColor: Colors.grey,
|
||||||
progressColor: Theme.of(context).primaryColor,
|
progressColor: Theme.of(context).primaryColor,
|
||||||
@@ -64,8 +69,12 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 12.0),
|
padding: const EdgeInsets.only(top: 12.0),
|
||||||
child: Text(
|
child: const Text('backup_controller_page_storage_format').tr(
|
||||||
'${backupState.serverInfo.diskUse} of ${backupState.serverInfo.diskSize} used'),
|
args: [
|
||||||
|
backupState.serverInfo.diskUse,
|
||||||
|
backupState.serverInfo.diskSize
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -74,11 +83,13 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ListTile _buildBackupController() {
|
ListTile _buildBackupController() {
|
||||||
var backUpOption =
|
var backUpOption = authenticationState.deviceInfo.isAutoBackup
|
||||||
authenticationState.deviceInfo.isAutoBackup ? "on" : "off";
|
? "backup_controller_page_status_on".tr()
|
||||||
|
: "backup_controller_page_status_off".tr();
|
||||||
var isAutoBackup = authenticationState.deviceInfo.isAutoBackup;
|
var isAutoBackup = authenticationState.deviceInfo.isAutoBackup;
|
||||||
var backupBtnText =
|
var backupBtnText = authenticationState.deviceInfo.isAutoBackup
|
||||||
authenticationState.deviceInfo.isAutoBackup ? "off" : "on";
|
? "backup_controller_page_turn_off".tr()
|
||||||
|
: "backup_controller_page_turn_on".tr();
|
||||||
return ListTile(
|
return ListTile(
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
leading: isAutoBackup
|
leading: isAutoBackup
|
||||||
@@ -88,7 +99,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
: const Icon(Icons.cloud_off_rounded),
|
: const Icon(Icons.cloud_off_rounded),
|
||||||
title: Text(
|
title: Text(
|
||||||
"Back up is $backUpOption",
|
backUpOption,
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
),
|
),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
@@ -96,12 +107,11 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
!isAutoBackup
|
if (!isAutoBackup)
|
||||||
? const Text(
|
const Text(
|
||||||
"Turn on backup to automatically upload new assets to the server.",
|
"backup_controller_page_desc_backup",
|
||||||
style: TextStyle(fontSize: 14),
|
style: TextStyle(fontSize: 14),
|
||||||
)
|
).tr(),
|
||||||
: Container(),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
@@ -112,16 +122,20 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
isAutoBackup
|
if (isAutoBackup) {
|
||||||
? ref
|
ref
|
||||||
.watch(authenticationProvider.notifier)
|
.read(authenticationProvider.notifier)
|
||||||
.setAutoBackup(false)
|
.setAutoBackup(false);
|
||||||
: ref
|
} else {
|
||||||
.watch(authenticationProvider.notifier)
|
ref
|
||||||
.setAutoBackup(true);
|
.read(authenticationProvider.notifier)
|
||||||
|
.setAutoBackup(true);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Text("Turn $backupBtnText Backup",
|
child: Text(
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
backupBtnText,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -131,13 +145,13 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSelectedAlbumName() {
|
Widget _buildSelectedAlbumName() {
|
||||||
var text = "Selected: ";
|
var text = "backup_controller_page_backup_selected".tr();
|
||||||
var albums = ref.watch(backupProvider).selectedBackupAlbums;
|
var albums = ref.watch(backupProvider).selectedBackupAlbums;
|
||||||
|
|
||||||
if (albums.isNotEmpty) {
|
if (albums.isNotEmpty) {
|
||||||
for (var album in albums) {
|
for (var album in albums) {
|
||||||
if (album.name == "Recent" || album.name == "Recents") {
|
if (album.name == "Recent" || album.name == "Recents") {
|
||||||
text += "${album.name} (All), ";
|
text += "${album.name} (${'backup_all'.tr()}), ";
|
||||||
} else {
|
} else {
|
||||||
text += "${album.name}, ";
|
text += "${album.name}, ";
|
||||||
}
|
}
|
||||||
@@ -148,27 +162,29 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
text.trim().substring(0, text.length - 2),
|
text.trim().substring(0, text.length - 2),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"None selected",
|
"backup_controller_page_none_selected".tr(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildExcludedAlbumName() {
|
Widget _buildExcludedAlbumName() {
|
||||||
var text = "Excluded: ";
|
var text = "backup_controller_page_excluded".tr();
|
||||||
var albums = ref.watch(backupProvider).excludedBackupAlbums;
|
var albums = ref.watch(backupProvider).excludedBackupAlbums;
|
||||||
|
|
||||||
if (albums.isNotEmpty) {
|
if (albums.isNotEmpty) {
|
||||||
@@ -181,13 +197,14 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
text.trim().substring(0, text.length - 2),
|
text.trim().substring(0, text.length - 2),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.red[300],
|
color: Colors.red[300],
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,17 +221,19 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
borderOnForeground: false,
|
borderOnForeground: false,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
minVerticalPadding: 15,
|
minVerticalPadding: 15,
|
||||||
title: const Text("Backup Albums",
|
title: const Text(
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
"backup_controller_page_albums",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||||
|
).tr(),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
"Albums to be backup",
|
"backup_controller_page_to_backup",
|
||||||
style: TextStyle(color: Color(0xFF808080), fontSize: 12),
|
style: TextStyle(color: Color(0xFF808080), fontSize: 12),
|
||||||
),
|
).tr(),
|
||||||
_buildSelectedAlbumName(),
|
_buildSelectedAlbumName(),
|
||||||
_buildExcludedAlbumName()
|
_buildExcludedAlbumName()
|
||||||
],
|
],
|
||||||
@@ -231,65 +250,215 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
|
AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
|
||||||
},
|
},
|
||||||
child: const Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 16.0,
|
vertical: 16.0,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: const Text(
|
||||||
"Select",
|
"backup_controller_page_select",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buildCurrentBackupAssetInfoCard() {
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
Icons.info_outline_rounded,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"backup_controller_page_uploading_file_info",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
|
).tr(),
|
||||||
|
if (ref.watch(errorBackupListProvider).isNotEmpty)
|
||||||
|
ActionChip(
|
||||||
|
avatar: Icon(
|
||||||
|
Icons.info,
|
||||||
|
size: 24,
|
||||||
|
color: Colors.red[400],
|
||||||
|
),
|
||||||
|
elevation: 1,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
label: Text(
|
||||||
|
"backup_controller_page_failed",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red[400],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
).tr(
|
||||||
|
args: [ref.watch(errorBackupListProvider).length.toString()],
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
onPressed: () {
|
||||||
|
AutoRouter.of(context).push(const FailedBackupStatusRoute());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: LinearPercentIndicator(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||||
|
barRadius: const Radius.circular(2),
|
||||||
|
lineHeight: 10.0,
|
||||||
|
trailing: Text(
|
||||||
|
" ${backupState.progressInPercentage.toStringAsFixed(0)}%",
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
percent: backupState.progressInPercentage / 100.0,
|
||||||
|
backgroundColor: Colors.grey,
|
||||||
|
progressColor: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Table(
|
||||||
|
border: TableBorder.all(
|
||||||
|
color: Colors.black12,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TableRow(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TableCell(
|
||||||
|
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: const Text(
|
||||||
|
'backup_controller_page_filename',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 10.0,
|
||||||
|
),
|
||||||
|
).tr(
|
||||||
|
args: [
|
||||||
|
backupState.currentUploadAsset.fileName,
|
||||||
|
backupState.currentUploadAsset.fileType
|
||||||
|
.toLowerCase()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TableRow(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TableCell(
|
||||||
|
verticalAlignment: TableCellVerticalAlignment.middle,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: const Text(
|
||||||
|
"backup_controller_page_created",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 10.0,
|
||||||
|
),
|
||||||
|
).tr(
|
||||||
|
args: [
|
||||||
|
DateFormat.yMMMMd('en_US').format(
|
||||||
|
DateTime.parse(
|
||||||
|
backupState.currentUploadAsset.createdAt
|
||||||
|
.toString(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
TableRow(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TableCell(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(6.0),
|
||||||
|
child: const Text(
|
||||||
|
"backup_controller_page_id",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 10.0,
|
||||||
|
),
|
||||||
|
).tr(args: [backupState.currentUploadAsset.id]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void startBackup() {
|
||||||
|
ref.watch(errorBackupListProvider.notifier).empty();
|
||||||
|
ref.watch(backupProvider.notifier).startBackupProcess();
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Backup",
|
"backup_controller_page_backup",
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.watch(websocketProvider.notifier).listenUploadEvent();
|
ref.watch(websocketProvider.notifier).listenUploadEvent();
|
||||||
AutoRouter.of(context).pop(true);
|
AutoRouter.of(context).pop(true);
|
||||||
},
|
},
|
||||||
splashRadius: 24,
|
splashRadius: 24,
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.arrow_back_ios_rounded,
|
Icons.arrow_back_ios_rounded,
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 32),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: const Text(
|
||||||
"Backup Information",
|
"backup_controller_page_info",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
_buildFolderSelectionTile(),
|
_buildFolderSelectionTile(),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Total",
|
title: "backup_controller_page_total".tr(),
|
||||||
subtitle: "All unique photos and videos from selected albums",
|
subtitle: "backup_controller_page_total_sub".tr(),
|
||||||
info: "${backupState.allUniqueAssets.length}",
|
info: "${backupState.allUniqueAssets.length}",
|
||||||
),
|
),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Backup",
|
title: "backup_controller_page_backup".tr(),
|
||||||
subtitle:
|
subtitle: "backup_controller_page_backup_sub".tr(),
|
||||||
"Photos and videos from selected albums that are backup",
|
|
||||||
info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
|
info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
|
||||||
),
|
),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Remainder",
|
title: "backup_controller_page_remainder".tr(),
|
||||||
subtitle:
|
subtitle: "backup_controller_page_remainder_sub".tr(),
|
||||||
"Photos and videos that has not been backing up from selected albums",
|
|
||||||
info:
|
info:
|
||||||
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
|
"${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
|
||||||
),
|
),
|
||||||
@@ -298,23 +467,11 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
const Divider(),
|
const Divider(),
|
||||||
_buildStorageInformation(),
|
_buildStorageInformation(),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
|
_buildCurrentBackupAssetInfoCard(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.only(
|
||||||
child: Text(
|
top: 24,
|
||||||
"Asset that were being backup: ${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length} [${backupState.progressInPercentage.toStringAsFixed(0)}%]"),
|
),
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
|
||||||
child: Row(children: [
|
|
||||||
const Text("Backup Progress:"),
|
|
||||||
const Padding(padding: EdgeInsets.symmetric(horizontal: 2)),
|
|
||||||
backupState.backupProgress == BackUpProgressEnum.inProgress
|
|
||||||
? const CircularProgressIndicator.adaptive()
|
|
||||||
: const Text("Done"),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
child:
|
child:
|
||||||
backupState.backupProgress == BackUpProgressEnum.inProgress
|
backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||||
@@ -322,25 +479,33 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
primary: Colors.red[300],
|
primary: Colors.red[300],
|
||||||
onPrimary: Colors.grey[50],
|
onPrimary: Colors.grey[50],
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(backupProvider.notifier).cancelBackup();
|
ref.read(backupProvider.notifier).cancelBackup();
|
||||||
},
|
},
|
||||||
child: const Text("Cancel"),
|
child: const Text(
|
||||||
|
"backup_controller_page_cancel",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
)
|
)
|
||||||
: ElevatedButton(
|
: ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
primary: Theme.of(context).primaryColor,
|
primary: Theme.of(context).primaryColor,
|
||||||
onPrimary: Colors.grey[50],
|
onPrimary: Colors.grey[50],
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
),
|
),
|
||||||
onPressed: shouldBackup
|
onPressed: shouldBackup ? startBackup : null,
|
||||||
? () {
|
child: const Text(
|
||||||
ref
|
"backup_controller_page_start_backup",
|
||||||
.read(backupProvider.notifier)
|
style: TextStyle(
|
||||||
.startBackupProcess();
|
fontSize: 14,
|
||||||
}
|
fontWeight: FontWeight.bold,
|
||||||
: null,
|
),
|
||||||
child: const Text("Start Backup"),
|
).tr(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
141
mobile/lib/modules/backup/views/failed_backup_status_page.dart
Normal file
141
mobile/lib/modules/backup/views/failed_backup_status_page.dart
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
class FailedBackupStatusPage extends HookConsumerWidget {
|
||||||
|
const FailedBackupStatusPage({Key? key}) : super(key: key);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final errorBackupList = ref.watch(errorBackupListProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
title: Text(
|
||||||
|
"Failed Backup (${errorBackupList.length})",
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
AutoRouter.of(context).pop(true);
|
||||||
|
},
|
||||||
|
splashRadius: 24,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back_ios_rounded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: errorBackupList.length,
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
var errorAsset = errorBackupList.elementAt(index);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12.0,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
child: Card(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15), // if you need this
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Colors.black12,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 100,
|
||||||
|
minHeight: 150,
|
||||||
|
maxWidth: 100,
|
||||||
|
maxHeight: 200,
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(15),
|
||||||
|
topLeft: Radius.circular(15),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: Image(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
image: AssetEntityImageProvider(
|
||||||
|
errorAsset.asset,
|
||||||
|
isOriginal: false,
|
||||||
|
thumbnailSize: const ThumbnailSize.square(512),
|
||||||
|
thumbnailFormat: ThumbnailFormat.jpeg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
DateFormat.yMMMMd('en_US').format(
|
||||||
|
DateTime.parse(
|
||||||
|
errorAsset.createdAt.toString(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.error,
|
||||||
|
color: Colors.red.withAlpha(200),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Text(
|
||||||
|
errorAsset.fileName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
errorAsset.errorMessage,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey[800],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class DeleteAssetResponse {
|
|
||||||
final String id;
|
|
||||||
final String status;
|
|
||||||
|
|
||||||
DeleteAssetResponse({
|
|
||||||
required this.id,
|
|
||||||
required this.status,
|
|
||||||
});
|
|
||||||
|
|
||||||
DeleteAssetResponse copyWith({
|
|
||||||
String? id,
|
|
||||||
String? status,
|
|
||||||
}) {
|
|
||||||
return DeleteAssetResponse(
|
|
||||||
id: id ?? this.id,
|
|
||||||
status: status ?? this.status,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'status': status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory DeleteAssetResponse.fromMap(Map<String, dynamic> map) {
|
|
||||||
return DeleteAssetResponse(
|
|
||||||
id: map['id'] ?? '',
|
|
||||||
status: map['status'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory DeleteAssetResponse.fromJson(String source) => DeleteAssetResponse.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'DeleteAssetResponse(id: $id, status: $status)';
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is DeleteAssetResponse && other.id == id && other.status == status;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => id.hashCode ^ status.hashCode;
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class ImmichAssetGroupByDate {
|
class ImmichAssetGroupByDate {
|
||||||
final String date;
|
final String date;
|
||||||
List<ImmichAsset> assets;
|
List<AssetResponseDto> assets;
|
||||||
ImmichAssetGroupByDate({
|
ImmichAssetGroupByDate({
|
||||||
required this.date,
|
required this.date,
|
||||||
required this.assets,
|
required this.assets,
|
||||||
@@ -13,7 +11,7 @@ class ImmichAssetGroupByDate {
|
|||||||
|
|
||||||
ImmichAssetGroupByDate copyWith({
|
ImmichAssetGroupByDate copyWith({
|
||||||
String? date,
|
String? date,
|
||||||
List<ImmichAsset>? assets,
|
List<AssetResponseDto>? assets,
|
||||||
}) {
|
}) {
|
||||||
return ImmichAssetGroupByDate(
|
return ImmichAssetGroupByDate(
|
||||||
date: date ?? this.date,
|
date: date ?? this.date,
|
||||||
@@ -21,24 +19,6 @@ class ImmichAssetGroupByDate {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'date': date,
|
|
||||||
'assets': assets.map((x) => x.toMap()).toList(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory ImmichAssetGroupByDate.fromMap(Map<String, dynamic> map) {
|
|
||||||
return ImmichAssetGroupByDate(
|
|
||||||
date: map['date'] ?? '',
|
|
||||||
assets: List<ImmichAsset>.from(map['assets']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory ImmichAssetGroupByDate.fromJson(String source) => ImmichAssetGroupByDate.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ImmichAssetGroupByDate(date: $date, assets: $assets)';
|
String toString() => 'ImmichAssetGroupByDate(date: $date, assets: $assets)';
|
||||||
|
|
||||||
@@ -46,7 +26,9 @@ class ImmichAssetGroupByDate {
|
|||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
return other is ImmichAssetGroupByDate && other.date == date && listEquals(other.assets, assets);
|
return other is ImmichAssetGroupByDate &&
|
||||||
|
other.date == date &&
|
||||||
|
listEquals(other.assets, assets);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -75,28 +57,9 @@ class GetAllAssetResponse {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'count': count,
|
|
||||||
'data': data.map((x) => x.toMap()).toList(),
|
|
||||||
'nextPageKey': nextPageKey,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory GetAllAssetResponse.fromMap(Map<String, dynamic> map) {
|
|
||||||
return GetAllAssetResponse(
|
|
||||||
count: map['count']?.toInt() ?? 0,
|
|
||||||
data: List<ImmichAssetGroupByDate>.from(map['data']?.map((x) => ImmichAssetGroupByDate.fromMap(x))),
|
|
||||||
nextPageKey: map['nextPageKey'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory GetAllAssetResponse.fromJson(String source) => GetAllAssetResponse.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)';
|
String toString() =>
|
||||||
|
'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class HomePageState {
|
class HomePageState {
|
||||||
final bool isMultiSelectEnable;
|
final bool isMultiSelectEnable;
|
||||||
final Set<ImmichAsset> selectedItems;
|
final Set<AssetResponseDto> selectedItems;
|
||||||
final Set<String> selectedDateGroup;
|
final Set<String> selectedDateGroup;
|
||||||
HomePageState({
|
HomePageState({
|
||||||
required this.isMultiSelectEnable,
|
required this.isMultiSelectEnable,
|
||||||
@@ -16,7 +14,7 @@ class HomePageState {
|
|||||||
|
|
||||||
HomePageState copyWith({
|
HomePageState copyWith({
|
||||||
bool? isMultiSelectEnable,
|
bool? isMultiSelectEnable,
|
||||||
Set<ImmichAsset>? selectedItems,
|
Set<AssetResponseDto>? selectedItems,
|
||||||
Set<String>? selectedDateGroup,
|
Set<String>? selectedDateGroup,
|
||||||
}) {
|
}) {
|
||||||
return HomePageState(
|
return HomePageState(
|
||||||
@@ -26,26 +24,6 @@ class HomePageState {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'isMultiSelectEnable': isMultiSelectEnable,
|
|
||||||
'selectedItems': selectedItems.map((x) => x.toMap()).toList(),
|
|
||||||
'selectedDateGroup': selectedDateGroup.toList(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory HomePageState.fromMap(Map<String, dynamic> map) {
|
|
||||||
return HomePageState(
|
|
||||||
isMultiSelectEnable: map['isMultiSelectEnable'] ?? false,
|
|
||||||
selectedItems: Set<ImmichAsset>.from(map['selectedItems']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
selectedDateGroup: Set<String>.from(map['selectedDateGroup']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory HomePageState.fromJson(String source) => HomePageState.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)';
|
'HomePageState(isMultiSelectEnable: $isMultiSelectEnable, selectedItems: $selectedItems, selectedDateGroup: $selectedDateGroup)';
|
||||||
@@ -62,5 +40,8 @@ class HomePageState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => isMultiSelectEnable.hashCode ^ selectedItems.hashCode ^ selectedDateGroup.hashCode;
|
int get hashCode =>
|
||||||
|
isMultiSelectEnable.hashCode ^
|
||||||
|
selectedItems.hashCode ^
|
||||||
|
selectedDateGroup.hashCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
||||||
HomePageStateNotifier()
|
HomePageStateNotifier()
|
||||||
@@ -13,7 +13,9 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
void addSelectedDateGroup(String dateGroupTitle) {
|
void addSelectedDateGroup(String dateGroupTitle) {
|
||||||
state = state.copyWith(selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle});
|
state = state.copyWith(
|
||||||
|
selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSelectedDateGroup(String dateGroupTitle) {
|
void removeSelectedDateGroup(String dateGroupTitle) {
|
||||||
@@ -24,34 +26,39 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
|||||||
state = state.copyWith(selectedDateGroup: currentDateGroup);
|
state = state.copyWith(selectedDateGroup: currentDateGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
void enableMultiSelect(Set<ImmichAsset> selectedItems) {
|
void enableMultiSelect(Set<AssetResponseDto> selectedItems) {
|
||||||
state = state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems);
|
state =
|
||||||
|
state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
void disableMultiSelect() {
|
void disableMultiSelect() {
|
||||||
state = state.copyWith(isMultiSelectEnable: false, selectedItems: {}, selectedDateGroup: {});
|
state = state.copyWith(
|
||||||
|
isMultiSelectEnable: false,
|
||||||
|
selectedItems: {},
|
||||||
|
selectedDateGroup: {},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addSingleSelectedItem(ImmichAsset asset) {
|
void addSingleSelectedItem(AssetResponseDto asset) {
|
||||||
state = state.copyWith(selectedItems: {...state.selectedItems, asset});
|
state = state.copyWith(selectedItems: {...state.selectedItems, asset});
|
||||||
}
|
}
|
||||||
|
|
||||||
void addMultipleSelectedItems(List<ImmichAsset> assets) {
|
void addMultipleSelectedItems(List<AssetResponseDto> assets) {
|
||||||
state = state.copyWith(selectedItems: {...state.selectedItems, ...assets});
|
state = state.copyWith(selectedItems: {...state.selectedItems, ...assets});
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSingleSelectedItem(ImmichAsset asset) {
|
void removeSingleSelectedItem(AssetResponseDto asset) {
|
||||||
Set<ImmichAsset> currentList = state.selectedItems;
|
Set<AssetResponseDto> currentList = state.selectedItems;
|
||||||
|
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
|
|
||||||
state = state.copyWith(selectedItems: currentList);
|
state = state.copyWith(selectedItems: currentList);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeMultipleSelectedItem(List<ImmichAsset> assets) {
|
void removeMultipleSelectedItem(List<AssetResponseDto> assets) {
|
||||||
Set<ImmichAsset> currentList = state.selectedItems;
|
Set<AssetResponseDto> currentList = state.selectedItems;
|
||||||
|
|
||||||
for (ImmichAsset asset in assets) {
|
for (AssetResponseDto asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,4 +67,6 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final homePageStateProvider =
|
final homePageStateProvider =
|
||||||
StateNotifierProvider<HomePageStateNotifier, HomePageState>(((ref) => HomePageStateNotifier()));
|
StateNotifierProvider<HomePageStateNotifier, HomePageState>(
|
||||||
|
((ref) => HomePageStateNotifier()),
|
||||||
|
);
|
||||||
|
|||||||
@@ -50,37 +50,49 @@ class UploadProfileImageState {
|
|||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
factory UploadProfileImageState.fromJson(String source) => UploadProfileImageState.fromMap(json.decode(source));
|
factory UploadProfileImageState.fromJson(String source) =>
|
||||||
|
UploadProfileImageState.fromMap(json.decode(source));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)';
|
String toString() =>
|
||||||
|
'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
return other is UploadProfileImageState && other.status == status && other.profileImagePath == profileImagePath;
|
return other is UploadProfileImageState &&
|
||||||
|
other.status == status &&
|
||||||
|
other.profileImagePath == profileImagePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => status.hashCode ^ profileImagePath.hashCode;
|
int get hashCode => status.hashCode ^ profileImagePath.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState> {
|
class UploadProfileImageNotifier
|
||||||
UploadProfileImageNotifier()
|
extends StateNotifier<UploadProfileImageState> {
|
||||||
: super(UploadProfileImageState(
|
UploadProfileImageNotifier(this._userSErvice)
|
||||||
profileImagePath: '',
|
: super(
|
||||||
status: UploadProfileStatus.idle,
|
UploadProfileImageState(
|
||||||
));
|
profileImagePath: '',
|
||||||
|
status: UploadProfileStatus.idle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final UserService _userSErvice;
|
||||||
|
|
||||||
Future<bool> upload(XFile file) async {
|
Future<bool> upload(XFile file) async {
|
||||||
state = state.copyWith(status: UploadProfileStatus.loading);
|
state = state.copyWith(status: UploadProfileStatus.loading);
|
||||||
|
|
||||||
var res = await UserService().uploadProfileImage(file);
|
var res = await _userSErvice.uploadProfileImage(file);
|
||||||
|
|
||||||
if (res != null) {
|
if (res != null) {
|
||||||
debugPrint("Succesfully upload profile image");
|
debugPrint("Succesfully upload profile image");
|
||||||
state = state.copyWith(status: UploadProfileStatus.success, profileImagePath: res.profileImagePath);
|
state = state.copyWith(
|
||||||
|
status: UploadProfileStatus.success,
|
||||||
|
profileImagePath: res.profileImagePath,
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,4 +102,6 @@ class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
final uploadProfileImageProvider =
|
final uploadProfileImageProvider =
|
||||||
StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>(((ref) => UploadProfileImageNotifier()));
|
StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>(
|
||||||
|
((ref) => UploadProfileImageNotifier(ref.watch(userServiceProvider))),
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,110 +1,51 @@
|
|||||||
import 'dart:convert';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/modules/home/models/delete_asset_response.model.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/home/models/get_all_asset_response.model.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
final assetServiceProvider = Provider(
|
||||||
|
(ref) => AssetService(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
class AssetService {
|
class AssetService {
|
||||||
final NetworkService _networkService = NetworkService();
|
final ApiService _apiService;
|
||||||
|
|
||||||
Future<List<ImmichAsset>?> getAllAsset() async {
|
AssetService(this._apiService);
|
||||||
var res = await _networkService.getRequest(url: "asset/");
|
|
||||||
|
Future<List<AssetResponseDto>?> getAllAsset() async {
|
||||||
try {
|
try {
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
return await _apiService.assetApi.getAllAssets();
|
||||||
|
|
||||||
List<ImmichAsset> result = List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error getAllAsset ${e.toString()}");
|
debugPrint("Error [getAllAsset] ${e.toString()}");
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<GetAllAssetResponse?> getAllAssetWithPagination() async {
|
|
||||||
var res = await _networkService.getRequest(url: "asset/all");
|
|
||||||
try {
|
|
||||||
Map<String, dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
GetAllAssetResponse result = GetAllAssetResponse.fromMap(decodedData);
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Error getAllAsset ${e.toString()}");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<GetAllAssetResponse?> getOlderAsset(String? nextPageKey) async {
|
|
||||||
try {
|
|
||||||
var res = await _networkService.getRequest(
|
|
||||||
url: "asset/all?nextPageKey=$nextPageKey",
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
GetAllAssetResponse result = GetAllAssetResponse.fromMap(decodedData);
|
|
||||||
if (result.count != 0) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Error getAllAsset ${e.toString()}");
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<ImmichAsset>> getNewAsset(String latestDate) async {
|
|
||||||
try {
|
|
||||||
var res = await _networkService.getRequest(
|
|
||||||
url: "asset/new?latestDate=$latestDate",
|
|
||||||
);
|
|
||||||
|
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
List<ImmichAsset> result = List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
|
|
||||||
if (result.isNotEmpty) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Error getAllAsset ${e.toString()}");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<ImmichAssetWithExif?> getAssetById(String assetId) async {
|
|
||||||
try {
|
|
||||||
var res = await _networkService.getRequest(
|
|
||||||
url: "asset/assetById/$assetId",
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
ImmichAssetWithExif result = ImmichAssetWithExif.fromMap(decodedData);
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Error getAllAsset ${e.toString()}");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<DeleteAssetResponse>?> deleteAssets(Set<ImmichAsset> deleteAssets) async {
|
Future<AssetResponseDto?> getAssetById(String assetId) async {
|
||||||
try {
|
try {
|
||||||
var payload = [];
|
return await _apiService.assetApi.getAssetById(assetId);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error [getAssetById] ${e.toString()}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<DeleteAssetResponseDto>?> deleteAssets(
|
||||||
|
Set<AssetResponseDto> deleteAssets,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
List<String> payload = [];
|
||||||
|
|
||||||
for (var asset in deleteAssets) {
|
for (var asset in deleteAssets) {
|
||||||
payload.add(asset.id);
|
payload.add(asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
var res = await _networkService.deleteRequest(url: "asset/", data: {"ids": payload});
|
return await _apiService.assetApi
|
||||||
|
.deleteAsset(DeleteAssetDto(ids: payload));
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
List<DeleteAssetResponse> result = List.from(decodedData.map((a) => DeleteAssetResponse.fromMap(a)));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error getAllAsset ${e.toString()}");
|
debugPrint("Error getAllAsset ${e.toString()}");
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
|
import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart';
|
||||||
|
|
||||||
@@ -13,7 +14,10 @@ class ControlBottomAppBar extends StatelessWidget {
|
|||||||
width: MediaQuery.of(context).size.width,
|
width: MediaQuery.of(context).size.width,
|
||||||
height: MediaQuery.of(context).size.height * 0.15,
|
height: MediaQuery.of(context).size.height * 0.15,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(15), topRight: Radius.circular(15)),
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(15),
|
||||||
|
topRight: Radius.circular(15),
|
||||||
|
),
|
||||||
color: Colors.grey[300]?.withOpacity(0.98),
|
color: Colors.grey[300]?.withOpacity(0.98),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -25,7 +29,7 @@ class ControlBottomAppBar extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData: Icons.delete_forever_rounded,
|
iconData: Icons.delete_forever_rounded,
|
||||||
label: "Delete",
|
label: "control_bottom_app_bar_delete".tr(),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -46,8 +50,12 @@ class ControlBottomAppBar extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ControlBoxButton extends StatelessWidget {
|
class ControlBoxButton extends StatelessWidget {
|
||||||
const ControlBoxButton({Key? key, required this.label, required this.iconData, required this.onPressed})
|
const ControlBoxButton({
|
||||||
: super(key: key);
|
Key? key,
|
||||||
|
required this.label,
|
||||||
|
required this.iconData,
|
||||||
|
required this.onPressed,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final IconData iconData;
|
final IconData iconData;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
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/home/providers/home_page_state.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
class DailyTitleText extends ConsumerWidget {
|
class DailyTitleText extends ConsumerWidget {
|
||||||
const DailyTitleText({
|
const DailyTitleText({
|
||||||
@@ -12,15 +12,19 @@ class DailyTitleText extends ConsumerWidget {
|
|||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String isoDate;
|
final String isoDate;
|
||||||
final List<ImmichAsset> assetGroup;
|
final List<AssetResponseDto> assetGroup;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var currentYear = DateTime.now().year;
|
var currentYear = DateTime.now().year;
|
||||||
var groupYear = DateTime.parse(isoDate).year;
|
var groupYear = DateTime.parse(isoDate).year;
|
||||||
var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
|
var formatDateTemplate = currentYear == groupYear
|
||||||
var dateText = DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
|
? "daily_title_text_date".tr()
|
||||||
var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
|
: "daily_title_text_date_year".tr();
|
||||||
|
var dateText =
|
||||||
|
DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
|
||||||
|
var isMultiSelectEnable =
|
||||||
|
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||||
var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
|
var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
|
||||||
var selectedItems = ref.watch(homePageStateProvider).selectedItems;
|
var selectedItems = ref.watch(homePageStateProvider).selectedItems;
|
||||||
|
|
||||||
@@ -35,23 +39,46 @@ class DailyTitleText extends ConsumerWidget {
|
|||||||
selectedDateGroup.contains(dateText) &&
|
selectedDateGroup.contains(dateText) &&
|
||||||
selectedItems.length != assetGroup.length) {
|
selectedItems.length != assetGroup.length) {
|
||||||
// Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items
|
// Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items
|
||||||
ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText);
|
ref
|
||||||
ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup);
|
.watch(homePageStateProvider.notifier)
|
||||||
} else if (isMultiSelectEnable && selectedDateGroup.contains(dateText) && selectedDateGroup.length > 1) {
|
.removeSelectedDateGroup(dateText);
|
||||||
ref.watch(homePageStateProvider.notifier).removeSelectedDateGroup(dateText);
|
ref
|
||||||
ref.watch(homePageStateProvider.notifier).removeMultipleSelectedItem(assetGroup);
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.removeMultipleSelectedItem(assetGroup);
|
||||||
|
} else if (isMultiSelectEnable &&
|
||||||
|
selectedDateGroup.contains(dateText) &&
|
||||||
|
selectedDateGroup.length > 1) {
|
||||||
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.removeSelectedDateGroup(dateText);
|
||||||
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.removeMultipleSelectedItem(assetGroup);
|
||||||
} else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
|
} else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
|
||||||
ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText);
|
ref
|
||||||
ref.watch(homePageStateProvider.notifier).addMultipleSelectedItems(assetGroup);
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.addSelectedDateGroup(dateText);
|
||||||
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.addMultipleSelectedItems(assetGroup);
|
||||||
} else {
|
} else {
|
||||||
ref.watch(homePageStateProvider.notifier).enableMultiSelect(assetGroup.toSet());
|
ref
|
||||||
ref.watch(homePageStateProvider.notifier).addSelectedDateGroup(dateText);
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.enableMultiSelect(assetGroup.toSet());
|
||||||
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.addSelectedDateGroup(dateText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 29.0, bottom: 29.0, left: 12.0, right: 12.0),
|
padding: const EdgeInsets.only(
|
||||||
|
top: 29.0,
|
||||||
|
bottom: 29.0,
|
||||||
|
left: 12.0,
|
||||||
|
right: 12.0,
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
@@ -13,29 +14,31 @@ class DeleteDialog extends ConsumerWidget {
|
|||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
backgroundColor: Colors.grey[200],
|
backgroundColor: Colors.grey[200],
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
title: const Text("Delete Permanently"),
|
title: const Text("delete_dialog_title").tr(),
|
||||||
content: const Text("These items will be permanently deleted from Immich and from your device"),
|
content: const Text("delete_dialog_alert").tr(),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
"Cancel",
|
"delete_dialog_cancel",
|
||||||
style: TextStyle(color: Colors.blueGrey),
|
style: TextStyle(color: Colors.blueGrey),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.watch(assetProvider.notifier).deleteAssets(homePageState.selectedItems);
|
ref
|
||||||
|
.watch(assetProvider.notifier)
|
||||||
|
.deleteAssets(homePageState.selectedItems);
|
||||||
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
||||||
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
"Delete",
|
"delete_dialog_ok",
|
||||||
style: TextStyle(color: Colors.red[400]),
|
style: TextStyle(color: Colors.red[400]),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,15 +29,18 @@ class DisableMultiSelectButton extends ConsumerWidget {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onPressed();
|
onPressed();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
label: Text(
|
label: Text(
|
||||||
selectedItemCount.toString(),
|
'$selectedItemCount',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w600, fontSize: 18),
|
fontWeight: FontWeight.w600,
|
||||||
)),
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -118,20 +118,24 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
this.labelConstraints,
|
this.labelConstraints,
|
||||||
}) : assert(child.scrollDirection == Axis.vertical),
|
}) : assert(child.scrollDirection == Axis.vertical),
|
||||||
scrollThumbBuilder = _thumbSemicircleBuilder(
|
scrollThumbBuilder = _thumbSemicircleBuilder(
|
||||||
heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb),
|
heightScrollThumb * 0.6,
|
||||||
|
scrollThumbKey,
|
||||||
|
alwaysVisibleScrollThumb,
|
||||||
|
),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
DraggableScrollbarState createState() => DraggableScrollbarState();
|
DraggableScrollbarState createState() => DraggableScrollbarState();
|
||||||
|
|
||||||
static buildScrollThumbAndLabel(
|
static buildScrollThumbAndLabel({
|
||||||
{required Widget scrollThumb,
|
required Widget scrollThumb,
|
||||||
required Color backgroundColor,
|
required Color backgroundColor,
|
||||||
required Animation<double>? thumbAnimation,
|
required Animation<double>? thumbAnimation,
|
||||||
required Animation<double>? labelAnimation,
|
required Animation<double>? labelAnimation,
|
||||||
required Text? labelText,
|
required Text? labelText,
|
||||||
required BoxConstraints? labelConstraints,
|
required BoxConstraints? labelConstraints,
|
||||||
required bool alwaysVisibleScrollThumb}) {
|
required bool alwaysVisibleScrollThumb,
|
||||||
|
}) {
|
||||||
var scrollThumbAndLabel = labelText == null
|
var scrollThumbAndLabel = labelText == null
|
||||||
? scrollThumb
|
? scrollThumb
|
||||||
: Row(
|
: Row(
|
||||||
@@ -158,7 +162,10 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static ScrollThumbBuilder _thumbSemicircleBuilder(
|
static ScrollThumbBuilder _thumbSemicircleBuilder(
|
||||||
double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
double width,
|
||||||
|
Key? scrollThumbKey,
|
||||||
|
bool alwaysVisibleScrollThumb,
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
Color backgroundColor,
|
Color backgroundColor,
|
||||||
Animation<double> thumbAnimation,
|
Animation<double> thumbAnimation,
|
||||||
@@ -198,7 +205,9 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static ScrollThumbBuilder _thumbArrowBuilder(
|
static ScrollThumbBuilder _thumbArrowBuilder(
|
||||||
Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
Key? scrollThumbKey,
|
||||||
|
bool alwaysVisibleScrollThumb,
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
Color backgroundColor,
|
Color backgroundColor,
|
||||||
Animation<double> thumbAnimation,
|
Animation<double> thumbAnimation,
|
||||||
@@ -234,7 +243,9 @@ class DraggableScrollbar extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static ScrollThumbBuilder _thumbRRectBuilder(
|
static ScrollThumbBuilder _thumbRRectBuilder(
|
||||||
Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
|
Key? scrollThumbKey,
|
||||||
|
bool alwaysVisibleScrollThumb,
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
Color backgroundColor,
|
Color backgroundColor,
|
||||||
Animation<double> thumbAnimation,
|
Animation<double> thumbAnimation,
|
||||||
@@ -372,42 +383,44 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
//print("LayoutBuilder constraints=$constraints");
|
//print("LayoutBuilder constraints=$constraints");
|
||||||
|
|
||||||
return NotificationListener<ScrollNotification>(
|
return NotificationListener<ScrollNotification>(
|
||||||
onNotification: (ScrollNotification notification) {
|
onNotification: (ScrollNotification notification) {
|
||||||
changePosition(notification);
|
changePosition(notification);
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
RepaintBoundary(
|
RepaintBoundary(
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
RepaintBoundary(
|
RepaintBoundary(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onVerticalDragStart: _onVerticalDragStart,
|
onVerticalDragStart: _onVerticalDragStart,
|
||||||
onVerticalDragUpdate: _onVerticalDragUpdate,
|
onVerticalDragUpdate: _onVerticalDragUpdate,
|
||||||
onVerticalDragEnd: _onVerticalDragEnd,
|
onVerticalDragEnd: _onVerticalDragEnd,
|
||||||
child: Container(
|
child: Container(
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
margin: EdgeInsets.only(top: _barOffset),
|
margin: EdgeInsets.only(top: _barOffset),
|
||||||
padding: widget.padding,
|
padding: widget.padding,
|
||||||
child: widget.scrollThumbBuilder(
|
child: widget.scrollThumbBuilder(
|
||||||
widget.backgroundColor,
|
widget.backgroundColor,
|
||||||
_thumbAnimation,
|
_thumbAnimation,
|
||||||
_labelAnimation,
|
_labelAnimation,
|
||||||
widget.heightScrollThumb,
|
widget.heightScrollThumb,
|
||||||
labelText: labelText,
|
labelText: labelText,
|
||||||
labelConstraints: widget.labelConstraints,
|
labelConstraints: widget.labelConstraints,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
],
|
||||||
],
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//scroll bar has received notification that it's view was scrolled
|
//scroll bar has received notification that it's view was scrolled
|
||||||
@@ -498,7 +511,10 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
|
|||||||
}
|
}
|
||||||
|
|
||||||
double viewDelta = getScrollViewDelta(
|
double viewDelta = getScrollViewDelta(
|
||||||
details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent);
|
details.delta.dy,
|
||||||
|
barMaxScrollExtent,
|
||||||
|
viewMaxScrollExtent,
|
||||||
|
);
|
||||||
|
|
||||||
_viewOffset = widget.controller.position.pixels + viewDelta;
|
_viewOffset = widget.controller.position.pixels + viewDelta;
|
||||||
if (_viewOffset < widget.controller.position.minScrollExtent) {
|
if (_viewOffset < widget.controller.position.minScrollExtent) {
|
||||||
@@ -579,7 +595,9 @@ class ArrowClipper extends CustomClipper<Path> {
|
|||||||
path.lineTo(startPointX + arrowWidth, startPointY);
|
path.lineTo(startPointX + arrowWidth, startPointY);
|
||||||
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
|
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
|
||||||
path.lineTo(
|
path.lineTo(
|
||||||
startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
|
startPointX + arrowWidth / 2,
|
||||||
|
startPointY - arrowWidth / 2 + 1.0,
|
||||||
|
);
|
||||||
path.lineTo(startPointX, startPointY + 1.0);
|
path.lineTo(startPointX, startPointY + 1.0);
|
||||||
path.close();
|
path.close();
|
||||||
|
|
||||||
@@ -589,7 +607,9 @@ class ArrowClipper extends CustomClipper<Path> {
|
|||||||
path.lineTo(startPointX, startPointY);
|
path.lineTo(startPointX, startPointY);
|
||||||
path.lineTo(startPointX, startPointY - 1.0);
|
path.lineTo(startPointX, startPointY - 1.0);
|
||||||
path.lineTo(
|
path.lineTo(
|
||||||
startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
|
startPointX + arrowWidth / 2,
|
||||||
|
startPointY + arrowWidth / 2 - 1.0,
|
||||||
|
);
|
||||||
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
|
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
|
||||||
path.close();
|
path.close();
|
||||||
|
|
||||||
@@ -615,7 +635,7 @@ class SlideFadeTransition extends StatelessWidget {
|
|||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: animation,
|
animation: animation,
|
||||||
builder: (context, child) =>
|
builder: (context, child) =>
|
||||||
animation.value == 0.0 ? Container() : child!,
|
animation.value == 0.0 ? const SizedBox() : child!,
|
||||||
child: SlideTransition(
|
child: SlideTransition(
|
||||||
position: Tween(
|
position: Tween(
|
||||||
begin: const Offset(0.3, 0.0),
|
begin: const Offset(0.3, 0.0),
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
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/home/ui/thumbnail_image.dart';
|
import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class ImageGrid extends ConsumerWidget {
|
class ImageGrid extends ConsumerWidget {
|
||||||
final List<ImmichAsset> assetGroup;
|
final List<AssetResponseDto> assetGroup;
|
||||||
|
|
||||||
const ImageGrid({Key? key, required this.assetGroup}) : super(key: key);
|
const ImageGrid({Key? key, required this.assetGroup}) : super(key: key);
|
||||||
|
|
||||||
@@ -25,27 +25,26 @@ class ImageGrid extends ConsumerWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
ThumbnailImage(asset: assetGroup[index]),
|
ThumbnailImage(asset: assetGroup[index]),
|
||||||
assetType == 'IMAGE'
|
if (assetType != AssetTypeEnum.IMAGE)
|
||||||
? Container()
|
Positioned(
|
||||||
: Positioned(
|
top: 5,
|
||||||
top: 5,
|
right: 5,
|
||||||
right: 5,
|
child: Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
Text(
|
||||||
Text(
|
assetGroup[index].duration.toString().substring(0, 7),
|
||||||
assetGroup[index].duration.toString().substring(0, 7),
|
style: const TextStyle(
|
||||||
style: const TextStyle(
|
color: Colors.white,
|
||||||
color: Colors.white,
|
fontSize: 10,
|
||||||
fontSize: 10,
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
const Icon(
|
|
||||||
Icons.play_circle_outline_rounded,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
)
|
const Icon(
|
||||||
|
Icons.play_circle_outline_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
pinned: false,
|
pinned: false,
|
||||||
snap: false,
|
snap: false,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(5))),
|
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||||
|
),
|
||||||
leading: Builder(
|
leading: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return Stack(
|
return Stack(
|
||||||
@@ -49,30 +50,29 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
serverInfoState.isVersionMismatch
|
if (serverInfoState.isVersionMismatch)
|
||||||
? Positioned(
|
Positioned(
|
||||||
bottom: 12,
|
bottom: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => Scaffold.of(context).openDrawer(),
|
onTap: () => Scaffold.of(context).openDrawer(),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(50.0),
|
borderRadius: BorderRadius.circular(50.0),
|
||||||
),
|
),
|
||||||
child: const Padding(
|
child: const Padding(
|
||||||
padding: EdgeInsets.all(2.0),
|
padding: EdgeInsets.all(2.0),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.info,
|
Icons.info,
|
||||||
color: Color.fromARGB(255, 243, 188, 106),
|
color: Color.fromARGB(255, 243, 188, 106),
|
||||||
size: 15,
|
size: 15,
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: Container(),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -90,21 +90,21 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
Stack(
|
Stack(
|
||||||
alignment: AlignmentDirectional.center,
|
alignment: AlignmentDirectional.center,
|
||||||
children: [
|
children: [
|
||||||
backupState.backupProgress == BackUpProgressEnum.inProgress
|
if (backupState.backupProgress == BackUpProgressEnum.inProgress)
|
||||||
? Positioned(
|
Positioned(
|
||||||
top: 10,
|
top: 10,
|
||||||
right: 12,
|
right: 12,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 8,
|
height: 8,
|
||||||
width: 8,
|
width: 8,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
Theme.of(context).primaryColor),
|
Theme.of(context).primaryColor,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: Container(),
|
),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
iconSize: 30,
|
iconSize: 30,
|
||||||
@@ -119,7 +119,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
Icons.cloud_off_rounded,
|
Icons.cloud_off_rounded,
|
||||||
size: 8,
|
size: 8,
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.backup_rounded)),
|
child: const Icon(Icons.backup_rounded),
|
||||||
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var onPop = await AutoRouter.of(context)
|
var onPop = await AutoRouter.of(context)
|
||||||
.push(const BackupControllerRoute());
|
.push(const BackupControllerRoute());
|
||||||
@@ -129,18 +130,15 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
backupState.backupProgress == BackUpProgressEnum.inProgress
|
if (backupState.backupProgress == BackUpProgressEnum.inProgress)
|
||||||
? Positioned(
|
Positioned(
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
child: Text(
|
child: Text(
|
||||||
(backupState.allUniqueAssets.length -
|
'${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}',
|
||||||
backupState.selectedAlbumsBackupAssetsIds.length)
|
style:
|
||||||
.toString(),
|
const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
|
||||||
style: const TextStyle(
|
),
|
||||||
fontSize: 9, fontWeight: FontWeight.bold),
|
),
|
||||||
),
|
|
||||||
)
|
|
||||||
: Container()
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
class MonthlyTitleText extends StatelessWidget {
|
class MonthlyTitleText extends StatelessWidget {
|
||||||
const MonthlyTitleText({
|
const MonthlyTitleText({
|
||||||
@@ -11,7 +11,8 @@ class MonthlyTitleText extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var monthTitleText = DateFormat('MMMM y').format(DateTime.parse(isoDate));
|
var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
|
||||||
|
.format(DateTime.parse(isoDate));
|
||||||
|
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
@@ -54,7 +55,8 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
radius: 35,
|
radius: 35,
|
||||||
backgroundImage: NetworkImage(
|
backgroundImage: NetworkImage(
|
||||||
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}'),
|
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
|
||||||
|
),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -70,7 +72,8 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
radius: 35,
|
radius: 35,
|
||||||
backgroundImage: NetworkImage(
|
backgroundImage: NetworkImage(
|
||||||
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}'),
|
'$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}',
|
||||||
|
),
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -87,12 +90,15 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
return const ImmichLoadingIndicator();
|
return const ImmichLoadingIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
_pickUserProfileImage() async {
|
_pickUserProfileImage() async {
|
||||||
final XFile? image = await ImagePicker().pickImage(
|
final XFile? image = await ImagePicker().pickImage(
|
||||||
source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024);
|
source: ImageSource.gallery,
|
||||||
|
maxHeight: 1024,
|
||||||
|
maxWidth: 1024,
|
||||||
|
);
|
||||||
|
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
var success =
|
var success =
|
||||||
@@ -100,16 +106,20 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
|
ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
|
||||||
ref.read(uploadProfileImageProvider).profileImagePath);
|
ref.read(uploadProfileImageProvider).profileImagePath,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
_getPackageInfo();
|
() {
|
||||||
_buildUserProfileImage();
|
_getPackageInfo();
|
||||||
return null;
|
_buildUserProfileImage();
|
||||||
}, []);
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
return Drawer(
|
return Drawer(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@@ -183,15 +193,16 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
color: Colors.black54,
|
color: Colors.black54,
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Sign Out",
|
"profile_drawer_sign_out",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black54,
|
color: Colors.black54,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
|
).tr(),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
bool res =
|
bool res =
|
||||||
await ref.read(authenticationProvider.notifier).logout();
|
await ref.watch(authenticationProvider.notifier).logout();
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
ref.watch(backupProvider.notifier).cancelBackup();
|
ref.watch(backupProvider.notifier).cancelBackup();
|
||||||
@@ -227,12 +238,13 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
serverInfoState.isVersionMismatch
|
serverInfoState.isVersionMismatch
|
||||||
? serverInfoState.versionMismatchErrorMessage
|
? serverInfoState.versionMismatchErrorMessage
|
||||||
: "Client and Server are up-to-date",
|
: "profile_drawer_client_server_up_to_date".tr(),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontWeight: FontWeight.w600),
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
@@ -270,7 +282,7 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
"${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}",
|
"${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch_}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.grey[500],
|
color: Colors.grey[500],
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ 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/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/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class ThumbnailImage extends HookConsumerWidget {
|
class ThumbnailImage extends HookConsumerWidget {
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
|
|
||||||
const ThumbnailImage({Key? key, required this.asset}) : super(key: key);
|
const ThumbnailImage({Key? key, required this.asset}) : super(key: key);
|
||||||
|
|
||||||
@@ -22,13 +22,13 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
|
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl =
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
|
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
|
||||||
|
|
||||||
var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
|
var selectedAsset = ref.watch(homePageStateProvider).selectedItems;
|
||||||
var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
|
var isMultiSelectEnable =
|
||||||
|
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||||
var deviceId = ref.watch(authenticationProvider).deviceId;
|
var deviceId = ref.watch(authenticationProvider).deviceId;
|
||||||
|
|
||||||
Widget _buildSelectionIcon(ImmichAsset asset) {
|
Widget _buildSelectionIcon(AssetResponseDto asset) {
|
||||||
if (selectedAsset.contains(asset)) {
|
if (selectedAsset.contains(asset)) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.check_circle,
|
Icons.check_circle,
|
||||||
@@ -45,14 +45,22 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
debugPrint("View ${asset.id}");
|
debugPrint("View ${asset.id}");
|
||||||
if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length == 1) {
|
if (isMultiSelectEnable &&
|
||||||
|
selectedAsset.contains(asset) &&
|
||||||
|
selectedAsset.length == 1) {
|
||||||
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
||||||
} else if (isMultiSelectEnable && selectedAsset.contains(asset) && selectedAsset.length > 1) {
|
} else if (isMultiSelectEnable &&
|
||||||
ref.watch(homePageStateProvider.notifier).removeSingleSelectedItem(asset);
|
selectedAsset.contains(asset) &&
|
||||||
|
selectedAsset.length > 1) {
|
||||||
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.removeSingleSelectedItem(asset);
|
||||||
} else if (isMultiSelectEnable && !selectedAsset.contains(asset)) {
|
} else if (isMultiSelectEnable && !selectedAsset.contains(asset)) {
|
||||||
ref.watch(homePageStateProvider.notifier).addSingleSelectedItem(asset);
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.addSingleSelectedItem(asset);
|
||||||
} else {
|
} else {
|
||||||
if (asset.type == 'IMAGE') {
|
if (asset.type == AssetTypeEnum.IMAGE) {
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
ImageViewerRoute(
|
ImageViewerRoute(
|
||||||
imageUrl:
|
imageUrl:
|
||||||
@@ -65,8 +73,10 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
} else {
|
} else {
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
VideoViewerRoute(
|
VideoViewerRoute(
|
||||||
videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
|
videoUrl:
|
||||||
asset: asset),
|
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
|
||||||
|
asset: asset,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,23 +93,32 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: isMultiSelectEnable && selectedAsset.contains(asset)
|
border: isMultiSelectEnable && selectedAsset.contains(asset)
|
||||||
? Border.all(color: Theme.of(context).primaryColorLight, width: 10)
|
? Border.all(
|
||||||
|
color: Theme.of(context).primaryColorLight,
|
||||||
|
width: 10,
|
||||||
|
)
|
||||||
: const Border(),
|
: const Border(),
|
||||||
),
|
),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
cacheKey: "${asset.id}-${cacheKey.value}",
|
cacheKey: "${asset.id}-${cacheKey.value}",
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 300,
|
height: 300,
|
||||||
memCacheHeight: asset.type == 'IMAGE' ? 250 : 400,
|
memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 250 : 400,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
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) => Transform.scale(
|
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
||||||
|
Transform.scale(
|
||||||
scale: 0.2,
|
scale: 0.2,
|
||||||
child: CircularProgressIndicator(value: downloadProgress.progress),
|
child: CircularProgressIndicator(
|
||||||
|
value: downloadProgress.progress,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
errorWidget: (context, url, error) {
|
errorWidget: (context, url, error) {
|
||||||
|
debugPrint("Error getting thumbnail $url = $error");
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.image_not_supported_outlined,
|
Icons.image_not_supported_outlined,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
@@ -107,22 +126,21 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
if (isMultiSelectEnable)
|
||||||
child: isMultiSelectEnable
|
Padding(
|
||||||
? Padding(
|
padding: const EdgeInsets.all(3.0),
|
||||||
padding: const EdgeInsets.all(3.0),
|
child: Align(
|
||||||
child: Align(
|
alignment: Alignment.topLeft,
|
||||||
alignment: Alignment.topLeft,
|
child: _buildSelectionIcon(asset),
|
||||||
child: _buildSelectionIcon(asset),
|
),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
),
|
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 10,
|
right: 10,
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
(deviceId != asset.deviceId) ? Icons.cloud_done_outlined : Icons.photo_library_rounded,
|
(deviceId != asset.deviceId)
|
||||||
|
? Icons.cloud_done_outlined
|
||||||
|
: Icons.photo_library_rounded,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -26,29 +26,25 @@ class HomePage extends HookConsumerWidget {
|
|||||||
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||||
var homePageState = ref.watch(homePageStateProvider);
|
var homePageState = ref.watch(homePageStateProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
ref.read(websocketProvider.notifier).connect();
|
() {
|
||||||
ref.read(assetProvider.notifier).getAllAsset();
|
ref.read(websocketProvider.notifier).connect();
|
||||||
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
ref.read(assetProvider.notifier).getAllAsset();
|
||||||
return null;
|
ref.watch(serverInfoProvider.notifier).getServerVersion();
|
||||||
}, []);
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
void reloadAllAsset() {
|
void reloadAllAsset() {
|
||||||
ref.read(assetProvider.notifier).getAllAsset();
|
ref.read(assetProvider.notifier).getAllAsset();
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildSelectedItemCountIndicator() {
|
_buildSelectedItemCountIndicator() {
|
||||||
return isMultiSelectEnable
|
return DisableMultiSelectButton(
|
||||||
? DisableMultiSelectButton(
|
onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
|
||||||
onPressed:
|
selectedItemCount: homePageState.selectedItems.length,
|
||||||
ref.watch(homePageStateProvider.notifier).disableMultiSelect,
|
);
|
||||||
selectedItemCount: homePageState.selectedItems.length,
|
|
||||||
)
|
|
||||||
: Container();
|
|
||||||
}
|
|
||||||
|
|
||||||
_buildBottomAppBar() {
|
|
||||||
return isMultiSelectEnable ? const ControlBottomAppBar() : Container();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
@@ -121,8 +117,10 @@ class HomePage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildSelectedItemCountIndicator(),
|
if (isMultiSelectEnable) ...[
|
||||||
_buildBottomAppBar(),
|
_buildSelectedItemCountIndicator(),
|
||||||
|
const ControlBottomAppBar(),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
import 'dart:convert';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/device_info.model.dart';
|
|
||||||
|
|
||||||
class AuthenticationState {
|
class AuthenticationState {
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
final String deviceType;
|
final DeviceTypeEnum deviceType;
|
||||||
final String userId;
|
final String userId;
|
||||||
final String userEmail;
|
final String userEmail;
|
||||||
final bool isAuthenticated;
|
final bool isAuthenticated;
|
||||||
final String firstName;
|
final String firstName;
|
||||||
final String lastName;
|
final String lastName;
|
||||||
final bool isAdmin;
|
final bool isAdmin;
|
||||||
final bool isFirstLogin;
|
final bool shouldChangePassword;
|
||||||
final String profileImagePath;
|
final String profileImagePath;
|
||||||
final DeviceInfoRemote deviceInfo;
|
final DeviceInfoResponseDto deviceInfo;
|
||||||
|
|
||||||
AuthenticationState({
|
AuthenticationState({
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
required this.deviceType,
|
required this.deviceType,
|
||||||
@@ -24,23 +21,23 @@ class AuthenticationState {
|
|||||||
required this.firstName,
|
required this.firstName,
|
||||||
required this.lastName,
|
required this.lastName,
|
||||||
required this.isAdmin,
|
required this.isAdmin,
|
||||||
required this.isFirstLogin,
|
required this.shouldChangePassword,
|
||||||
required this.profileImagePath,
|
required this.profileImagePath,
|
||||||
required this.deviceInfo,
|
required this.deviceInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
AuthenticationState copyWith({
|
AuthenticationState copyWith({
|
||||||
String? deviceId,
|
String? deviceId,
|
||||||
String? deviceType,
|
DeviceTypeEnum? deviceType,
|
||||||
String? userId,
|
String? userId,
|
||||||
String? userEmail,
|
String? userEmail,
|
||||||
bool? isAuthenticated,
|
bool? isAuthenticated,
|
||||||
String? firstName,
|
String? firstName,
|
||||||
String? lastName,
|
String? lastName,
|
||||||
bool? isAdmin,
|
bool? isAdmin,
|
||||||
bool? isFirstLoggedIn,
|
bool? shouldChangePassword,
|
||||||
String? profileImagePath,
|
String? profileImagePath,
|
||||||
DeviceInfoRemote? deviceInfo,
|
DeviceInfoResponseDto? deviceInfo,
|
||||||
}) {
|
}) {
|
||||||
return AuthenticationState(
|
return AuthenticationState(
|
||||||
deviceId: deviceId ?? this.deviceId,
|
deviceId: deviceId ?? this.deviceId,
|
||||||
@@ -51,7 +48,7 @@ class AuthenticationState {
|
|||||||
firstName: firstName ?? this.firstName,
|
firstName: firstName ?? this.firstName,
|
||||||
lastName: lastName ?? this.lastName,
|
lastName: lastName ?? this.lastName,
|
||||||
isAdmin: isAdmin ?? this.isAdmin,
|
isAdmin: isAdmin ?? this.isAdmin,
|
||||||
isFirstLogin: isFirstLoggedIn ?? isFirstLogin,
|
shouldChangePassword: shouldChangePassword ?? this.shouldChangePassword,
|
||||||
profileImagePath: profileImagePath ?? this.profileImagePath,
|
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||||
deviceInfo: deviceInfo ?? this.deviceInfo,
|
deviceInfo: deviceInfo ?? this.deviceInfo,
|
||||||
);
|
);
|
||||||
@@ -59,47 +56,9 @@ class AuthenticationState {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, isFirstLoggedIn: $isFirstLogin, profileImagePath: $profileImagePath, deviceInfo: $deviceInfo)';
|
return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword, profileImagePath: $profileImagePath, deviceInfo: $deviceInfo)';
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'deviceId': deviceId});
|
|
||||||
result.addAll({'deviceType': deviceType});
|
|
||||||
result.addAll({'userId': userId});
|
|
||||||
result.addAll({'userEmail': userEmail});
|
|
||||||
result.addAll({'isAuthenticated': isAuthenticated});
|
|
||||||
result.addAll({'firstName': firstName});
|
|
||||||
result.addAll({'lastName': lastName});
|
|
||||||
result.addAll({'isAdmin': isAdmin});
|
|
||||||
result.addAll({'isFirstLogin': isFirstLogin});
|
|
||||||
result.addAll({'profileImagePath': profileImagePath});
|
|
||||||
result.addAll({'deviceInfo': deviceInfo.toMap()});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory AuthenticationState.fromMap(Map<String, dynamic> map) {
|
|
||||||
return AuthenticationState(
|
|
||||||
deviceId: map['deviceId'] ?? '',
|
|
||||||
deviceType: map['deviceType'] ?? '',
|
|
||||||
userId: map['userId'] ?? '',
|
|
||||||
userEmail: map['userEmail'] ?? '',
|
|
||||||
isAuthenticated: map['isAuthenticated'] ?? false,
|
|
||||||
firstName: map['firstName'] ?? '',
|
|
||||||
lastName: map['lastName'] ?? '',
|
|
||||||
isAdmin: map['isAdmin'] ?? false,
|
|
||||||
isFirstLogin: map['isFirstLogin'] ?? false,
|
|
||||||
profileImagePath: map['profileImagePath'] ?? '',
|
|
||||||
deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory AuthenticationState.fromJson(String source) => AuthenticationState.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
@@ -113,7 +72,7 @@ class AuthenticationState {
|
|||||||
other.firstName == firstName &&
|
other.firstName == firstName &&
|
||||||
other.lastName == lastName &&
|
other.lastName == lastName &&
|
||||||
other.isAdmin == isAdmin &&
|
other.isAdmin == isAdmin &&
|
||||||
other.isFirstLogin == isFirstLogin &&
|
other.shouldChangePassword == shouldChangePassword &&
|
||||||
other.profileImagePath == profileImagePath &&
|
other.profileImagePath == profileImagePath &&
|
||||||
other.deviceInfo == deviceInfo;
|
other.deviceInfo == deviceInfo;
|
||||||
}
|
}
|
||||||
@@ -128,7 +87,7 @@ class AuthenticationState {
|
|||||||
firstName.hashCode ^
|
firstName.hashCode ^
|
||||||
lastName.hashCode ^
|
lastName.hashCode ^
|
||||||
isAdmin.hashCode ^
|
isAdmin.hashCode ^
|
||||||
isFirstLogin.hashCode ^
|
shouldChangePassword.hashCode ^
|
||||||
profileImagePath.hashCode ^
|
profileImagePath.hashCode ^
|
||||||
deviceInfo.hashCode;
|
deviceInfo.hashCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,10 @@ class HiveSavedLoginInfo {
|
|||||||
@HiveField(3)
|
@HiveField(3)
|
||||||
bool isSaveLogin;
|
bool isSaveLogin;
|
||||||
|
|
||||||
HiveSavedLoginInfo({required this.email, required this.password, required this.serverUrl, required this.isSaveLogin});
|
HiveSavedLoginInfo({
|
||||||
|
required this.email,
|
||||||
|
required this.password,
|
||||||
|
required this.serverUrl,
|
||||||
|
required this.isSaveLogin,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class LogInReponse {
|
|||||||
final String lastName;
|
final String lastName;
|
||||||
final String profileImagePath;
|
final String profileImagePath;
|
||||||
final bool isAdmin;
|
final bool isAdmin;
|
||||||
final bool isFirstLogin;
|
final bool shouldChangePassword;
|
||||||
|
|
||||||
LogInReponse({
|
LogInReponse({
|
||||||
required this.accessToken,
|
required this.accessToken,
|
||||||
@@ -18,7 +18,7 @@ class LogInReponse {
|
|||||||
required this.lastName,
|
required this.lastName,
|
||||||
required this.profileImagePath,
|
required this.profileImagePath,
|
||||||
required this.isAdmin,
|
required this.isAdmin,
|
||||||
required this.isFirstLogin,
|
required this.shouldChangePassword,
|
||||||
});
|
});
|
||||||
|
|
||||||
LogInReponse copyWith({
|
LogInReponse copyWith({
|
||||||
@@ -29,7 +29,7 @@ class LogInReponse {
|
|||||||
String? lastName,
|
String? lastName,
|
||||||
String? profileImagePath,
|
String? profileImagePath,
|
||||||
bool? isAdmin,
|
bool? isAdmin,
|
||||||
bool? isFirstLogin,
|
bool? shouldChangePassword,
|
||||||
}) {
|
}) {
|
||||||
return LogInReponse(
|
return LogInReponse(
|
||||||
accessToken: accessToken ?? this.accessToken,
|
accessToken: accessToken ?? this.accessToken,
|
||||||
@@ -39,7 +39,7 @@ class LogInReponse {
|
|||||||
lastName: lastName ?? this.lastName,
|
lastName: lastName ?? this.lastName,
|
||||||
profileImagePath: profileImagePath ?? this.profileImagePath,
|
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||||
isAdmin: isAdmin ?? this.isAdmin,
|
isAdmin: isAdmin ?? this.isAdmin,
|
||||||
isFirstLogin: isFirstLogin ?? this.isFirstLogin,
|
shouldChangePassword: shouldChangePassword ?? this.shouldChangePassword,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class LogInReponse {
|
|||||||
result.addAll({'lastName': lastName});
|
result.addAll({'lastName': lastName});
|
||||||
result.addAll({'profileImagePath': profileImagePath});
|
result.addAll({'profileImagePath': profileImagePath});
|
||||||
result.addAll({'isAdmin': isAdmin});
|
result.addAll({'isAdmin': isAdmin});
|
||||||
result.addAll({'isFirstLogin': isFirstLogin});
|
result.addAll({'shouldChangePassword': shouldChangePassword});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -67,17 +67,18 @@ class LogInReponse {
|
|||||||
lastName: map['lastName'] ?? '',
|
lastName: map['lastName'] ?? '',
|
||||||
profileImagePath: map['profileImagePath'] ?? '',
|
profileImagePath: map['profileImagePath'] ?? '',
|
||||||
isAdmin: map['isAdmin'] ?? false,
|
isAdmin: map['isAdmin'] ?? false,
|
||||||
isFirstLogin: map['isFirstLogin'] ?? false,
|
shouldChangePassword: map['shouldChangePassword'] ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
factory LogInReponse.fromJson(String source) => LogInReponse.fromMap(json.decode(source));
|
factory LogInReponse.fromJson(String source) =>
|
||||||
|
LogInReponse.fromMap(json.decode(source));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail, firstName: $firstName, lastName: $lastName, profileImagePath: $profileImagePath, isAdmin: $isAdmin, isFirstLogin: $isFirstLogin)';
|
return 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail, firstName: $firstName, lastName: $lastName, profileImagePath: $profileImagePath, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -92,7 +93,7 @@ class LogInReponse {
|
|||||||
other.lastName == lastName &&
|
other.lastName == lastName &&
|
||||||
other.profileImagePath == profileImagePath &&
|
other.profileImagePath == profileImagePath &&
|
||||||
other.isAdmin == isAdmin &&
|
other.isAdmin == isAdmin &&
|
||||||
other.isFirstLogin == isFirstLogin;
|
other.shouldChangePassword == shouldChangePassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -104,6 +105,6 @@ class LogInReponse {
|
|||||||
lastName.hashCode ^
|
lastName.hashCode ^
|
||||||
profileImagePath.hashCode ^
|
profileImagePath.hashCode ^
|
||||||
isAdmin.hashCode ^
|
isAdmin.hashCode ^
|
||||||
isFirstLogin.hashCode;
|
shouldChangePassword.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,52 @@
|
|||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/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/login/models/login_response.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/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:immich_mobile/shared/services/network.service.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/shared/models/device_info.model.dart';
|
|
||||||
|
|
||||||
class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||||
AuthenticationNotifier(this.ref)
|
AuthenticationNotifier(
|
||||||
: super(
|
this._deviceInfoService,
|
||||||
|
this._backupService,
|
||||||
|
this._apiService,
|
||||||
|
) : super(
|
||||||
AuthenticationState(
|
AuthenticationState(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
deviceType: "",
|
deviceType: DeviceTypeEnum.ANDROID,
|
||||||
userId: "",
|
userId: "",
|
||||||
userEmail: "",
|
userEmail: "",
|
||||||
firstName: '',
|
firstName: '',
|
||||||
lastName: '',
|
lastName: '',
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isFirstLogin: false,
|
shouldChangePassword: false,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
deviceInfo: DeviceInfoRemote(
|
deviceInfo: DeviceInfoResponseDto(
|
||||||
id: 0,
|
id: 0,
|
||||||
userId: "",
|
userId: "",
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
deviceType: "",
|
deviceType: DeviceTypeEnum.ANDROID,
|
||||||
notificationToken: "",
|
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
isAutoBackup: false,
|
isAutoBackup: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final Ref ref;
|
final DeviceInfoService _deviceInfoService;
|
||||||
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
final BackupService _backupService;
|
||||||
final BackupService _backupService = BackupService();
|
final ApiService _apiService;
|
||||||
final NetworkService _networkService = NetworkService();
|
|
||||||
|
|
||||||
Future<bool> login(String email, String password, String serverEndpoint, bool isSavedLoginInfo) async {
|
Future<bool> login(
|
||||||
|
String email,
|
||||||
|
String password,
|
||||||
|
String serverEndpoint,
|
||||||
|
bool isSavedLoginInfo,
|
||||||
|
) async {
|
||||||
// Store server endpoint to Hive and test endpoint
|
// Store server endpoint to Hive and test endpoint
|
||||||
if (serverEndpoint[serverEndpoint.length - 1] == "/") {
|
if (serverEndpoint[serverEndpoint.length - 1] == "/") {
|
||||||
var validUrl = serverEndpoint.substring(0, serverEndpoint.length - 1);
|
var validUrl = serverEndpoint.substring(0, serverEndpoint.length - 1);
|
||||||
@@ -51,12 +55,12 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
Hive.box(userInfoBox).put(serverEndpointKey, serverEndpoint);
|
Hive.box(userInfoBox).put(serverEndpointKey, serverEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check Server URL validity
|
||||||
try {
|
try {
|
||||||
bool isServerEndpointVerified = await _networkService.pingServer();
|
_apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
|
||||||
if (!isServerEndpointVerified) {
|
await _apiService.serverInfoApi.pingServer();
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
debugPrint('Invalid Server Endpoint Url $e');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,49 +75,73 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
|
|
||||||
// Make sign-in request
|
// Make sign-in request
|
||||||
try {
|
try {
|
||||||
Response res = await _networkService.postRequest(url: 'auth/login', data: {'email': email, 'password': password});
|
var loginResponse = await _apiService.authenticationApi.login(
|
||||||
|
LoginCredentialDto(
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
var payload = LogInReponse.fromJson(res.toString());
|
if (loginResponse == null) {
|
||||||
|
debugPrint('Login Response is null');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Hive.box(userInfoBox).put(accessTokenKey, payload.accessToken);
|
Hive.box(userInfoBox).put(accessTokenKey, loginResponse.accessToken);
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
userId: payload.userId,
|
userId: loginResponse.userId,
|
||||||
userEmail: payload.userEmail,
|
userEmail: loginResponse.userEmail,
|
||||||
firstName: payload.firstName,
|
firstName: loginResponse.firstName,
|
||||||
lastName: payload.lastName,
|
lastName: loginResponse.lastName,
|
||||||
profileImagePath: payload.profileImagePath,
|
profileImagePath: loginResponse.profileImagePath,
|
||||||
isAdmin: payload.isAdmin,
|
isAdmin: loginResponse.isAdmin,
|
||||||
isFirstLoggedIn: payload.isFirstLogin,
|
shouldChangePassword: loginResponse.shouldChangePassword,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Login Success - Set Access Token to API Client
|
||||||
|
_apiService.setAccessToken(loginResponse.accessToken);
|
||||||
|
|
||||||
if (isSavedLoginInfo) {
|
if (isSavedLoginInfo) {
|
||||||
// Save login info to local storage
|
// Save login info to local storage
|
||||||
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put(
|
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put(
|
||||||
savedLoginInfoKey,
|
savedLoginInfoKey,
|
||||||
HiveSavedLoginInfo(
|
HiveSavedLoginInfo(
|
||||||
email: email,
|
email: email,
|
||||||
password: password,
|
password: password,
|
||||||
isSaveLogin: true,
|
isSaveLogin: true,
|
||||||
serverUrl: Hive.box(userInfoBox).get(serverEndpointKey)),
|
serverUrl: Hive.box(userInfoBox).get(serverEndpointKey),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).delete(savedLoginInfoKey);
|
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox)
|
||||||
|
.delete(savedLoginInfoKey);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
debugPrint("Error logging in $e");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register device info
|
// Register device info
|
||||||
try {
|
try {
|
||||||
Response res = await _networkService
|
DeviceInfoResponseDto? deviceInfo =
|
||||||
.postRequest(url: 'device-info', data: {'deviceId': state.deviceId, 'deviceType': state.deviceType});
|
await _apiService.deviceInfoApi.createDeviceInfo(
|
||||||
|
CreateDeviceInfoDto(
|
||||||
|
deviceId: state.deviceId,
|
||||||
|
deviceType: state.deviceType,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (deviceInfo == null) {
|
||||||
|
debugPrint('Device Info Response is null');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
DeviceInfoRemote deviceInfo = DeviceInfoRemote.fromJson(res.toString());
|
|
||||||
state = state.copyWith(deviceInfo: deviceInfo);
|
state = state.copyWith(deviceInfo: deviceInfo);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("ERROR Register Device Info: $e");
|
debugPrint("ERROR Register Device Info: $e");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -121,27 +149,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
|
|
||||||
Future<bool> logout() async {
|
Future<bool> logout() async {
|
||||||
Hive.box(userInfoBox).delete(accessTokenKey);
|
Hive.box(userInfoBox).delete(accessTokenKey);
|
||||||
state = AuthenticationState(
|
state = state.copyWith(isAuthenticated: false);
|
||||||
deviceId: "",
|
|
||||||
deviceType: "",
|
|
||||||
userId: "",
|
|
||||||
userEmail: "",
|
|
||||||
firstName: '',
|
|
||||||
lastName: '',
|
|
||||||
profileImagePath: '',
|
|
||||||
isFirstLogin: false,
|
|
||||||
isAuthenticated: false,
|
|
||||||
isAdmin: false,
|
|
||||||
deviceInfo: DeviceInfoRemote(
|
|
||||||
id: 0,
|
|
||||||
userId: "",
|
|
||||||
deviceId: "",
|
|
||||||
deviceType: "",
|
|
||||||
notificationToken: "",
|
|
||||||
createdAt: "",
|
|
||||||
isAutoBackup: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -149,17 +157,44 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
setAutoBackup(bool backupState) async {
|
setAutoBackup(bool backupState) async {
|
||||||
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
var deviceInfo = await _deviceInfoService.getDeviceInfo();
|
||||||
var deviceId = deviceInfo["deviceId"];
|
var deviceId = deviceInfo["deviceId"];
|
||||||
var deviceType = deviceInfo["deviceType"];
|
|
||||||
|
|
||||||
DeviceInfoRemote deviceInfoRemote = await _backupService.setAutoBackup(backupState, deviceId, deviceType);
|
DeviceTypeEnum deviceType = deviceInfo["deviceType"];
|
||||||
state = state.copyWith(deviceInfo: deviceInfoRemote);
|
|
||||||
|
DeviceInfoResponseDto updatedDeviceInfo =
|
||||||
|
await _backupService.setAutoBackup(backupState, deviceId, deviceType);
|
||||||
|
|
||||||
|
state = state.copyWith(deviceInfo: updatedDeviceInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUserProfileImagePath(String path) {
|
updateUserProfileImagePath(String path) {
|
||||||
state = state.copyWith(profileImagePath: path);
|
state = state.copyWith(profileImagePath: path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> changePassword(String newPassword) async {
|
||||||
|
try {
|
||||||
|
await _apiService.userApi.updateUser(
|
||||||
|
UpdateUserDto(
|
||||||
|
id: state.userId,
|
||||||
|
password: newPassword,
|
||||||
|
shouldChangePassword: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
state = state.copyWith(shouldChangePassword: false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error changing password $e");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
|
final authenticationProvider =
|
||||||
return AuthenticationNotifier(ref);
|
StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
|
||||||
|
return AuthenticationNotifier(
|
||||||
|
ref.watch(deviceInfoServiceProvider),
|
||||||
|
ref.watch(backupServiceProvider),
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
175
mobile/lib/modules/login/ui/change_password_form.dart
Normal file
175
mobile/lib/modules/login/ui/change_password_form.dart
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
|
|
||||||
|
class ChangePasswordForm extends HookConsumerWidget {
|
||||||
|
const ChangePasswordForm({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final passwordController =
|
||||||
|
useTextEditingController.fromValue(TextEditingValue.empty);
|
||||||
|
final confirmPasswordController =
|
||||||
|
useTextEditingController.fromValue(TextEditingValue.empty);
|
||||||
|
final authState = ref.watch(authenticationProvider);
|
||||||
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 300),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 16,
|
||||||
|
alignment: WrapAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Change Password',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||||
|
child: Text(
|
||||||
|
'Hi ${authState.firstName} ${authState.lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Form(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
PasswordInput(controller: passwordController),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
|
child: ConfirmPasswordInput(
|
||||||
|
originalController: passwordController,
|
||||||
|
confirmController: confirmPasswordController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ChangePasswordButton(
|
||||||
|
passwordController: passwordController,
|
||||||
|
formKey: formKey,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PasswordInput extends StatelessWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
|
||||||
|
const PasswordInput({Key? key, required this.controller}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextFormField(
|
||||||
|
obscureText: true,
|
||||||
|
controller: controller,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'New Password',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
hintText: 'New Password',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConfirmPasswordInput extends StatelessWidget {
|
||||||
|
final TextEditingController originalController;
|
||||||
|
final TextEditingController confirmController;
|
||||||
|
|
||||||
|
const ConfirmPasswordInput({
|
||||||
|
Key? key,
|
||||||
|
required this.originalController,
|
||||||
|
required this.confirmController,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
String? _validateInput(String? email) {
|
||||||
|
if (confirmController.value != originalController.value) {
|
||||||
|
return 'Passwords do not match';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TextFormField(
|
||||||
|
obscureText: true,
|
||||||
|
controller: confirmController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Confirm Password',
|
||||||
|
hintText: 'Re-enter New Password',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
validator: _validateInput,
|
||||||
|
autovalidateMode: AutovalidateMode.always,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChangePasswordButton extends ConsumerWidget {
|
||||||
|
final TextEditingController passwordController;
|
||||||
|
final GlobalKey<FormState> formKey;
|
||||||
|
|
||||||
|
const ChangePasswordButton({
|
||||||
|
Key? key,
|
||||||
|
required this.passwordController,
|
||||||
|
required this.formKey,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
visualDensity: VisualDensity.standard,
|
||||||
|
primary: Theme.of(context).primaryColor,
|
||||||
|
onPrimary: Colors.grey[50],
|
||||||
|
elevation: 2,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
if (formKey.currentState!.validate()) {
|
||||||
|
var isSuccess = await ref
|
||||||
|
.watch(authenticationProvider.notifier)
|
||||||
|
.changePassword(passwordController.value.text);
|
||||||
|
|
||||||
|
if (isSuccess) {
|
||||||
|
bool res =
|
||||||
|
await ref.watch(authenticationProvider.notifier).logout();
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
ref.watch(backupProvider.notifier).cancelBackup();
|
||||||
|
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||||
|
ref.watch(websocketProvider.notifier).disconnect();
|
||||||
|
AutoRouter.of(context).replace(const LoginRoute());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"Change Password",
|
||||||
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package: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/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/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
@@ -15,23 +17,30 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final usernameController = useTextEditingController.fromValue(TextEditingValue.empty);
|
final usernameController =
|
||||||
final passwordController = useTextEditingController.fromValue(TextEditingValue.empty);
|
useTextEditingController.fromValue(TextEditingValue.empty);
|
||||||
final serverEndpointController = useTextEditingController(text: 'http://your-server-ip:2283');
|
final passwordController =
|
||||||
|
useTextEditingController.fromValue(TextEditingValue.empty);
|
||||||
|
final serverEndpointController =
|
||||||
|
useTextEditingController(text: 'login_form_endpoint_hint'.tr());
|
||||||
final isSaveLoginInfo = useState<bool>(false);
|
final isSaveLoginInfo = useState<bool>(false);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
|
() {
|
||||||
|
var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox)
|
||||||
|
.get(savedLoginInfoKey);
|
||||||
|
|
||||||
if (loginInfo != null) {
|
if (loginInfo != null) {
|
||||||
usernameController.text = loginInfo.email;
|
usernameController.text = loginInfo.email;
|
||||||
passwordController.text = loginInfo.password;
|
passwordController.text = loginInfo.password;
|
||||||
serverEndpointController.text = loginInfo.serverUrl;
|
serverEndpointController.text = loginInfo.serverUrl;
|
||||||
isSaveLoginInfo.value = loginInfo.isSaveLogin;
|
isSaveLoginInfo.value = loginInfo.isSaveLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
@@ -64,12 +73,18 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
dense: true,
|
dense: true,
|
||||||
side: const BorderSide(color: Colors.grey, width: 1.5),
|
side: const BorderSide(color: Colors.grey, width: 1.5),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
),
|
||||||
enableFeedback: true,
|
enableFeedback: true,
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Stay logged in",
|
"login_form_save_login",
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey),
|
style: TextStyle(
|
||||||
),
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
value: isSaveLoginInfo.value,
|
value: isSaveLoginInfo.value,
|
||||||
onChanged: (switchValue) {
|
onChanged: (switchValue) {
|
||||||
if (switchValue != null) {
|
if (switchValue != null) {
|
||||||
@@ -94,20 +109,26 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
class ServerEndpointInput extends StatelessWidget {
|
class ServerEndpointInput extends StatelessWidget {
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
|
|
||||||
const ServerEndpointInput({Key? key, required this.controller}) : super(key: key);
|
const ServerEndpointInput({Key? key, required this.controller})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
String? _validateInput(String? url) {
|
String? _validateInput(String? url) {
|
||||||
if (url == null) return null;
|
if (url?.startsWith(RegExp(r'https?://')) == true) {
|
||||||
if (!url.startsWith(RegExp(r'https?://'))) return 'Please specify http:// or https://';
|
return null;
|
||||||
return null;
|
} else {
|
||||||
|
return 'login_form_err_http'.tr();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Server Endpoint URL', border: OutlineInputBorder(), hintText: 'http://your-server-ip:port'),
|
labelText: 'login_form_endpoint_url'.tr(),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
hintText: 'login_form_endpoint_hint'.tr(),
|
||||||
|
),
|
||||||
validator: _validateInput,
|
validator: _validateInput,
|
||||||
autovalidateMode: AutovalidateMode.always,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
);
|
);
|
||||||
@@ -121,9 +142,11 @@ class EmailInput extends StatelessWidget {
|
|||||||
|
|
||||||
String? _validateInput(String? email) {
|
String? _validateInput(String? email) {
|
||||||
if (email == null || email == '') return null;
|
if (email == null || email == '') return null;
|
||||||
if (email.endsWith(' ')) return 'Trailing whitespace';
|
if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr();
|
||||||
if (email.startsWith(' ')) return 'Leading whitespace';
|
if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr();
|
||||||
if (email.contains(' ') || !email.contains('@')) return 'Invalid Email';
|
if (email.contains(' ') || !email.contains('@')) {
|
||||||
|
return 'login_form_err_invalid_email'.tr();
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,8 +154,11 @@ class EmailInput extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration:
|
decoration: InputDecoration(
|
||||||
const InputDecoration(labelText: 'Email', border: OutlineInputBorder(), hintText: 'youremail@email.com'),
|
labelText: 'login_form_label_email'.tr(),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
hintText: 'login_form_email_hint'.tr(),
|
||||||
|
),
|
||||||
validator: _validateInput,
|
validator: _validateInput,
|
||||||
autovalidateMode: AutovalidateMode.always,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
);
|
);
|
||||||
@@ -149,7 +175,11 @@ class PasswordInput extends StatelessWidget {
|
|||||||
return TextFormField(
|
return TextFormField(
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
decoration: const InputDecoration(labelText: 'Password', border: OutlineInputBorder(), hintText: 'password'),
|
decoration: InputDecoration(
|
||||||
|
labelText: 'login_form_label_password'.tr(),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
hintText: 'login_form_password_hint'.tr(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,36 +201,47 @@ class LoginButton extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
visualDensity: VisualDensity.standard,
|
visualDensity: VisualDensity.standard,
|
||||||
primary: Theme.of(context).primaryColor,
|
primary: Theme.of(context).primaryColor,
|
||||||
onPrimary: Colors.grey[50],
|
onPrimary: Colors.grey[50],
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// This will remove current cache asset state of previous user login.
|
// This will remove current cache asset state of previous user login.
|
||||||
ref.watch(assetProvider.notifier).clearAllAsset();
|
ref.watch(assetProvider.notifier).clearAllAsset();
|
||||||
|
|
||||||
var isAuthenticated = await ref
|
var isAuthenticated =
|
||||||
.read(authenticationProvider.notifier)
|
await ref.watch(authenticationProvider.notifier).login(
|
||||||
.login(emailController.text, passwordController.text, serverEndpointController.text, isSavedLoginInfo);
|
emailController.text,
|
||||||
|
passwordController.text,
|
||||||
|
serverEndpointController.text,
|
||||||
|
isSavedLoginInfo,
|
||||||
|
);
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
// Resume backup (if enable) then navigate
|
// Resume backup (if enable) then navigate
|
||||||
|
|
||||||
|
if (ref.watch(authenticationProvider).shouldChangePassword &&
|
||||||
|
!ref.watch(authenticationProvider).isAdmin) {
|
||||||
|
AutoRouter.of(context).push(const ChangePasswordRoute());
|
||||||
|
} else {
|
||||||
ref.watch(backupProvider.notifier).resumeBackup();
|
ref.watch(backupProvider.notifier).resumeBackup();
|
||||||
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
||||||
} else {
|
|
||||||
ImmichToast.show(
|
|
||||||
context: context,
|
|
||||||
msg: "Error logging you in, check server url, email and password!",
|
|
||||||
toastType: ToastType.error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
} else {
|
||||||
child: const Text(
|
ImmichToast.show(
|
||||||
"Login",
|
context: context,
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
msg: "login_form_failed_login".tr(),
|
||||||
));
|
toastType: ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
"login_form_button_text",
|
||||||
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
|
).tr(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
mobile/lib/modules/login/views/change_password_page.dart
Normal file
14
mobile/lib/modules/login/views/change_password_page.dart
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/ui/change_password_form.dart';
|
||||||
|
|
||||||
|
class ChangePasswordPage extends HookConsumerWidget {
|
||||||
|
const ChangePasswordPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: ChangePasswordForm(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class CuratedLocation {
|
|
||||||
final String id;
|
|
||||||
final String city;
|
|
||||||
final String resizePath;
|
|
||||||
final String deviceAssetId;
|
|
||||||
final String deviceId;
|
|
||||||
|
|
||||||
CuratedLocation({
|
|
||||||
required this.id,
|
|
||||||
required this.city,
|
|
||||||
required this.resizePath,
|
|
||||||
required this.deviceAssetId,
|
|
||||||
required this.deviceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
CuratedLocation copyWith({
|
|
||||||
String? id,
|
|
||||||
String? city,
|
|
||||||
String? resizePath,
|
|
||||||
String? deviceAssetId,
|
|
||||||
String? deviceId,
|
|
||||||
}) {
|
|
||||||
return CuratedLocation(
|
|
||||||
id: id ?? this.id,
|
|
||||||
city: city ?? this.city,
|
|
||||||
resizePath: resizePath ?? this.resizePath,
|
|
||||||
deviceAssetId: deviceAssetId ?? this.deviceAssetId,
|
|
||||||
deviceId: deviceId ?? this.deviceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'city': city,
|
|
||||||
'resizePath': resizePath,
|
|
||||||
'deviceAssetId': deviceAssetId,
|
|
||||||
'deviceId': deviceId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory CuratedLocation.fromMap(Map<String, dynamic> map) {
|
|
||||||
return CuratedLocation(
|
|
||||||
id: map['id'] ?? '',
|
|
||||||
city: map['city'] ?? '',
|
|
||||||
resizePath: map['resizePath'] ?? '',
|
|
||||||
deviceAssetId: map['deviceAssetId'] ?? '',
|
|
||||||
deviceId: map['deviceId'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory CuratedLocation.fromJson(String source) => CuratedLocation.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'CuratedLocation(id: $id, city: $city, resizePath: $resizePath, deviceAssetId: $deviceAssetId, deviceId: $deviceId)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is CuratedLocation &&
|
|
||||||
other.id == id &&
|
|
||||||
other.city == city &&
|
|
||||||
other.resizePath == resizePath &&
|
|
||||||
other.deviceAssetId == deviceAssetId &&
|
|
||||||
other.deviceId == deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return id.hashCode ^ city.hashCode ^ resizePath.hashCode ^ deviceAssetId.hashCode ^ deviceId.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
class CuratedObject {
|
|
||||||
final String id;
|
|
||||||
final String object;
|
|
||||||
final String resizePath;
|
|
||||||
final String deviceAssetId;
|
|
||||||
final String deviceId;
|
|
||||||
CuratedObject({
|
|
||||||
required this.id,
|
|
||||||
required this.object,
|
|
||||||
required this.resizePath,
|
|
||||||
required this.deviceAssetId,
|
|
||||||
required this.deviceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
CuratedObject copyWith({
|
|
||||||
String? id,
|
|
||||||
String? object,
|
|
||||||
String? resizePath,
|
|
||||||
String? deviceAssetId,
|
|
||||||
String? deviceId,
|
|
||||||
}) {
|
|
||||||
return CuratedObject(
|
|
||||||
id: id ?? this.id,
|
|
||||||
object: object ?? this.object,
|
|
||||||
resizePath: resizePath ?? this.resizePath,
|
|
||||||
deviceAssetId: deviceAssetId ?? this.deviceAssetId,
|
|
||||||
deviceId: deviceId ?? this.deviceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'id': id});
|
|
||||||
result.addAll({'object': object});
|
|
||||||
result.addAll({'resizePath': resizePath});
|
|
||||||
result.addAll({'deviceAssetId': deviceAssetId});
|
|
||||||
result.addAll({'deviceId': deviceId});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory CuratedObject.fromMap(Map<String, dynamic> map) {
|
|
||||||
return CuratedObject(
|
|
||||||
id: map['id'] ?? '',
|
|
||||||
object: map['object'] ?? '',
|
|
||||||
resizePath: map['resizePath'] ?? '',
|
|
||||||
deviceAssetId: map['deviceAssetId'] ?? '',
|
|
||||||
deviceId: map['deviceId'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory CuratedObject.fromJson(String source) => CuratedObject.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'CuratedObject(id: $id, object: $object, resizePath: $resizePath, deviceAssetId: $deviceAssetId, deviceId: $deviceId)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is CuratedObject &&
|
|
||||||
other.id == id &&
|
|
||||||
other.object == object &&
|
|
||||||
other.resizePath == resizePath &&
|
|
||||||
other.deviceAssetId == deviceAssetId &&
|
|
||||||
other.deviceId == deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return id.hashCode ^ object.hashCode ^ resizePath.hashCode ^ deviceAssetId.hashCode ^ deviceId.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,8 @@ class SearchPageState {
|
|||||||
searchTerm: searchTerm ?? this.searchTerm,
|
searchTerm: searchTerm ?? this.searchTerm,
|
||||||
isSearchEnabled: isSearchEnabled ?? this.isSearchEnabled,
|
isSearchEnabled: isSearchEnabled ?? this.isSearchEnabled,
|
||||||
searchSuggestion: searchSuggestion ?? this.searchSuggestion,
|
searchSuggestion: searchSuggestion ?? this.searchSuggestion,
|
||||||
userSuggestedSearchTerms: userSuggestedSearchTerms ?? this.userSuggestedSearchTerms,
|
userSuggestedSearchTerms:
|
||||||
|
userSuggestedSearchTerms ?? this.userSuggestedSearchTerms,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,13 +44,15 @@ class SearchPageState {
|
|||||||
searchTerm: map['searchTerm'] ?? '',
|
searchTerm: map['searchTerm'] ?? '',
|
||||||
isSearchEnabled: map['isSearchEnabled'] ?? false,
|
isSearchEnabled: map['isSearchEnabled'] ?? false,
|
||||||
searchSuggestion: List<String>.from(map['searchSuggestion']),
|
searchSuggestion: List<String>.from(map['searchSuggestion']),
|
||||||
userSuggestedSearchTerms: List<String>.from(map['userSuggestedSearchTerms']),
|
userSuggestedSearchTerms:
|
||||||
|
List<String>.from(map['userSuggestedSearchTerms']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
factory SearchPageState.fromJson(String source) => SearchPageState.fromMap(json.decode(source));
|
factory SearchPageState.fromJson(String source) =>
|
||||||
|
SearchPageState.fromMap(json.decode(source));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SearchResultPageState {
|
class SearchResultPageState {
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final bool isSuccess;
|
final bool isSuccess;
|
||||||
final bool isError;
|
final bool isError;
|
||||||
final List<ImmichAsset> searchResult;
|
final List<AssetResponseDto> searchResult;
|
||||||
|
|
||||||
SearchResultPageState({
|
SearchResultPageState({
|
||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
@@ -20,7 +20,7 @@ class SearchResultPageState {
|
|||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
bool? isSuccess,
|
bool? isSuccess,
|
||||||
bool? isError,
|
bool? isError,
|
||||||
List<ImmichAsset>? searchResult,
|
List<AssetResponseDto>? searchResult,
|
||||||
}) {
|
}) {
|
||||||
return SearchResultPageState(
|
return SearchResultPageState(
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
@@ -35,7 +35,7 @@ class SearchResultPageState {
|
|||||||
'isLoading': isLoading,
|
'isLoading': isLoading,
|
||||||
'isSuccess': isSuccess,
|
'isSuccess': isSuccess,
|
||||||
'isError': isError,
|
'isError': isError,
|
||||||
'searchResult': searchResult.map((x) => x.toMap()).toList(),
|
'searchResult': searchResult.map((x) => x.toJson()).toList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,13 +44,16 @@ class SearchResultPageState {
|
|||||||
isLoading: map['isLoading'] ?? false,
|
isLoading: map['isLoading'] ?? false,
|
||||||
isSuccess: map['isSuccess'] ?? false,
|
isSuccess: map['isSuccess'] ?? false,
|
||||||
isError: map['isError'] ?? false,
|
isError: map['isError'] ?? false,
|
||||||
searchResult: List<ImmichAsset>.from(map['searchResult']?.map((x) => ImmichAsset.fromMap(x))),
|
searchResult: List<AssetResponseDto>.from(
|
||||||
|
map['searchResult']?.map((x) => AssetResponseDto.mapFromJson(x)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
factory SearchResultPageState.fromJson(String source) => SearchResultPageState.fromMap(json.decode(source));
|
factory SearchResultPageState.fromJson(String source) =>
|
||||||
|
SearchResultPageState.fromMap(json.decode(source));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -71,6 +74,9 @@ class SearchResultPageState {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode {
|
int get hashCode {
|
||||||
return isLoading.hashCode ^ isSuccess.hashCode ^ isError.hashCode ^ searchResult.hashCode;
|
return isLoading.hashCode ^
|
||||||
|
isSuccess.hashCode ^
|
||||||
|
isError.hashCode ^
|
||||||
|
searchResult.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/search/models/curated_object.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/search/models/search_page_state.model.dart';
|
import 'package:immich_mobile/modules/search/models/search_page_state.model.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
|
class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
|
||||||
SearchPageStateNotifier()
|
SearchPageStateNotifier(this._searchService)
|
||||||
: super(
|
: super(
|
||||||
SearchPageState(
|
SearchPageState(
|
||||||
searchTerm: "",
|
searchTerm: "",
|
||||||
@@ -16,7 +15,7 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final SearchService _searchService = SearchService();
|
final SearchService _searchService;
|
||||||
|
|
||||||
void enableSearch() {
|
void enableSearch() {
|
||||||
state = state.copyWith(isSearchEnabled: true);
|
state = state.copyWith(isSearchEnabled: true);
|
||||||
@@ -54,29 +53,22 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
|
|||||||
|
|
||||||
final searchPageStateProvider =
|
final searchPageStateProvider =
|
||||||
StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
|
StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
|
||||||
return SearchPageStateNotifier();
|
return SearchPageStateNotifier(ref.watch(searchServiceProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
final getCuratedLocationProvider =
|
final getCuratedLocationProvider =
|
||||||
FutureProvider.autoDispose<List<CuratedLocation>>((ref) async {
|
FutureProvider.autoDispose<List<CuratedLocationsResponseDto>>((ref) async {
|
||||||
final SearchService searchService = SearchService();
|
final SearchService searchService = ref.watch(searchServiceProvider);
|
||||||
|
|
||||||
var curatedLocation = await searchService.getCuratedLocation();
|
var curatedLocation = await searchService.getCuratedLocation();
|
||||||
if (curatedLocation != null) {
|
return curatedLocation ?? [];
|
||||||
return curatedLocation;
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
final getCuratedObjectProvider =
|
final getCuratedObjectProvider =
|
||||||
FutureProvider.autoDispose<List<CuratedObject>>((ref) async {
|
FutureProvider.autoDispose<List<CuratedObjectsResponseDto>>((ref) async {
|
||||||
final SearchService searchService = SearchService();
|
final SearchService searchService = ref.watch(searchServiceProvider);
|
||||||
|
|
||||||
var curatedObject = await searchService.getCuratedObjects();
|
var curatedObject = await searchService.getCuratedObjects();
|
||||||
if (curatedObject != null) {
|
|
||||||
return curatedObject;
|
return curatedObject ?? [];
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,35 +3,66 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
|
import 'package:immich_mobile/modules/search/models/search_result_page_state.model.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
import 'package:immich_mobile/modules/search/services/search.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
|
||||||
SearchResultPageNotifier()
|
SearchResultPageNotifier(this._searchService)
|
||||||
: super(SearchResultPageState(searchResult: [], isError: false, isLoading: true, isSuccess: false));
|
: super(
|
||||||
|
SearchResultPageState(
|
||||||
|
searchResult: [],
|
||||||
|
isError: false,
|
||||||
|
isLoading: true,
|
||||||
|
isSuccess: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final SearchService _searchService = SearchService();
|
final SearchService _searchService;
|
||||||
|
|
||||||
void search(String searchTerm) async {
|
void search(String searchTerm) async {
|
||||||
state = state.copyWith(searchResult: [], isError: false, isLoading: true, isSuccess: false);
|
state = state.copyWith(
|
||||||
|
searchResult: [],
|
||||||
|
isError: false,
|
||||||
|
isLoading: true,
|
||||||
|
isSuccess: false,
|
||||||
|
);
|
||||||
|
|
||||||
List<ImmichAsset>? assets = await _searchService.searchAsset(searchTerm);
|
List<AssetResponseDto>? assets =
|
||||||
|
await _searchService.searchAsset(searchTerm);
|
||||||
|
|
||||||
if (assets != null) {
|
if (assets != null) {
|
||||||
state = state.copyWith(searchResult: assets, isError: false, isLoading: false, isSuccess: true);
|
state = state.copyWith(
|
||||||
|
searchResult: assets,
|
||||||
|
isError: false,
|
||||||
|
isLoading: false,
|
||||||
|
isSuccess: true,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
state = state.copyWith(searchResult: [], isError: true, isLoading: false, isSuccess: false);
|
state = state.copyWith(
|
||||||
|
searchResult: [],
|
||||||
|
isError: true,
|
||||||
|
isLoading: false,
|
||||||
|
isSuccess: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final searchResultPageProvider = StateNotifierProvider<SearchResultPageNotifier, SearchResultPageState>((ref) {
|
final searchResultPageProvider =
|
||||||
return SearchResultPageNotifier();
|
StateNotifierProvider<SearchResultPageNotifier, SearchResultPageState>(
|
||||||
|
(ref) {
|
||||||
|
return SearchResultPageNotifier(ref.watch(searchServiceProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
final searchResultGroupByDateTimeProvider = StateProvider((ref) {
|
final searchResultGroupByDateTimeProvider = StateProvider((ref) {
|
||||||
var assets = ref.watch(searchResultPageProvider).searchResult;
|
var assets = ref.watch(searchResultPageProvider).searchResult;
|
||||||
|
|
||||||
assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
|
assets.sortByCompare<DateTime>(
|
||||||
return assets.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
|
(e) => DateTime.parse(e.createdAt),
|
||||||
|
(a, b) => b.compareTo(a),
|
||||||
|
);
|
||||||
|
return assets.groupListsBy(
|
||||||
|
(element) =>
|
||||||
|
DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,71 +1,54 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_location.model.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/search/models/curated_object.model.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
|
||||||
|
final searchServiceProvider = Provider(
|
||||||
|
(ref) => SearchService(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
class SearchService {
|
class SearchService {
|
||||||
final NetworkService _networkService = NetworkService();
|
final ApiService _apiService;
|
||||||
|
SearchService(this._apiService);
|
||||||
|
|
||||||
Future<List<String>?> getUserSuggestedSearchTerms() async {
|
Future<List<String>?> getUserSuggestedSearchTerms() async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.getRequest(url: "asset/searchTerm");
|
return await _apiService.assetApi.getAssetSearchTerms();
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
return List.from(decodedData);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] [getUserSuggestedSearchTerms] ${e.toString()}");
|
debugPrint("[ERROR] [getUserSuggestedSearchTerms] ${e.toString()}");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<ImmichAsset>?> searchAsset(String searchTerm) async {
|
Future<List<AssetResponseDto>?> searchAsset(String searchTerm) async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.postRequest(
|
return await _apiService.assetApi
|
||||||
url: "asset/search",
|
.searchAsset(SearchAssetDto(searchTerm: searchTerm));
|
||||||
data: {"searchTerm": searchTerm},
|
|
||||||
);
|
|
||||||
|
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
List<ImmichAsset> result = List.from(decodedData.map((a) => ImmichAsset.fromMap(a)));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] [searchAsset] ${e.toString()}");
|
debugPrint("[ERROR] [searchAsset] ${e.toString()}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<CuratedLocation>?> getCuratedLocation() async {
|
Future<List<CuratedLocationsResponseDto>?> getCuratedLocation() async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.getRequest(url: "asset/allLocation");
|
var locations = await _apiService.assetApi.getCuratedLocations();
|
||||||
|
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
return locations;
|
||||||
|
|
||||||
List<CuratedLocation> result = List.from(decodedData.map((a) => CuratedLocation.fromMap(a)));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] [getCuratedLocation] ${e.toString()}");
|
debugPrint("Error [getCuratedLocation] ${e.toString()}");
|
||||||
throw Error();
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<CuratedObject>?> getCuratedObjects() async {
|
Future<List<CuratedObjectsResponseDto>?> getCuratedObjects() async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.getRequest(url: "asset/allObjects");
|
return await _apiService.assetApi.getCuratedObjects();
|
||||||
|
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
|
|
||||||
List<CuratedObject> result = List.from(decodedData.map((a) => CuratedObject.fromMap(a)));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("[ERROR] [CuratedObject] ${e.toString()}");
|
debugPrint("Error [getCuratedObjects] ${e.toString()}");
|
||||||
throw Error();
|
throw [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
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/search/providers/search_page_state.provider.dart';
|
import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
|
||||||
|
|
||||||
class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
|
class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
|
||||||
SearchBar({Key? key, required this.searchFocusNode, required this.onSubmitted}) : super(key: key);
|
SearchBar({
|
||||||
|
Key? key,
|
||||||
|
required this.searchFocusNode,
|
||||||
|
required this.onSubmitted,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
final FocusNode searchFocusNode;
|
final FocusNode searchFocusNode;
|
||||||
final Function(String) onSubmitted;
|
final Function(String) onSubmitted;
|
||||||
@@ -23,7 +28,8 @@ class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
ref.watch(searchPageStateProvider.notifier).disableSearch();
|
ref.watch(searchPageStateProvider.notifier).disableSearch();
|
||||||
searchTermController.clear();
|
searchTermController.clear();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded))
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
|
)
|
||||||
: const Icon(Icons.search_rounded),
|
: const Icon(Icons.search_rounded),
|
||||||
title: TextField(
|
title: TextField(
|
||||||
controller: searchTermController,
|
controller: searchTermController,
|
||||||
@@ -45,12 +51,12 @@ class SearchBar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
ref.watch(searchPageStateProvider.notifier).setSearchTerm(value);
|
ref.watch(searchPageStateProvider.notifier).setSearchTerm(value);
|
||||||
},
|
},
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Search your photos',
|
hintText: 'search_bar_hint'.tr(),
|
||||||
enabledBorder: UnderlineInputBorder(
|
enabledBorder: const UnderlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.transparent),
|
borderSide: BorderSide(color: Colors.transparent),
|
||||||
),
|
),
|
||||||
focusedBorder: UnderlineInputBorder(
|
focusedBorder: const UnderlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.transparent),
|
borderSide: BorderSide(color: Colors.transparent),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,16 +3,20 @@ import 'package:hooks_riverpod/hooks_riverpod.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';
|
||||||
|
|
||||||
class SearchSuggestionList extends ConsumerWidget {
|
class SearchSuggestionList extends ConsumerWidget {
|
||||||
const SearchSuggestionList({Key? key, required this.onSubmitted}) : super(key: key);
|
const SearchSuggestionList({Key? key, required this.onSubmitted})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
final Function(String) onSubmitted;
|
final Function(String) onSubmitted;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final searchTerm = ref.watch(searchPageStateProvider).searchTerm;
|
final searchTerm = ref.watch(searchPageStateProvider).searchTerm;
|
||||||
final searchSuggestion = ref.watch(searchPageStateProvider).searchSuggestion;
|
final searchSuggestion =
|
||||||
|
ref.watch(searchPageStateProvider).searchSuggestion;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
color: searchTerm.isEmpty ? Colors.black.withOpacity(0.5) : Theme.of(context).scaffoldBackgroundColor,
|
color: searchTerm.isEmpty
|
||||||
|
? Colors.black.withOpacity(0.5)
|
||||||
|
: Theme.of(context).scaffoldBackgroundColor,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverFillRemaining(
|
SliverFillRemaining(
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import 'package:cached_network_image/cached_network_image.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
import 'package:immich_mobile/utils/capitalize_first_letter.dart';
|
|
||||||
|
|
||||||
class ThumbnailWithInfo extends StatelessWidget {
|
class ThumbnailWithInfo extends StatelessWidget {
|
||||||
const ThumbnailWithInfo({Key? key, required this.textInfo, required this.imageUrl, required this.onTap})
|
const ThumbnailWithInfo({
|
||||||
: super(key: key);
|
Key? key,
|
||||||
|
required this.textInfo,
|
||||||
|
required this.imageUrl,
|
||||||
|
required this.onTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
final String textInfo;
|
final String textInfo;
|
||||||
final String imageUrl;
|
final String imageUrl;
|
||||||
@@ -39,7 +42,9 @@ class ThumbnailWithInfo extends StatelessWidget {
|
|||||||
height: 250,
|
height: 250,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
httpHeaders: {
|
||||||
|
"Authorization": "Bearer ${box.get(accessTokenKey)}"
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -49,7 +54,7 @@ class ThumbnailWithInfo extends StatelessWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: MediaQuery.of(context).size.width / 3,
|
width: MediaQuery.of(context).size.width / 3,
|
||||||
child: Text(
|
child: Text(
|
||||||
textInfo.capitalizeFirstLetter(),
|
textInfo,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package: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/search/models/curated_location.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/search/models/curated_object.model.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/ui/search_bar.dart';
|
import 'package:immich_mobile/modules/search/ui/search_bar.dart';
|
||||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
|
||||||
@@ -13,26 +12,30 @@ import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.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/utils/capitalize_first_letter.dart';
|
import 'package:immich_mobile/utils/capitalize_first_letter.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class SearchPage extends HookConsumerWidget {
|
class SearchPage extends HookConsumerWidget {
|
||||||
SearchPage({Key? key}) : super(key: key);
|
SearchPage({Key? key}) : super(key: key);
|
||||||
|
|
||||||
late FocusNode searchFocusNode;
|
FocusNode searchFocusNode = FocusNode();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
|
final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled;
|
||||||
AsyncValue<List<CuratedLocation>> curatedLocation =
|
AsyncValue<List<CuratedLocationsResponseDto>> curatedLocation =
|
||||||
ref.watch(getCuratedLocationProvider);
|
ref.watch(getCuratedLocationProvider);
|
||||||
AsyncValue<List<CuratedObject>> curatedObjects =
|
AsyncValue<List<CuratedObjectsResponseDto>> curatedObjects =
|
||||||
ref.watch(getCuratedObjectProvider);
|
ref.watch(getCuratedObjectProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
searchFocusNode = FocusNode();
|
() {
|
||||||
return () => searchFocusNode.dispose();
|
searchFocusNode = FocusNode();
|
||||||
}, []);
|
return () => searchFocusNode.dispose();
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
_onSearchSubmitted(String searchTerm) async {
|
_onSearchSubmitted(String searchTerm) async {
|
||||||
searchFocusNode.unfocus();
|
searchFocusNode.unfocus();
|
||||||
@@ -57,16 +60,16 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: curatedLocation.value?.length,
|
itemCount: curatedLocation.value?.length,
|
||||||
itemBuilder: ((context, index) {
|
itemBuilder: ((context, index) {
|
||||||
CuratedLocation locationInfo = curatedLocations[index];
|
var locationInfo = curatedLocations[index];
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl =
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${locationInfo.deviceAssetId}&did=${locationInfo.deviceId}&isThumb=true';
|
'${box.get(serverEndpointKey)}/asset/thumbnail/${locationInfo.id}';
|
||||||
|
|
||||||
return ThumbnailWithInfo(
|
return ThumbnailWithInfo(
|
||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
textInfo: locationInfo.city,
|
textInfo: locationInfo.city,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
SearchResultRoute(searchTerm: locationInfo.city));
|
SearchResultRoute(searchTerm: locationInfo.city),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -82,7 +85,7 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
return ThumbnailWithInfo(
|
return ThumbnailWithInfo(
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60',
|
'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60',
|
||||||
textInfo: 'No Places Info Available',
|
textInfo: 'search_page_no_places'.tr(),
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -108,7 +111,7 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: curatedObjects.value?.length,
|
itemCount: curatedObjects.value?.length,
|
||||||
itemBuilder: ((context, index) {
|
itemBuilder: ((context, index) {
|
||||||
CuratedObject curatedObjectInfo = objects[index];
|
var curatedObjectInfo = objects[index];
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl =
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${curatedObjectInfo.deviceAssetId}&did=${curatedObjectInfo.deviceId}&isThumb=true';
|
'${box.get(serverEndpointKey)}/asset/file?aid=${curatedObjectInfo.deviceAssetId}&did=${curatedObjectInfo.deviceId}&isThumb=true';
|
||||||
|
|
||||||
@@ -116,9 +119,12 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
textInfo: curatedObjectInfo.object,
|
textInfo: curatedObjectInfo.object,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
AutoRouter.of(context).push(SearchResultRoute(
|
AutoRouter.of(context).push(
|
||||||
|
SearchResultRoute(
|
||||||
searchTerm: curatedObjectInfo.object
|
searchTerm: curatedObjectInfo.object
|
||||||
.capitalizeFirstLetter()));
|
.capitalizeFirstLetter(),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -134,7 +140,7 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
return ThumbnailWithInfo(
|
return ThumbnailWithInfo(
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60',
|
'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60',
|
||||||
textInfo: 'No Object Info Available',
|
textInfo: 'sarch_no_objects'.tr(),
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -158,27 +164,26 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
ListView(
|
ListView(
|
||||||
children: [
|
children: [
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Text(
|
child: const Text(
|
||||||
"Places",
|
"search_page_places",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
_buildPlaces(),
|
_buildPlaces(),
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Text(
|
child: const Text(
|
||||||
"Things",
|
"search_page_things",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
),
|
).tr(),
|
||||||
),
|
),
|
||||||
_buildThings()
|
_buildThings()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
isSearchEnabled
|
if (isSearchEnabled)
|
||||||
? SearchSuggestionList(onSubmitted: _onSearchSubmitted)
|
SearchSuggestionList(onSubmitted: _onSearchSubmitted),
|
||||||
: Container(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||||
@@ -28,13 +29,18 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
late FocusNode searchFocusNode;
|
late FocusNode searchFocusNode;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
searchFocusNode = FocusNode();
|
() {
|
||||||
|
searchFocusNode = FocusNode();
|
||||||
|
|
||||||
Future.delayed(Duration.zero,
|
Future.delayed(
|
||||||
() => ref.read(searchResultPageProvider.notifier).search(searchTerm));
|
Duration.zero,
|
||||||
return () => searchFocusNode.dispose();
|
() => ref.read(searchResultPageProvider.notifier).search(searchTerm),
|
||||||
}, []);
|
);
|
||||||
|
return () => searchFocusNode.dispose();
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
_onSearchSubmitted(String newSearchTerm) {
|
_onSearchSubmitted(String newSearchTerm) {
|
||||||
debugPrint("Re-Search with $newSearchTerm");
|
debugPrint("Re-Search with $newSearchTerm");
|
||||||
@@ -66,12 +72,12 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
ref.watch(searchPageStateProvider.notifier).setSearchTerm(value);
|
ref.watch(searchPageStateProvider.notifier).setSearchTerm(value);
|
||||||
},
|
},
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'New Search',
|
hintText: 'search_result_page_new_search_hint'.tr(),
|
||||||
enabledBorder: UnderlineInputBorder(
|
enabledBorder: const UnderlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.transparent),
|
borderSide: BorderSide(color: Colors.transparent),
|
||||||
),
|
),
|
||||||
focusedBorder: UnderlineInputBorder(
|
focusedBorder: const UnderlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.transparent),
|
borderSide: BorderSide(color: Colors.transparent),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -89,9 +95,10 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
Text(
|
Text(
|
||||||
currentSearchTerm.value,
|
currentSearchTerm.value,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
Icon(
|
Icon(
|
||||||
@@ -115,9 +122,10 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
if (searchResultPageState.isLoading) {
|
if (searchResultPageState.isLoading) {
|
||||||
return Center(
|
return Center(
|
||||||
child: SpinKitDancingSquare(
|
child: SpinKitDancingSquare(
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchResultPageState.isSuccess) {
|
if (searchResultPageState.isSuccess) {
|
||||||
@@ -166,7 +174,7 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -183,11 +191,12 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
),
|
),
|
||||||
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(),
|
||||||
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: GestureDetector(
|
body: GestureDetector(
|
||||||
@@ -198,9 +207,8 @@ class SearchResultPage extends HookConsumerWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildSearchResult(),
|
_buildSearchResult(),
|
||||||
isNewSearch.value
|
if (isNewSearch.value)
|
||||||
? SearchSuggestionList(onSubmitted: _onSearchSubmitted)
|
SearchSuggestionList(onSubmitted: _onSearchSubmitted),
|
||||||
: Container(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -36,16 +36,20 @@ class AlbumViewerPageState {
|
|||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
factory AlbumViewerPageState.fromJson(String source) => AlbumViewerPageState.fromMap(json.decode(source));
|
factory AlbumViewerPageState.fromJson(String source) =>
|
||||||
|
AlbumViewerPageState.fromMap(json.decode(source));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)';
|
String toString() =>
|
||||||
|
'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
return other is AlbumViewerPageState && other.isEditAlbum == isEditAlbum && other.editTitleText == editTitleText;
|
return other is AlbumViewerPageState &&
|
||||||
|
other.isEditAlbum == isEditAlbum &&
|
||||||
|
other.editTitleText == editTitleText;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AssetSelectionPageResult {
|
class AssetSelectionPageResult {
|
||||||
final Set<ImmichAsset> selectedNewAsset;
|
final Set<AssetResponseDto> selectedNewAsset;
|
||||||
final Set<ImmichAsset> selectedAdditionalAsset;
|
final Set<AssetResponseDto> selectedAdditionalAsset;
|
||||||
final bool isAlbumExist;
|
final bool isAlbumExist;
|
||||||
|
|
||||||
AssetSelectionPageResult({
|
AssetSelectionPageResult({
|
||||||
@@ -16,40 +14,18 @@ class AssetSelectionPageResult {
|
|||||||
});
|
});
|
||||||
|
|
||||||
AssetSelectionPageResult copyWith({
|
AssetSelectionPageResult copyWith({
|
||||||
Set<ImmichAsset>? selectedNewAsset,
|
Set<AssetResponseDto>? selectedNewAsset,
|
||||||
Set<ImmichAsset>? selectedAdditionalAsset,
|
Set<AssetResponseDto>? selectedAdditionalAsset,
|
||||||
bool? isAlbumExist,
|
bool? isAlbumExist,
|
||||||
}) {
|
}) {
|
||||||
return AssetSelectionPageResult(
|
return AssetSelectionPageResult(
|
||||||
selectedNewAsset: selectedNewAsset ?? this.selectedNewAsset,
|
selectedNewAsset: selectedNewAsset ?? this.selectedNewAsset,
|
||||||
selectedAdditionalAsset: selectedAdditionalAsset ?? this.selectedAdditionalAsset,
|
selectedAdditionalAsset:
|
||||||
|
selectedAdditionalAsset ?? this.selectedAdditionalAsset,
|
||||||
isAlbumExist: isAlbumExist ?? this.isAlbumExist,
|
isAlbumExist: isAlbumExist ?? this.isAlbumExist,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'selectedNewAsset': selectedNewAsset.map((x) => x.toMap()).toList()});
|
|
||||||
result.addAll({'selectedAdditionalAsset': selectedAdditionalAsset.map((x) => x.toMap()).toList()});
|
|
||||||
result.addAll({'isAlbumExist': isAlbumExist});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory AssetSelectionPageResult.fromMap(Map<String, dynamic> map) {
|
|
||||||
return AssetSelectionPageResult(
|
|
||||||
selectedNewAsset: Set<ImmichAsset>.from(map['selectedNewAsset']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
selectedAdditionalAsset:
|
|
||||||
Set<ImmichAsset>.from(map['selectedAdditionalAsset']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
isAlbumExist: map['isAlbumExist'] ?? false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory AssetSelectionPageResult.fromJson(String source) => AssetSelectionPageResult.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
String toString() =>
|
||||||
'AssetSelectionPageResult(selectedNewAsset: $selectedNewAsset, selectedAdditionalAsset: $selectedAdditionalAsset, isAlbumExist: $isAlbumExist)';
|
'AssetSelectionPageResult(selectedNewAsset: $selectedNewAsset, selectedAdditionalAsset: $selectedAdditionalAsset, isAlbumExist: $isAlbumExist)';
|
||||||
@@ -66,5 +42,8 @@ class AssetSelectionPageResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => selectedNewAsset.hashCode ^ selectedAdditionalAsset.hashCode ^ isAlbumExist.hashCode;
|
int get hashCode =>
|
||||||
|
selectedNewAsset.hashCode ^
|
||||||
|
selectedAdditionalAsset.hashCode ^
|
||||||
|
isAlbumExist.hashCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AssetSelectionState {
|
class AssetSelectionState {
|
||||||
final Set<String> selectedMonths;
|
final Set<String> selectedMonths;
|
||||||
final Set<ImmichAsset> selectedNewAssetsForAlbum;
|
final Set<AssetResponseDto> selectedNewAssetsForAlbum;
|
||||||
final Set<ImmichAsset> selectedAdditionalAssetsForAlbum;
|
final Set<AssetResponseDto> selectedAdditionalAssetsForAlbum;
|
||||||
final Set<ImmichAsset> selectedAssetsInAlbumViewer;
|
final Set<AssetResponseDto> selectedAssetsInAlbumViewer;
|
||||||
final bool isMultiselectEnable;
|
final bool isMultiselectEnable;
|
||||||
|
|
||||||
/// Indicate the asset selection page is navigated from existing album
|
/// Indicate the asset selection page is navigated from existing album
|
||||||
@@ -24,54 +22,25 @@ class AssetSelectionState {
|
|||||||
|
|
||||||
AssetSelectionState copyWith({
|
AssetSelectionState copyWith({
|
||||||
Set<String>? selectedMonths,
|
Set<String>? selectedMonths,
|
||||||
Set<ImmichAsset>? selectedNewAssetsForAlbum,
|
Set<AssetResponseDto>? selectedNewAssetsForAlbum,
|
||||||
Set<ImmichAsset>? selectedAdditionalAssetsForAlbum,
|
Set<AssetResponseDto>? selectedAdditionalAssetsForAlbum,
|
||||||
Set<ImmichAsset>? selectedAssetsInAlbumViewer,
|
Set<AssetResponseDto>? selectedAssetsInAlbumViewer,
|
||||||
bool? isMultiselectEnable,
|
bool? isMultiselectEnable,
|
||||||
bool? isAlbumExist,
|
bool? isAlbumExist,
|
||||||
}) {
|
}) {
|
||||||
return AssetSelectionState(
|
return AssetSelectionState(
|
||||||
selectedMonths: selectedMonths ?? this.selectedMonths,
|
selectedMonths: selectedMonths ?? this.selectedMonths,
|
||||||
selectedNewAssetsForAlbum: selectedNewAssetsForAlbum ?? this.selectedNewAssetsForAlbum,
|
selectedNewAssetsForAlbum:
|
||||||
selectedAdditionalAssetsForAlbum: selectedAdditionalAssetsForAlbum ?? this.selectedAdditionalAssetsForAlbum,
|
selectedNewAssetsForAlbum ?? this.selectedNewAssetsForAlbum,
|
||||||
selectedAssetsInAlbumViewer: selectedAssetsInAlbumViewer ?? this.selectedAssetsInAlbumViewer,
|
selectedAdditionalAssetsForAlbum: selectedAdditionalAssetsForAlbum ??
|
||||||
|
this.selectedAdditionalAssetsForAlbum,
|
||||||
|
selectedAssetsInAlbumViewer:
|
||||||
|
selectedAssetsInAlbumViewer ?? this.selectedAssetsInAlbumViewer,
|
||||||
isMultiselectEnable: isMultiselectEnable ?? this.isMultiselectEnable,
|
isMultiselectEnable: isMultiselectEnable ?? this.isMultiselectEnable,
|
||||||
isAlbumExist: isAlbumExist ?? this.isAlbumExist,
|
isAlbumExist: isAlbumExist ?? this.isAlbumExist,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'selectedMonths': selectedMonths.toList()});
|
|
||||||
result.addAll({'selectedNewAssetsForAlbum': selectedNewAssetsForAlbum.map((x) => x.toMap()).toList()});
|
|
||||||
result
|
|
||||||
.addAll({'selectedAdditionalAssetsForAlbum': selectedAdditionalAssetsForAlbum.map((x) => x.toMap()).toList()});
|
|
||||||
result.addAll({'selectedAssetsInAlbumViewer': selectedAssetsInAlbumViewer.map((x) => x.toMap()).toList()});
|
|
||||||
result.addAll({'isMultiselectEnable': isMultiselectEnable});
|
|
||||||
result.addAll({'isAlbumExist': isAlbumExist});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory AssetSelectionState.fromMap(Map<String, dynamic> map) {
|
|
||||||
return AssetSelectionState(
|
|
||||||
selectedMonths: Set<String>.from(map['selectedMonths']),
|
|
||||||
selectedNewAssetsForAlbum:
|
|
||||||
Set<ImmichAsset>.from(map['selectedNewAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
selectedAdditionalAssetsForAlbum:
|
|
||||||
Set<ImmichAsset>.from(map['selectedAdditionalAssetsForAlbum']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
selectedAssetsInAlbumViewer:
|
|
||||||
Set<ImmichAsset>.from(map['selectedAssetsInAlbumViewer']?.map((x) => ImmichAsset.fromMap(x))),
|
|
||||||
isMultiselectEnable: map['isMultiselectEnable'] ?? false,
|
|
||||||
isAlbumExist: map['isAlbumExist'] ?? false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory AssetSelectionState.fromJson(String source) => AssetSelectionState.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AssetSelectionState(selectedMonths: $selectedMonths, selectedNewAssetsForAlbum: $selectedNewAssetsForAlbum, selectedAdditionalAssetsForAlbum: $selectedAdditionalAssetsForAlbum, selectedAssetsInAlbumViewer: $selectedAssetsInAlbumViewer, isMultiselectEnable: $isMultiselectEnable, isAlbumExist: $isAlbumExist)';
|
return 'AssetSelectionState(selectedMonths: $selectedMonths, selectedNewAssetsForAlbum: $selectedNewAssetsForAlbum, selectedAdditionalAssetsForAlbum: $selectedAdditionalAssetsForAlbum, selectedAssetsInAlbumViewer: $selectedAssetsInAlbumViewer, isMultiselectEnable: $isMultiselectEnable, isAlbumExist: $isAlbumExist)';
|
||||||
@@ -85,8 +54,14 @@ class AssetSelectionState {
|
|||||||
return other is AssetSelectionState &&
|
return other is AssetSelectionState &&
|
||||||
setEquals(other.selectedMonths, selectedMonths) &&
|
setEquals(other.selectedMonths, selectedMonths) &&
|
||||||
setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) &&
|
setEquals(other.selectedNewAssetsForAlbum, selectedNewAssetsForAlbum) &&
|
||||||
setEquals(other.selectedAdditionalAssetsForAlbum, selectedAdditionalAssetsForAlbum) &&
|
setEquals(
|
||||||
setEquals(other.selectedAssetsInAlbumViewer, selectedAssetsInAlbumViewer) &&
|
other.selectedAdditionalAssetsForAlbum,
|
||||||
|
selectedAdditionalAssetsForAlbum,
|
||||||
|
) &&
|
||||||
|
setEquals(
|
||||||
|
other.selectedAssetsInAlbumViewer,
|
||||||
|
selectedAssetsInAlbumViewer,
|
||||||
|
) &&
|
||||||
other.isMultiselectEnable == isMultiselectEnable &&
|
other.isMultiselectEnable == isMultiselectEnable &&
|
||||||
other.isAlbumExist == isAlbumExist;
|
other.isAlbumExist == isAlbumExist;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/user.model.dart';
|
|
||||||
|
|
||||||
class SharedAlbum {
|
|
||||||
final String id;
|
|
||||||
final String ownerId;
|
|
||||||
final String albumName;
|
|
||||||
final String createdAt;
|
|
||||||
final String? albumThumbnailAssetId;
|
|
||||||
final List<User> sharedUsers;
|
|
||||||
final List<ImmichAsset>? assets;
|
|
||||||
|
|
||||||
SharedAlbum({
|
|
||||||
required this.id,
|
|
||||||
required this.ownerId,
|
|
||||||
required this.albumName,
|
|
||||||
required this.createdAt,
|
|
||||||
required this.albumThumbnailAssetId,
|
|
||||||
required this.sharedUsers,
|
|
||||||
this.assets,
|
|
||||||
});
|
|
||||||
|
|
||||||
SharedAlbum copyWith({
|
|
||||||
String? id,
|
|
||||||
String? ownerId,
|
|
||||||
String? albumName,
|
|
||||||
String? createdAt,
|
|
||||||
String? albumThumbnailAssetId,
|
|
||||||
List<User>? sharedUsers,
|
|
||||||
List<ImmichAsset>? assets,
|
|
||||||
}) {
|
|
||||||
return SharedAlbum(
|
|
||||||
id: id ?? this.id,
|
|
||||||
ownerId: ownerId ?? this.ownerId,
|
|
||||||
albumName: albumName ?? this.albumName,
|
|
||||||
createdAt: createdAt ?? this.createdAt,
|
|
||||||
albumThumbnailAssetId: albumThumbnailAssetId ?? this.albumThumbnailAssetId,
|
|
||||||
sharedUsers: sharedUsers ?? this.sharedUsers,
|
|
||||||
assets: assets ?? this.assets,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
|
|
||||||
result.addAll({'id': id});
|
|
||||||
result.addAll({'ownerId': ownerId});
|
|
||||||
result.addAll({'albumName': albumName});
|
|
||||||
result.addAll({'createdAt': createdAt});
|
|
||||||
if (albumThumbnailAssetId != null) {
|
|
||||||
result.addAll({'albumThumbnailAssetId': albumThumbnailAssetId});
|
|
||||||
}
|
|
||||||
result.addAll({'sharedUsers': sharedUsers.map((x) => x.toMap()).toList()});
|
|
||||||
if (assets != null) {
|
|
||||||
result.addAll({'assets': assets!.map((x) => x.toMap()).toList()});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory SharedAlbum.fromMap(Map<String, dynamic> map) {
|
|
||||||
return SharedAlbum(
|
|
||||||
id: map['id'] ?? '',
|
|
||||||
ownerId: map['ownerId'] ?? '',
|
|
||||||
albumName: map['albumName'] ?? '',
|
|
||||||
createdAt: map['createdAt'] ?? '',
|
|
||||||
albumThumbnailAssetId: map['albumThumbnailAssetId'],
|
|
||||||
sharedUsers: List<User>.from(map['sharedUsers']?.map((x) => User.fromMap(x))),
|
|
||||||
assets: map['assets'] != null
|
|
||||||
? List<ImmichAsset>.from(map['assets']?.map((x) => ImmichAsset.fromMap(x)))
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String toJson() => json.encode(toMap());
|
|
||||||
|
|
||||||
factory SharedAlbum.fromJson(String source) => SharedAlbum.fromMap(json.decode(source));
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'SharedAlbum(id: $id, ownerId: $ownerId, albumName: $albumName, createdAt: $createdAt, albumThumbnailAssetId: $albumThumbnailAssetId, sharedUsers: $sharedUsers, assets: $assets)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
final listEquals = const DeepCollectionEquality().equals;
|
|
||||||
|
|
||||||
return other is SharedAlbum &&
|
|
||||||
other.id == id &&
|
|
||||||
other.ownerId == ownerId &&
|
|
||||||
other.albumName == albumName &&
|
|
||||||
other.createdAt == createdAt &&
|
|
||||||
other.albumThumbnailAssetId == albumThumbnailAssetId &&
|
|
||||||
listEquals(other.sharedUsers, sharedUsers) &&
|
|
||||||
listEquals(other.assets, assets);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return id.hashCode ^
|
|
||||||
ownerId.hashCode ^
|
|
||||||
albumName.hashCode ^
|
|
||||||
createdAt.hashCode ^
|
|
||||||
albumThumbnailAssetId.hashCode ^
|
|
||||||
sharedUsers.hashCode ^
|
|
||||||
assets.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,4 +12,6 @@ class AlbumTitleNotifier extends StateNotifier<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>((ref) => AlbumTitleNotifier());
|
final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>(
|
||||||
|
(ref) => AlbumTitleNotifier(),
|
||||||
|
);
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.da
|
|||||||
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
||||||
|
|
||||||
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||||
AlbumViewerNotifier(this.ref) : super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false));
|
AlbumViewerNotifier(this.ref)
|
||||||
|
: super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false));
|
||||||
|
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
|
|
||||||
@@ -28,10 +29,15 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
|||||||
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> changeAlbumTitle(String albumId, String ownerId, String newAlbumTitle) async {
|
Future<bool> changeAlbumTitle(
|
||||||
SharedAlbumService service = SharedAlbumService();
|
String albumId,
|
||||||
|
String ownerId,
|
||||||
|
String newAlbumTitle,
|
||||||
|
) async {
|
||||||
|
SharedAlbumService service = ref.watch(sharedAlbumServiceProvider);
|
||||||
|
|
||||||
bool isSuccess = await service.changeTitleAlbum(albumId, ownerId, newAlbumTitle);
|
bool isSuccess =
|
||||||
|
await service.changeTitleAlbum(albumId, ownerId, newAlbumTitle);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
||||||
@@ -45,6 +51,7 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final albumViewerProvider = StateNotifierProvider<AlbumViewerNotifier, AlbumViewerPageState>((ref) {
|
final albumViewerProvider =
|
||||||
|
StateNotifierProvider<AlbumViewerNotifier, AlbumViewerPageState>((ref) {
|
||||||
return AlbumViewerNotifier(ref);
|
return AlbumViewerNotifier(ref);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,69 +1,87 @@
|
|||||||
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/sharing/models/asset_selection_state.model.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
||||||
AssetSelectionNotifier()
|
AssetSelectionNotifier()
|
||||||
: super(AssetSelectionState(
|
: super(
|
||||||
selectedNewAssetsForAlbum: {},
|
AssetSelectionState(
|
||||||
selectedMonths: {},
|
selectedNewAssetsForAlbum: {},
|
||||||
selectedAdditionalAssetsForAlbum: {},
|
selectedMonths: {},
|
||||||
selectedAssetsInAlbumViewer: {},
|
selectedAdditionalAssetsForAlbum: {},
|
||||||
isAlbumExist: false,
|
selectedAssetsInAlbumViewer: {},
|
||||||
isMultiselectEnable: false,
|
isAlbumExist: false,
|
||||||
));
|
isMultiselectEnable: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
void setIsAlbumExist(bool isAlbumExist) {
|
void setIsAlbumExist(bool isAlbumExist) {
|
||||||
state = state.copyWith(isAlbumExist: isAlbumExist);
|
state = state.copyWith(isAlbumExist: isAlbumExist);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeAssetsInMonth(String removedMonth, List<ImmichAsset> assetsInMonth) {
|
void removeAssetsInMonth(
|
||||||
Set<ImmichAsset> currentAssetList = state.selectedNewAssetsForAlbum;
|
String removedMonth,
|
||||||
|
List<AssetResponseDto> assetsInMonth,
|
||||||
|
) {
|
||||||
|
Set<AssetResponseDto> currentAssetList = state.selectedNewAssetsForAlbum;
|
||||||
Set<String> currentMonthList = state.selectedMonths;
|
Set<String> currentMonthList = state.selectedMonths;
|
||||||
|
|
||||||
currentMonthList.removeWhere((selectedMonth) => selectedMonth == removedMonth);
|
currentMonthList
|
||||||
|
.removeWhere((selectedMonth) => selectedMonth == removedMonth);
|
||||||
|
|
||||||
for (ImmichAsset asset in assetsInMonth) {
|
for (AssetResponseDto asset in assetsInMonth) {
|
||||||
currentAssetList.removeWhere((e) => e.id == asset.id);
|
currentAssetList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = state.copyWith(selectedNewAssetsForAlbum: currentAssetList, selectedMonths: currentMonthList);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addAdditionalAssets(List<ImmichAsset> assets) {
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedAdditionalAssetsForAlbum: {...state.selectedAdditionalAssetsForAlbum, ...assets},
|
selectedNewAssetsForAlbum: currentAssetList,
|
||||||
|
selectedMonths: currentMonthList,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAllAssetsInMonth(String month, List<ImmichAsset> assetsInMonth) {
|
void addAdditionalAssets(List<AssetResponseDto> assets) {
|
||||||
|
state = state.copyWith(
|
||||||
|
selectedAdditionalAssetsForAlbum: {
|
||||||
|
...state.selectedAdditionalAssetsForAlbum,
|
||||||
|
...assets
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addAllAssetsInMonth(String month, List<AssetResponseDto> assetsInMonth) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedMonths: {...state.selectedMonths, month},
|
selectedMonths: {...state.selectedMonths, month},
|
||||||
selectedNewAssetsForAlbum: {...state.selectedNewAssetsForAlbum, ...assetsInMonth},
|
selectedNewAssetsForAlbum: {
|
||||||
|
...state.selectedNewAssetsForAlbum,
|
||||||
|
...assetsInMonth
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addNewAssets(List<ImmichAsset> assets) {
|
void addNewAssets(List<AssetResponseDto> assets) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedNewAssetsForAlbum: {...state.selectedNewAssetsForAlbum, ...assets},
|
selectedNewAssetsForAlbum: {
|
||||||
|
...state.selectedNewAssetsForAlbum,
|
||||||
|
...assets
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSelectedNewAssets(List<ImmichAsset> assets) {
|
void removeSelectedNewAssets(List<AssetResponseDto> assets) {
|
||||||
Set<ImmichAsset> currentList = state.selectedNewAssetsForAlbum;
|
Set<AssetResponseDto> currentList = state.selectedNewAssetsForAlbum;
|
||||||
|
|
||||||
for (ImmichAsset asset in assets) {
|
for (AssetResponseDto asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = state.copyWith(selectedNewAssetsForAlbum: currentList);
|
state = state.copyWith(selectedNewAssetsForAlbum: currentList);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeSelectedAdditionalAssets(List<ImmichAsset> assets) {
|
void removeSelectedAdditionalAssets(List<AssetResponseDto> assets) {
|
||||||
Set<ImmichAsset> currentList = state.selectedAdditionalAssetsForAlbum;
|
Set<AssetResponseDto> currentList = state.selectedAdditionalAssetsForAlbum;
|
||||||
|
|
||||||
for (ImmichAsset asset in assets) {
|
for (AssetResponseDto asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,16 +109,19 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void addAssetsInAlbumViewer(List<ImmichAsset> assets) {
|
void addAssetsInAlbumViewer(List<AssetResponseDto> assets) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
selectedAssetsInAlbumViewer: {...state.selectedAssetsInAlbumViewer, ...assets},
|
selectedAssetsInAlbumViewer: {
|
||||||
|
...state.selectedAssetsInAlbumViewer,
|
||||||
|
...assets
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeAssetsInAlbumViewer(List<ImmichAsset> assets) {
|
void removeAssetsInAlbumViewer(List<AssetResponseDto> assets) {
|
||||||
Set<ImmichAsset> currentList = state.selectedAssetsInAlbumViewer;
|
Set<AssetResponseDto> currentList = state.selectedAssetsInAlbumViewer;
|
||||||
|
|
||||||
for (ImmichAsset asset in assets) {
|
for (AssetResponseDto asset in assets) {
|
||||||
currentList.removeWhere((e) => e.id == asset.id);
|
currentList.removeWhere((e) => e.id == asset.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +129,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final assetSelectionProvider = StateNotifierProvider<AssetSelectionNotifier, AssetSelectionState>((ref) {
|
final assetSelectionProvider =
|
||||||
|
StateNotifierProvider<AssetSelectionNotifier, AssetSelectionState>((ref) {
|
||||||
return AssetSelectionNotifier();
|
return AssetSelectionNotifier();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
|
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
|
||||||
SharedAlbumNotifier() : super([]);
|
SharedAlbumNotifier(this._sharedAlbumService) : super([]);
|
||||||
|
|
||||||
final SharedAlbumService _sharedAlbumService = SharedAlbumService();
|
final SharedAlbumService _sharedAlbumService;
|
||||||
|
|
||||||
getAllSharedAlbums() async {
|
getAllSharedAlbums() async {
|
||||||
List<SharedAlbum> sharedAlbums =
|
List<AlbumResponseDto>? sharedAlbums =
|
||||||
await _sharedAlbumService.getAllSharedAlbum();
|
await _sharedAlbumService.getAllSharedAlbum();
|
||||||
|
|
||||||
state = sharedAlbums;
|
if (sharedAlbums != null) {
|
||||||
|
state = sharedAlbums;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> deleteAlbum(String albumId) async {
|
Future<bool> deleteAlbum(String albumId) async {
|
||||||
@@ -37,7 +39,9 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> removeAssetFromAlbum(
|
Future<bool> removeAssetFromAlbum(
|
||||||
String albumId, List<String> assetIds) async {
|
String albumId,
|
||||||
|
List<String> assetIds,
|
||||||
|
) async {
|
||||||
var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds);
|
var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds);
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
@@ -49,13 +53,14 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final sharedAlbumProvider =
|
final sharedAlbumProvider =
|
||||||
StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) {
|
StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
|
||||||
return SharedAlbumNotifier();
|
return SharedAlbumNotifier(ref.watch(sharedAlbumServiceProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
final sharedAlbumDetailProvider = FutureProvider.autoDispose
|
final sharedAlbumDetailProvider = FutureProvider.autoDispose
|
||||||
.family<SharedAlbum, String>((ref, albumId) async {
|
.family<AlbumResponseDto?, String>((ref, albumId) async {
|
||||||
final SharedAlbumService sharedAlbumService = SharedAlbumService();
|
final SharedAlbumService sharedAlbumService =
|
||||||
|
ref.watch(sharedAlbumServiceProvider);
|
||||||
|
|
||||||
return await sharedAlbumService.getAlbumDetail(albumId);
|
return await sharedAlbumService.getAlbumDetail(albumId);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/user.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/user.service.dart';
|
import 'package:immich_mobile/shared/services/user.service.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
final suggestedSharedUsersProvider = FutureProvider.autoDispose<List<User>>((ref) async {
|
final suggestedSharedUsersProvider =
|
||||||
UserService userService = UserService();
|
FutureProvider.autoDispose<List<UserResponseDto>>((ref) async {
|
||||||
|
UserService userService = ref.watch(userServiceProvider);
|
||||||
|
|
||||||
return await userService.getAllUsersInfo();
|
return await userService.getAllUsersInfo(isAll: false) ?? [];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
final sharedAlbumServiceProvider = Provider(
|
||||||
|
(ref) => SharedAlbumService(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
class SharedAlbumService {
|
class SharedAlbumService {
|
||||||
final NetworkService _networkService = NetworkService();
|
final ApiService _apiService;
|
||||||
|
SharedAlbumService(this._apiService);
|
||||||
|
|
||||||
Future<List<SharedAlbum>> getAllSharedAlbum() async {
|
Future<List<AlbumResponseDto>?> getAllSharedAlbum() async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.getRequest(url: 'album?shared=true');
|
return await _apiService.albumApi.getAllAlbums(shared: true);
|
||||||
List<dynamic> decodedData = jsonDecode(res.toString());
|
|
||||||
List<SharedAlbum> result =
|
|
||||||
List.from(decodedData.map((e) => SharedAlbum.fromMap(e)));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error getAllSharedAlbum ${e.toString()}");
|
debugPrint("Error getAllSharedAlbum ${e.toString()}");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> createSharedAlbum(String albumName, Set<ImmichAsset> assets,
|
Future<bool> createSharedAlbum(
|
||||||
List<String> sharedUserIds) async {
|
String albumName,
|
||||||
|
Set<AssetResponseDto> assets,
|
||||||
|
List<String> sharedUserIds,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.postRequest(url: 'album', data: {
|
_apiService.albumApi.createAlbum(
|
||||||
"albumName": albumName,
|
CreateAlbumDto(
|
||||||
"sharedWithUserIds": sharedUserIds,
|
albumName: albumName,
|
||||||
"assetIds": assets.map((asset) => asset.id).toList(),
|
assetIds: assets.map((asset) => asset.id).toList(),
|
||||||
});
|
sharedWithUserIds: sharedUserIds,
|
||||||
|
),
|
||||||
if (res == null) {
|
);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -45,32 +45,25 @@ class SharedAlbumService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SharedAlbum> getAlbumDetail(String albumId) async {
|
Future<AlbumResponseDto?> getAlbumDetail(String albumId) async {
|
||||||
try {
|
try {
|
||||||
var res = await _networkService.getRequest(url: 'album/$albumId');
|
return await _apiService.albumApi.getAlbumInfo(albumId);
|
||||||
dynamic decodedData = jsonDecode(res.toString());
|
|
||||||
SharedAlbum result = SharedAlbum.fromMap(decodedData);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Error getAllSharedAlbum ${e.toString()}');
|
debugPrint('Error [getAlbumDetail] ${e.toString()}');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> addAdditionalAssetToAlbum(
|
Future<bool> addAdditionalAssetToAlbum(
|
||||||
Set<ImmichAsset> assets, String albumId) async {
|
Set<AssetResponseDto> assets,
|
||||||
|
String albumId,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
var res =
|
var result = await _apiService.albumApi.addAssetsToAlbum(
|
||||||
await _networkService.putRequest(url: 'album/$albumId/assets', data: {
|
albumId,
|
||||||
"albumId": albumId,
|
AddAssetsDto(assetIds: assets.map((asset) => asset.id).toList()),
|
||||||
"assetIds": assets.map((asset) => asset.id).toList(),
|
);
|
||||||
});
|
return result != null;
|
||||||
|
|
||||||
if (res == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
||||||
return false;
|
return false;
|
||||||
@@ -78,18 +71,16 @@ class SharedAlbumService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> addAdditionalUserToAlbum(
|
Future<bool> addAdditionalUserToAlbum(
|
||||||
List<String> sharedUserIds, String albumId) async {
|
List<String> sharedUserIds,
|
||||||
|
String albumId,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
var res =
|
var result = await _apiService.albumApi.addUsersToAlbum(
|
||||||
await _networkService.putRequest(url: 'album/$albumId/users', data: {
|
albumId,
|
||||||
"sharedUserIds": sharedUserIds,
|
AddUsersDto(sharedUserIds: sharedUserIds),
|
||||||
});
|
);
|
||||||
|
|
||||||
if (res == null) {
|
return result != null;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
|
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
|
||||||
return false;
|
return false;
|
||||||
@@ -98,12 +89,7 @@ class SharedAlbumService {
|
|||||||
|
|
||||||
Future<bool> deleteAlbum(String albumId) async {
|
Future<bool> deleteAlbum(String albumId) async {
|
||||||
try {
|
try {
|
||||||
Response res = await _networkService.deleteRequest(url: 'album/$albumId');
|
await _apiService.albumApi.deleteAlbum(albumId);
|
||||||
|
|
||||||
if (res.statusCode != 200) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error deleteAlbum ${e.toString()}");
|
debugPrint("Error deleteAlbum ${e.toString()}");
|
||||||
@@ -113,12 +99,7 @@ class SharedAlbumService {
|
|||||||
|
|
||||||
Future<bool> leaveAlbum(String albumId) async {
|
Future<bool> leaveAlbum(String albumId) async {
|
||||||
try {
|
try {
|
||||||
Response res =
|
await _apiService.albumApi.removeUserFromAlbum(albumId, "me");
|
||||||
await _networkService.deleteRequest(url: 'album/$albumId/user/me');
|
|
||||||
|
|
||||||
if (res.statusCode != 200) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -128,16 +109,14 @@ class SharedAlbumService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> removeAssetFromAlbum(
|
Future<bool> removeAssetFromAlbum(
|
||||||
String albumId, List<String> assetIds) async {
|
String albumId,
|
||||||
|
List<String> assetIds,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
Response res = await _networkService
|
await _apiService.albumApi.removeAssetFromAlbum(
|
||||||
.deleteRequest(url: 'album/$albumId/assets', data: {
|
albumId,
|
||||||
"assetIds": assetIds,
|
RemoveAssetsDto(assetIds: assetIds),
|
||||||
});
|
);
|
||||||
|
|
||||||
if (res.statusCode != 200) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -147,17 +126,18 @@ class SharedAlbumService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> changeTitleAlbum(
|
Future<bool> changeTitleAlbum(
|
||||||
String albumId, String ownerId, String newAlbumTitle) async {
|
String albumId,
|
||||||
|
String ownerId,
|
||||||
|
String newAlbumTitle,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
Response res =
|
await _apiService.albumApi.updateAlbumInfo(
|
||||||
await _networkService.patchRequest(url: 'album/$albumId/', data: {
|
albumId,
|
||||||
"ownerId": ownerId,
|
UpdateAlbumDto(
|
||||||
"albumName": newAlbumTitle,
|
ownerId: ownerId,
|
||||||
});
|
albumName: newAlbumTitle,
|
||||||
|
),
|
||||||
if (res.statusCode != 200) {
|
);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ class AlbumActionOutlinedButton extends StatelessWidget {
|
|||||||
final String labelText;
|
final String labelText;
|
||||||
final IconData iconData;
|
final IconData iconData;
|
||||||
|
|
||||||
const AlbumActionOutlinedButton({Key? key, this.onPressed, required this.labelText, required this.iconData})
|
const AlbumActionOutlinedButton({
|
||||||
: super(key: key);
|
Key? key,
|
||||||
|
this.onPressed,
|
||||||
|
required this.labelText,
|
||||||
|
required this.iconData,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -26,7 +30,11 @@ class AlbumActionOutlinedButton extends StatelessWidget {
|
|||||||
icon: Icon(iconData, size: 15),
|
icon: Icon(iconData, size: 15),
|
||||||
label: Text(
|
label: Text(
|
||||||
labelText,
|
labelText,
|
||||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87),
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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/sharing/providers/album_title.provider.dart';
|
||||||
@@ -29,7 +30,11 @@ class AlbumTitleTextField extends ConsumerWidget {
|
|||||||
ref.watch(albumTitleProvider.notifier).setAlbumTitle(v);
|
ref.watch(albumTitleProvider.notifier).setAlbumTitle(v);
|
||||||
},
|
},
|
||||||
focusNode: albumTitleTextFieldFocusNode,
|
focusNode: albumTitleTextFieldFocusNode,
|
||||||
style: TextStyle(fontSize: 28, color: Colors.grey[700], fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
controller: albumTitleController,
|
controller: albumTitleController,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
isAlbumTitleTextFieldFocus.value = true;
|
isAlbumTitleTextFieldFocus.value = true;
|
||||||
@@ -58,7 +63,7 @@ class AlbumTitleTextField extends ConsumerWidget {
|
|||||||
borderSide: const BorderSide(color: Colors.transparent),
|
borderSide: const BorderSide(color: Colors.transparent),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
hintText: 'Add a title',
|
hintText: 'share_add_title'.tr(),
|
||||||
focusColor: Colors.grey[300],
|
focusColor: Colors.grey[300],
|
||||||
fillColor: Colors.grey[200],
|
fillColor: Colors.grey[200],
|
||||||
filled: isAlbumTitleTextFieldFocus.value,
|
filled: isAlbumTitleTextFieldFocus.value,
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package: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/models/shared_album.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.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';
|
||||||
|
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<SharedAlbum> albumInfo,
|
required AsyncValue<AlbumResponseDto?> albumInfo,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.albumId,
|
required this.albumId,
|
||||||
}) : _albumInfo = albumInfo,
|
}) : _albumInfo = albumInfo,
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
final AsyncValue<SharedAlbum> _albumInfo;
|
final AsyncValue<AlbumResponseDto?> _albumInfo;
|
||||||
final String userId;
|
final String userId;
|
||||||
final String albumId;
|
final String albumId;
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
} else {
|
} else {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "Failed to delete album",
|
msg: "album_viewer_appbar_share_err_delete".tr(),
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
@@ -67,7 +68,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "Failed to leave album",
|
msg: "album_viewer_appbar_share_err_leave".tr(),
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
@@ -93,7 +94,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "There are problems in removing assets from album",
|
msg: "album_viewer_appbar_share_err_remove".tr(),
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
@@ -104,35 +105,35 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
|
|
||||||
_buildBottomSheetActionButton() {
|
_buildBottomSheetActionButton() {
|
||||||
if (isMultiSelectionEnable) {
|
if (isMultiSelectionEnable) {
|
||||||
if (_albumInfo.asData?.value.ownerId == userId) {
|
if (_albumInfo.asData?.value?.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(
|
||||||
'Remove from album',
|
'album_viewer_appbar_share_remove',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
onTap: () => _onRemoveFromAlbumPressed(albumId),
|
onTap: () => _onRemoveFromAlbumPressed(albumId),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Container();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_albumInfo.asData?.value.ownerId == userId) {
|
if (_albumInfo.asData?.value?.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(
|
||||||
'Delete album',
|
'album_viewer_appbar_share_delete',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
onTap: () => _onDeleteAlbumPressed(albumId),
|
onTap: () => _onDeleteAlbumPressed(albumId),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.person_remove_rounded),
|
leading: const Icon(Icons.person_remove_rounded),
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'Leave album',
|
'album_viewer_appbar_share_leave',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
).tr(),
|
||||||
onTap: () => _onLeaveAlbumPressed(albumId),
|
onTap: () => _onLeaveAlbumPressed(albumId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -176,7 +177,7 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: "Failed to change album title",
|
msg: "album_viewer_appbar_share_err_title".tr(),
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
);
|
);
|
||||||
@@ -198,8 +199,8 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: _buildLeadingButton(),
|
leading: _buildLeadingButton(),
|
||||||
title: isMultiSelectionEnable
|
title: isMultiSelectionEnable
|
||||||
? Text(selectedAssetsInAlbum.length.toString())
|
? Text('${selectedAssetsInAlbum.length}')
|
||||||
: Container(),
|
: null,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
|
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/shared_album.model.dart';
|
|
||||||
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart';
|
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumViewerEditableTitle extends HookConsumerWidget {
|
class AlbumViewerEditableTitle extends HookConsumerWidget {
|
||||||
final SharedAlbum albumInfo;
|
final AlbumResponseDto albumInfo;
|
||||||
final FocusNode titleFocusNode;
|
final FocusNode titleFocusNode;
|
||||||
const AlbumViewerEditableTitle({Key? key, required this.albumInfo, required this.titleFocusNode}) : super(key: key);
|
const AlbumViewerEditableTitle({
|
||||||
|
Key? key,
|
||||||
|
required this.albumInfo,
|
||||||
|
required this.titleFocusNode,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final titleTextEditController = useTextEditingController(text: albumInfo.albumName);
|
final titleTextEditController =
|
||||||
|
useTextEditingController(text: albumInfo.albumName);
|
||||||
|
|
||||||
void onFocusModeChange() {
|
void onFocusModeChange() {
|
||||||
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
|
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
|
||||||
@@ -20,12 +26,15 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(
|
||||||
titleFocusNode.addListener(onFocusModeChange);
|
() {
|
||||||
return () {
|
titleFocusNode.addListener(onFocusModeChange);
|
||||||
titleFocusNode.removeListener(onFocusModeChange);
|
return () {
|
||||||
};
|
titleFocusNode.removeListener(onFocusModeChange);
|
||||||
}, []);
|
};
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return TextField(
|
return TextField(
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@@ -40,7 +49,9 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
FocusScope.of(context).requestFocus(titleFocusNode);
|
FocusScope.of(context).requestFocus(titleFocusNode);
|
||||||
|
|
||||||
ref.watch(albumViewerProvider.notifier).setEditTitleText(albumInfo.albumName);
|
ref
|
||||||
|
.watch(albumViewerProvider.notifier)
|
||||||
|
.setEditTitleText(albumInfo.albumName);
|
||||||
ref.watch(albumViewerProvider.notifier).enableEditAlbum();
|
ref.watch(albumViewerProvider.notifier).enableEditAlbum();
|
||||||
|
|
||||||
if (titleTextEditController.text == 'Untitled') {
|
if (titleTextEditController.text == 'Untitled') {
|
||||||
@@ -69,7 +80,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
|
|||||||
focusColor: Colors.grey[300],
|
focusColor: Colors.grey[300],
|
||||||
fillColor: Colors.grey[200],
|
fillColor: Colors.grey[200],
|
||||||
filled: titleFocusNode.hasFocus,
|
filled: titleFocusNode.hasFocus,
|
||||||
hintText: 'Add a title',
|
hintText: 'share_add_title'.tr(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ 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/sharing/providers/asset_selection.provider.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AlbumViewerThumbnail extends HookConsumerWidget {
|
class AlbumViewerThumbnail extends HookConsumerWidget {
|
||||||
final ImmichAsset asset;
|
final AssetResponseDto asset;
|
||||||
|
|
||||||
const AlbumViewerThumbnail({Key? key, required this.asset}) : super(key: key);
|
const AlbumViewerThumbnail({Key? key, required this.asset}) : super(key: key);
|
||||||
|
|
||||||
@@ -20,13 +20,15 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
final cacheKey = useState(1);
|
final cacheKey = useState(1);
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
var thumbnailRequestUrl =
|
var thumbnailRequestUrl =
|
||||||
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
|
'${box.get(serverEndpointKey)}/asset/thumbnail/${asset.id}';
|
||||||
var deviceId = ref.watch(authenticationProvider).deviceId;
|
var deviceId = ref.watch(authenticationProvider).deviceId;
|
||||||
final selectedAssetsInAlbumViewer = ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
final selectedAssetsInAlbumViewer =
|
||||||
final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable;
|
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
||||||
|
final isMultiSelectionEnable =
|
||||||
|
ref.watch(assetSelectionProvider).isMultiselectEnable;
|
||||||
|
|
||||||
_viewAsset() {
|
_viewAsset() {
|
||||||
if (asset.type == 'IMAGE') {
|
if (asset.type == AssetTypeEnum.IMAGE) {
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
ImageViewerRoute(
|
ImageViewerRoute(
|
||||||
imageUrl:
|
imageUrl:
|
||||||
@@ -39,8 +41,10 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
} else {
|
} else {
|
||||||
AutoRouter.of(context).push(
|
AutoRouter.of(context).push(
|
||||||
VideoViewerRoute(
|
VideoViewerRoute(
|
||||||
videoUrl: '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
|
videoUrl:
|
||||||
asset: asset),
|
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
|
||||||
|
asset: asset,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +62,9 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
|
|
||||||
_enableMultiSelection() {
|
_enableMultiSelection() {
|
||||||
ref.watch(assetSelectionProvider.notifier).enableMultiselection();
|
ref.watch(assetSelectionProvider.notifier).enableMultiselection();
|
||||||
ref.watch(assetSelectionProvider.notifier).addAssetsInAlbumViewer([asset]);
|
ref
|
||||||
|
.watch(assetSelectionProvider.notifier)
|
||||||
|
.addAssetsInAlbumViewer([asset]);
|
||||||
}
|
}
|
||||||
|
|
||||||
_disableMultiSelection() {
|
_disableMultiSelection() {
|
||||||
@@ -66,29 +72,25 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_buildVideoLabel() {
|
_buildVideoLabel() {
|
||||||
if (asset.type == 'IMAGE') {
|
return Positioned(
|
||||||
return Container();
|
top: 5,
|
||||||
} else {
|
right: 5,
|
||||||
return Positioned(
|
child: Row(
|
||||||
top: 5,
|
children: [
|
||||||
right: 5,
|
Text(
|
||||||
child: Row(
|
asset.duration.toString().substring(0, 7),
|
||||||
children: [
|
style: const TextStyle(
|
||||||
Text(
|
|
||||||
asset.duration.toString().substring(0, 7),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Icon(
|
|
||||||
Icons.play_circle_outline_rounded,
|
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
fontSize: 10,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
const Icon(
|
||||||
);
|
Icons.play_circle_outline_rounded,
|
||||||
}
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildAssetStoreLocationIcon() {
|
_buildAssetStoreLocationIcon() {
|
||||||
@@ -96,7 +98,9 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
right: 10,
|
right: 10,
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
(deviceId != asset.deviceId) ? Icons.cloud_done_outlined : Icons.photo_library_rounded,
|
(deviceId != asset.deviceId)
|
||||||
|
? Icons.cloud_done_outlined
|
||||||
|
: Icons.photo_library_rounded,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
@@ -105,23 +109,20 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
|
|
||||||
_buildAssetSelectionIcon() {
|
_buildAssetSelectionIcon() {
|
||||||
bool isSelected = selectedAssetsInAlbumViewer.contains(asset);
|
bool isSelected = selectedAssetsInAlbumViewer.contains(asset);
|
||||||
if (isMultiSelectionEnable) {
|
|
||||||
return Positioned(
|
return Positioned(
|
||||||
left: 10,
|
left: 10,
|
||||||
top: 5,
|
top: 5,
|
||||||
child: isSelected
|
child: isSelected
|
||||||
? Icon(
|
? Icon(
|
||||||
Icons.check_circle_rounded,
|
Icons.check_circle_rounded,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).primaryColor,
|
||||||
)
|
)
|
||||||
: const Icon(
|
: const Icon(
|
||||||
Icons.check_circle_outline_rounded,
|
Icons.check_circle_outline_rounded,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return Container();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildThumbnailImage() {
|
_buildThumbnailImage() {
|
||||||
@@ -136,7 +137,8 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
imageUrl: thumbnailRequestUrl,
|
imageUrl: thumbnailRequestUrl,
|
||||||
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) => Transform.scale(
|
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
||||||
|
Transform.scale(
|
||||||
scale: 0.2,
|
scale: 0.2,
|
||||||
child: CircularProgressIndicator(value: downloadProgress.progress),
|
child: CircularProgressIndicator(value: downloadProgress.progress),
|
||||||
),
|
),
|
||||||
@@ -152,29 +154,30 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
|
|||||||
|
|
||||||
_handleSelectionGesture() {
|
_handleSelectionGesture() {
|
||||||
if (selectedAssetsInAlbumViewer.contains(asset)) {
|
if (selectedAssetsInAlbumViewer.contains(asset)) {
|
||||||
ref.watch(assetSelectionProvider.notifier).removeAssetsInAlbumViewer([asset]);
|
ref
|
||||||
|
.watch(assetSelectionProvider.notifier)
|
||||||
|
.removeAssetsInAlbumViewer([asset]);
|
||||||
|
|
||||||
if (selectedAssetsInAlbumViewer.isEmpty) {
|
if (selectedAssetsInAlbumViewer.isEmpty) {
|
||||||
_disableMultiSelection();
|
_disableMultiSelection();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ref.watch(assetSelectionProvider.notifier).addAssetsInAlbumViewer([asset]);
|
ref
|
||||||
|
.watch(assetSelectionProvider.notifier)
|
||||||
|
.addAssetsInAlbumViewer([asset]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: isMultiSelectionEnable ? _handleSelectionGesture : _viewAsset,
|
onTap: isMultiSelectionEnable ? _handleSelectionGesture : _viewAsset,
|
||||||
onLongPress: _enableMultiSelection,
|
onLongPress: _enableMultiSelection,
|
||||||
child: Hero(
|
child: Stack(
|
||||||
tag: asset.id,
|
children: [
|
||||||
child: Stack(
|
_buildThumbnailImage(),
|
||||||
children: [
|
_buildAssetStoreLocationIcon(),
|
||||||
_buildThumbnailImage(),
|
if (asset.type != AssetTypeEnum.IMAGE) _buildVideoLabel(),
|
||||||
_buildAssetStoreLocationIcon(),
|
if (isMultiSelectionEnable) _buildAssetSelectionIcon(),
|
||||||
_buildVideoLabel(),
|
],
|
||||||
_buildAssetSelectionIcon(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
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/sharing/ui/selection_thumbnail_image.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
class AssetGridByMonth extends HookConsumerWidget {
|
class AssetGridByMonth extends HookConsumerWidget {
|
||||||
final List<ImmichAsset> assetGroup;
|
final List<AssetResponseDto> assetGroup;
|
||||||
const AssetGridByMonth({Key? key, required this.assetGroup}) : super(key: key);
|
const AssetGridByMonth({Key? key, required this.assetGroup})
|
||||||
|
: super(key: key);
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return SliverGrid(
|
return SliverGrid(
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user