Compare commits

...

45 Commits

Author SHA1 Message Date
Alex Tran
72c334e5e0 Pump build number 2022-07-13 10:12:03 -05:00
Alex Tran
e7f35822af Pump version number 2022-07-13 10:04:25 -05:00
Alex Tran
bd2152d568 Pump build number 2022-07-13 09:56:34 -05:00
Alex
b1d7ef03e2 Pump version for release (#339)
* Remove unncessesary line

* Pump version for release
2022-07-13 09:51:41 -05:00
Alex Tran
aa74417d11 Fixed web production build 2022-07-13 08:35:52 -05:00
Alex Tran
229b009b7f Remove Axios import in web hook.ts 2022-07-13 08:25:43 -05:00
Fynn Petersen-Frey
bece6253d5 Improve Docker setup and reduce memory usage of production containers (#338) 2022-07-13 07:42:00 -05:00
Alex
ae7e582ec8 Refactor mobile to use OpenApi generated SDK (#336) 2022-07-13 07:23:48 -05:00
Fynn Petersen-Frey
d69470e207 Add extended redis & DB port configuration via environment variables (#330)
* Add database port configuration via env variable

Add redis connection configuration via env variables

* Add redis connection configuration via env variables
2022-07-12 22:21:11 -05:00
Alex
c60e852226 fix 331 (#335)
* fix #331 - Video with no date information in EXIF upload via web caused mobile client not able to render other assets
2022-07-12 16:34:43 -05:00
Zack Pollard
a205478a29 Remove advice regarding running immich-server with scaling (#334) 2022-07-11 11:40:25 -05:00
Alex
22d30522e1 [Localizely] Translations update (#324) 2022-07-10 22:31:29 -05:00
Matthias Rupp
19b1fad274 Add message to login screen (useful for demo instances) (#329)
* Add message for demo instances to login screen

* Rename env variable

* Added key into

* Add styling to conform with Immich color scheme

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-07-10 22:31:17 -05:00
Alex
9a6dfacf9b Refactor web to use OpenAPI SDK (#326)
* Refactor main index page

* Refactor admin page

* Refactor Auth endpoint

* Refactor directory to prep for monorepo

* Fixed refactoring path

* Resolved file path in vite

* Refactor photo index page

* Refactor thumbnail

* Fixed test

* Refactor Video Viewer component

* Refactor download file

* Refactor navigation bar

* Refactor upload file check

* Simplify Upload Asset signature

* PR feedback
2022-07-10 21:41:45 -05:00
Alex
7f236c5b18 Add OpenAPI Specs and Response DTOs (#320)
* Added swagger bearer auth method authentication accordingly

* Update Auth endpoint

* Added additional api information for authentication

* Added Swagger CLI pluggin

* Added DTO for /user endpoint

* Added /device-info reponse DTOs

* Implement server version

* Added DTOs for /server-info

* Added DTOs for /assets

* Added album to Swagger group

* Added generated specs file

* Add Client API generator for web

* Remove incorrectly placed node_modules

* Created class to handle access token

* Remove password and hash when getting all user

* PR feedback

* Fixed video from CLI doesn't get metadata extracted

* Fixed issue with TSConfig to work with generated openAPI

* PR feedback

* Remove console.log
2022-07-08 21:26:50 -05:00
Matthias Rupp
25985c732d Merge pull request #323 from alextran1502/fix/localizely_format
Change localizely format to json
2022-07-08 15:56:49 +02:00
Matthias
9ce50b7e3d Change localizely format to json 2022-07-08 15:53:20 +02:00
Matthias Rupp
f5e93a8179 Add translation keys for upload info section (#319) 2022-07-07 17:25:02 -05:00
Matthias Rupp
2b5cef156c Internationalization (German) of the mobile app. (#246)
* Add i18n framework to mobile app and write simple translation generator

* Replace all texts in login_form with i18n keys

* Localization of sharing section

* Localization of asset viewer section

* Use JSON as base translation format

* Add check for missing/unused translation keys

* Add localizely

* Remove i18n directory in favour of localizely

* Backup Translation

* More translations

* Translate home page

* Translation of search page

* Translate new server version announcement

* Reformat code

* Fix typo in german translation

* Update englisch translations

* Change translation keys to match dart filenames

* Add /api to translated endpoint_urls

* Update localizely.yml

* Add languages to ios plist

* Remove unused keys

* Added script to check outdated key in other translations

* Add download key to localizely.yml

Co-authored-by: Alex <alex.tran1502@gmail.com>
2022-07-07 13:40:54 -05:00
Alex
f3032f74a4 Added changelog for Fdroid 2022-07-06 22:35:07 -05:00
Alex
58ec7553ea Add information for uploading asset and error indication with error message for each failed upload. (#315)
* Added info box

* Fixed upload endpoint doesn't report error status code

* Added chip to show update error

* Added chip to show failed upload

* Add duplication check for upload

* Better duplication-checking placement

* Remove check for duplicated asset

* Added failed backup status route

* added page

* Display error card with thumbnail

* Improved styling

* Set thumbnail with better quality

* Remove force upload error
2022-07-06 16:12:55 -05:00
Alex
357f7d1c31 Added schedule job to perform reverse geocoding if key is added after backing up assets (#305) 2022-07-04 15:16:39 -05:00
Zack Pollard
e6d30d72fa Fix typeorm migrations (#297)
* fix: remove config parameter from typeorm cli and update config

the config parameter is no longer supported since version 0.3
the config now needs to export a DataSource object to work with the 0.3 cli

* fix: update all typeorm entities and migrations to be aligned with database structure

* Fixed test-util import databaseConfig

* Fixed column mismatch in raw query with new migration

* Remove dist build directory when starting dev server

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-07-04 14:20:43 -05:00
Jaime Baez
355038a91a Use npm ci for installing pacakages (#304) 2022-07-04 13:47:25 -05:00
Alex
97d9b80baa Added creation date for video from ffmpeg.prob (#303) 2022-07-04 13:44:43 -05:00
Alex Tran
b6814fad57 Up version for hotfix 2022-07-03 20:55:30 -05:00
Alex
7586c65103 Fix cannot query shared album on mobile (#298) 2022-07-03 20:52:03 -05:00
Alex
633170d743 Fixed inconnect image grouping with the same date but different year (#296) 2022-07-03 18:00:56 -05:00
Alex Tran
c5be7827c3 Remove 2284 to avoid confusion since 443 is not exposed from internal proxy 2022-07-03 11:37:26 -05:00
Alex Tran
e84c705e31 Added changelog to Fdroid 2022-07-03 10:49:37 -05:00
Alex Tran
36162509e0 Up version for release 2022-07-03 10:39:09 -05:00
Alex
76bf1c0379 Remove thumbnail generation on mobile app (#292)
* Remove thumbnail generation on mobile

* Remove tconditions for missing thumbnail on the backend

* Remove console.log

* Refactor queue systems

* Convert queue and processor name to constant

* Added corresponding interface to job queue
2022-07-02 21:06:36 -05:00
Alex
32b847c26e Fixed event propagation trigger navigating twice (#293) 2022-07-01 20:49:41 -05:00
Alex
a45d6fdf57 Fix server crash on bad file operation and other optimizations (#291)
* Fixed issue with generating thumbnail for video with 0 length cause undefined file and crash the server
* Added all file error handling operation
* Temporarily disabled WebSocket on the web because receiving a new upload event doesn't put the new file in the correct place. 
* Cosmetic fixed on the info panel
2022-07-01 12:00:12 -05:00
Zack Pollard
c071e64a7e infra: switch port to 3003 for machine learning container (#290)
* infra: switch port to 3003 for machine learning container

fixes #289

* Changed port of machine-learning-endpoint to match with new port

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-07-01 10:20:04 -05:00
Alex
663f12851e Fixed filename duplication when upload from web (#288)
* Fixed filename duplication when upload from web

* Fixed cosmetic of detail panel view
2022-06-30 20:43:33 -05:00
xpwmaosldk
c4ef523564 Optimize mobile - Avoid creating unnecessary widgets (#268)
* Avoid creating unnecessary widgets

* more flexible null handling and runtime errors prevention
2022-06-30 20:08:49 -05:00
Alex
992f792c0a Fixed admin is forced to change password on mobile app (#287)
* Fixed issues

* Upversion and add changed log
2022-06-30 13:59:02 -05:00
Alex Tran
97611fa057 Fixed issue with unexposed Nginx port on release image 2022-06-30 00:26:54 -05:00
Alex Tran
32240777c3 fixed release build directory for Github action 2022-06-30 00:10:01 -05:00
Alex Tran
6065ff8caa Update readme with new discord invitation link 2022-06-29 23:50:24 -05:00
Alex Tran
8db073941d Up server version for release 2022-06-29 21:54:57 -05:00
Alex
5e281b44e9 Add Podman Support (#278) 2022-06-29 21:49:35 -05:00
Zack Pollard
142ede350e feat: create immich-nginx container to remove default nginx config setup (#280)
* feat: create immich-proxy container to remove default nginx config setup

* infra: make production docker-compose point at release builds for stability

* Fixed nginx config file was overriden by default.conf in nginx container; Fixed docker-compose.dev; Added additional tag 'release' for tagging after release build in Github Action

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2022-06-29 21:24:55 -05:00
Alex
a2e1d4caa2 Update server dependencies and fixed Typeorm API changes in new version (#276)
* Fixed dependencies

* Adapt typeorm API to be compatible with new version

* Fixed typeorm API in tests

* Remove console.log
2022-06-29 13:39:58 -05:00
422 changed files with 23666 additions and 5673 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
@@ -231,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.

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
FROM node:16-bullseye-slim FROM node:16-bullseye-slim
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
@@ -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 . .

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
* Fixed admin is forced to change password upon logging in on mobile app
* Fixed change password form validation

View File

@@ -0,0 +1 @@
* Removed thumbnail generation on mobile - the operation now will be on the server to reduce CPU load and battery usage.

View File

@@ -0,0 +1 @@
* Hot fix: Restore shared album functionality

View File

@@ -0,0 +1 @@
* Add information for uploading asset and error indication with error message for each failed upload.

View File

@@ -0,0 +1 @@
* Refactored app to use OpenAPI SDK to improve performance and project structure.

View 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"
}

View 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"
}

View File

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

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta" desc "iOS Beta"
lane :beta do lane :beta do
increment_version_number( increment_version_number(
version_number: "1.14.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,

View File

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

View File

@@ -3,17 +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() ImageViewerStateNotifier(this._imageViewerService)
: super(ImageViewerPageState( : super(
downloadAssetStatus: DownloadAssetStatus.idle)); 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);
@@ -43,4 +46,5 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
final imageViewerStateProvider = final imageViewerStateProvider =
StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>( StateNotifierProvider<ImageViewerStateNotifier, ImageViewerPageState>(
((ref) => ImageViewerStateNotifier())); ((ref) => ImageViewerStateNotifier(ref.watch(imageViewerServiceProvider))),
);

View File

@@ -1,33 +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: { asset.deviceId,
"Authorization": "Bearer ${Hive.box(userInfoBox).get(accessTokenKey)}" 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),
@@ -39,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;
} }
} }

View File

@@ -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} ",
),
),
],
),
),
], ],
), ),
); );

View File

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

View File

@@ -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),
)
], ],
); );
} }

View File

@@ -11,17 +11,16 @@ 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;
ImmichAssetWithExif? assetDetail; AssetResponseDto? assetDetail;
ImageViewerPage({ ImageViewerPage({
Key? key, Key? key,
@@ -54,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,
@@ -75,14 +77,15 @@ class ImageViewerPage extends HookConsumerWidget {
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) if (downloadAssetStatus == DownloadAssetStatus.loading)
const Center( const Center(

View File

@@ -12,15 +12,14 @@ 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;
VideoViewerPage({Key? key, required this.videoUrl, required this.asset}) VideoViewerPage({Key? key, required this.videoUrl, required this.asset})
: super(key: key); : super(key: key);
@@ -49,10 +48,13 @@ class VideoViewerPage extends HookConsumerWidget {
await ref.watch(assetServiceProvider).getAssetById(asset.id); 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,
@@ -116,8 +118,10 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
Future<void> initializePlayer() async { Future<void> initializePlayer() async {
try { try {
videoPlayerController = VideoPlayerController.network(widget.url, videoPlayerController = VideoPlayerController.network(
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"}); widget.url,
httpHeaders: {"Authorization": "Bearer ${widget.jwtToken}"},
);
await videoPlayerController.initialize(); await videoPlayerController.initialize();
_createChewieController(); _createChewieController();
@@ -147,8 +151,7 @@ class _VideoThumbnailPlayerState extends State<VideoThumbnailPlayer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return chewieController != null && return chewieController?.videoPlayerController.value.isInitialized == true
chewieController!.videoPlayerController.value.isInitialized
? SizedBox( ? SizedBox(
child: Chewie( child: Chewie(
controller: chewieController!, controller: chewieController!,

View File

@@ -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> allAssetsInDatabase; 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,6 +27,9 @@ 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.allAssetsInDatabase, required this.allAssetsInDatabase,
@@ -37,6 +41,7 @@ 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({
@@ -44,12 +49,13 @@ class BackUpState extends Equatable {
List<String>? allAssetsInDatabase, 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,
@@ -63,27 +69,49 @@ class BackUpState extends Equatable {
allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets, allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssetsIds:
selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds, selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset,
); );
} }
@override @override
String toString() { String toString() {
return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, 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;
allAssetsInDatabase,
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;
} }
} }

View File

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

View File

@@ -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,
];
}
}

View File

@@ -5,28 +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/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._backupService, this._serverInfoService, this._authState) BackupNotifier(
: super( this._backupService,
this._serverInfoService,
this._authState,
this.ref,
) : super(
BackUpState( BackUpState(
backupProgress: BackUpProgressEnum.idle, backupProgress: BackUpProgressEnum.idle,
allAssetsInDatabase: 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,
), ),
@@ -35,6 +42,12 @@ 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(); getBackupInfo();
@@ -43,6 +56,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
final BackupService _backupService; final BackupService _backupService;
final ServerInfoService _serverInfoService; final ServerInfoService _serverInfoService;
final AuthenticationState _authState; final AuthenticationState _authState;
final Ref ref;
/// ///
/// UI INTERACTION /// UI INTERACTION
@@ -99,7 +113,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
// 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);
@@ -142,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(
@@ -161,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");
@@ -197,8 +218,11 @@ class BackupNotifier extends StateNotifier<BackUpState> {
Set<AssetEntity> allUniqueAssets = Set<AssetEntity> allUniqueAssets =
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
List<String> allAssetsInDatabase = 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 =
@@ -235,8 +259,11 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// and then update the UI according to those information /// and then update the UI according to those information
/// ///
Future<void> getBackupInfo() async { Future<void> getBackupInfo() async {
await _getBackupAlbumsInfo(); await Future.wait([
await _updateServerInfo(); _getBackupAlbumsInfo(),
_updateServerInfo(),
]);
await _updateBackupAssetCount(); await _updateBackupAssetCount();
} }
@@ -287,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,
}, allAssetsInDatabase: [ deviceAssetId
...state.allAssetsInDatabase, },
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();
@@ -321,24 +366,19 @@ 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),
);
} }
Future<void> _updateServerInfo() async { Future<void> _updateServerInfo() async {
var serverInfo = await _serverInfoService.getServerInfo(); var serverInfo = await _serverInfoService.getServerInfo();
// Update server info // Update server info
state = state.copyWith( if (serverInfo != null) {
serverInfo: ServerInfo( state = state.copyWith(
diskSize: serverInfo.diskSize, serverInfo: serverInfo,
diskUse: serverInfo.diskUse, );
diskAvailable: serverInfo.diskAvailable, }
diskSizeRaw: serverInfo.diskSizeRaw,
diskUseRaw: serverInfo.diskUseRaw,
diskAvailableRaw: serverInfo.diskAvailableRaw,
diskUsagePercentage: serverInfo.diskUsagePercentage,
),
);
} }
void resumeBackup() { void resumeBackup() {
@@ -375,5 +415,6 @@ final backupProvider =
ref.watch(backupServiceProvider), ref.watch(backupServiceProvider),
ref.watch(serverInfoServiceProvider), ref.watch(serverInfoServiceProvider),
ref.watch(authenticationProvider), ref.watch(authenticationProvider),
ref,
); );
}); });

View File

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

View File

@@ -2,47 +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: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;
final backupServiceProvider = final backupServiceProvider = Provider(
Provider((ref) => BackupService(ref.watch(networkServiceProvider))); (ref) => BackupService(
ref.watch(apiServiceProvider),
),
);
class BackupService { class BackupService {
final NetworkService _networkService; final ApiService _apiService;
BackupService(this._networkService); BackupService(this._apiService);
Future<List<String>> getDeviceBackupAsset() async { Future<List<String>?> getDeviceBackupAsset() async {
String deviceId = Hive.box(userInfoBox).get(deviceIdKey); String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
Response response = try {
await _networkService.getRequest(url: "asset/$deviceId"); return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId);
List<dynamic> result = jsonDecode(response.toString()); } catch (e) {
debugPrint('Error [getDeviceBackupAsset] ${e.toString()}');
return result.cast<String>(); return null;
}
} }
backupAsset( backupAsset(
Set<AssetEntity> assetList, Set<AssetEntity> assetList,
http.CancellationToken cancelToken, http.CancellationToken cancelToken,
Function(String, String) singleAssetDoneCb, Function(String, String) singleAssetDoneCb,
Function(int, int) uploadProgress) async { 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) {
@@ -69,27 +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( var req = MultipartRequest(
'POST', Uri.parse('$savedEndpoint/asset/upload'), 'POST',
onProgress: ((bytes, totalBytes) => Uri.parse('$savedEndpoint/asset/upload'),
uploadProgress(bytes, totalBytes))); 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;
@@ -101,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 {
@@ -139,15 +157,29 @@ class BackupService {
} }
} }
Future<DeviceInfoRemote> setAutoBackup( Future<DeviceInfoResponseDto> setAutoBackup(
bool status, String deviceId, String deviceType) async { bool status,
var res = await _networkService.patchRequest(url: 'device-info', data: { String deviceId,
"isAutoBackup": status, DeviceTypeEnum deviceType,
"deviceId": deviceId, ) async {
"deviceType": deviceType, 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();
}
} }
} }

View File

@@ -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';
@@ -25,7 +26,9 @@ class AlbumInfoCard extends HookConsumerWidget {
ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo); ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
ColorFilter selectedFilter = ColorFilter.mode( ColorFilter selectedFilter = ColorFilter.mode(
Theme.of(context).primaryColor.withAlpha(100), BlendMode.darken); Theme.of(context).primaryColor.withAlpha(100),
BlendMode.darken,
);
ColorFilter excludedFilter = ColorFilter excludedFilter =
ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken); ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
ColorFilter unselectedFilter = ColorFilter unselectedFilter =
@@ -37,10 +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( style: TextStyle(
fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold), 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) {
@@ -48,15 +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( style: TextStyle(
fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold), fontSize: 10,
), color: Colors.white,
fontWeight: FontWeight.bold,
),
).tr(),
backgroundColor: Colors.red[300], backgroundColor: Colors.red[300],
); );
} }
return Container(); return const SizedBox();
} }
_buildImageFilter() { _buildImageFilter() {
@@ -77,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,
); );
@@ -104,7 +113,7 @@ class AlbumInfoCard extends HookConsumerWidget {
.contains(albumInfo)) { .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,
); );
@@ -137,21 +146,26 @@ class AlbumInfoCard extends HookConsumerWidget {
height: 200, height: 200,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12), topLeft: Radius.circular(12),
topRight: 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( : const AssetImage(
'assets/immich-logo-no-outline.png') 'assets/immich-logo-no-outline.png',
as ImageProvider, ) 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(
@@ -169,17 +183,22 @@ class AlbumInfoCard extends HookConsumerWidget {
Text( Text(
albumInfo.name, albumInfo.name,
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold), 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.assetCount.toString() +
(albumInfo.isAll ? " (ALL)" : ""), (albumInfo.isAll
? " (${'backup_all'.tr()})"
: ""),
style: TextStyle( style: TextStyle(
fontSize: 12, color: Colors.grey[600]), fontSize: 12,
color: Colors.grey[600],
),
), ),
) )
], ],

View File

@@ -1,15 +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( const BackupInfoCard({
{Key? key, Key? key,
required this.title, required this.title,
required this.subtitle, required this.subtitle,
required this.info}) required this.info,
: super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -44,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(),
], ],
), ),
), ),

View File

@@ -20,10 +20,13 @@ class AlbumPreviewPage extends HookConsumerWidget {
await album.getAssetListRange(start: 0, end: album.assetCount); await album.getAssetListRange(start: 0, end: album.assetCount);
} }
useEffect(() { useEffect(
_getAssetsInAlbum(); () {
return null; _getAssetsInAlbum();
}, []); return null;
},
[],
);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@@ -39,9 +42,10 @@ class AlbumPreviewPage extends HookConsumerWidget {
child: Text( child: Text(
"ID ${album.id}", "ID ${album.id}",
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.grey[600], color: Colors.grey[600],
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold,
),
), ),
), ),
], ],
@@ -59,9 +63,11 @@ class AlbumPreviewPage extends HookConsumerWidget {
), ),
itemCount: assets.value.length, itemCount: assets.value.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
Future<Uint8List?> thumbData = assets.value[index] Future<Uint8List?> thumbData =
.thumbnailDataWithSize(const ThumbnailSize(200, 200), assets.value[index].thumbnailDataWithSize(
quality: 50); const ThumbnailSize(200, 200),
quality: 50,
);
return FutureBuilder<Uint8List?>( return FutureBuilder<Uint8List?>(
future: thumbData, future: thumbData,

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package: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).getBackupInfo(); () {
return null; ref.read(backupProvider.notifier).getBackupInfo();
}, []); return null;
},
[],
);
_buildAlbumSelectionList() { _buildAlbumSelectionList() {
if (availableAlbums.isEmpty) { if (availableAlbums.isEmpty) {
@@ -41,8 +45,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
? const EdgeInsets.only(left: 16.00) ? const EdgeInsets.only(left: 16.00)
: const EdgeInsets.all(0), : const EdgeInsets.all(0),
child: AlbumInfoCard( child: AlbumInfoCard(
imageData: thumbnailData, imageData: thumbnailData,
albumInfo: availableAlbums[index].albumEntity), albumInfo: availableAlbums[index].albumEntity,
),
); );
}), }),
), ),
@@ -55,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,
); );
@@ -72,13 +77,15 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
child: Chip( child: Chip(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)), borderRadius: BorderRadius.circular(5),
),
label: Text( label: Text(
album.name, album.name,
style: const TextStyle( style: const TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold,
),
), ),
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
deleteIconColor: Colors.white, deleteIconColor: Colors.white,
@@ -108,13 +115,15 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
child: Chip( child: Chip(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)), borderRadius: BorderRadius.circular(5),
),
label: Text( label: Text(
album.name, album.name,
style: const TextStyle( style: const TextStyle(
fontSize: 10, fontSize: 10,
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold,
),
), ),
backgroundColor: Colors.red[300], backgroundColor: Colors.red[300],
deleteIconColor: Colors.white, deleteIconColor: Colors.white,
@@ -136,20 +145,21 @@ 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
@@ -181,12 +191,13 @@ 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( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 14,
color: Colors.grey[700]), color: Colors.grey[700],
), ),
).tr(),
trailing: Text( trailing: Text(
ref ref
.watch(backupProvider) .watch(backupProvider)
@@ -203,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,
@@ -231,24 +243,27 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)), 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( style: TextStyle(
fontSize: 14, color: Colors.grey[700]), fontSize: 14,
), color: Colors.grey[700],
),
).tr(),
], ],
), ),
), ),

View File

@@ -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.watch(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(
@@ -122,8 +132,10 @@ class BackupControllerPage extends HookConsumerWidget {
.setAutoBackup(true); .setAutoBackup(true);
} }
}, },
child: Text("Turn $backupBtnText Backup", child: Text(
style: const TextStyle(fontWeight: FontWeight.bold)), backupBtnText,
style: const TextStyle(fontWeight: FontWeight.bold),
),
), ),
) )
], ],
@@ -133,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}, ";
} }
@@ -150,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) {
@@ -183,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();
} }
} }
@@ -206,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 backed up", "backup_controller_page_to_backup",
style: TextStyle(color: Color(0xFF808080), fontSize: 12), style: TextStyle(color: Color(0xFF808080), fontSize: 12),
), ).tr(),
_buildSelectedAlbumName(), _buildSelectedAlbumName(),
_buildExcludedAlbumName() _buildExcludedAlbumName()
], ],
@@ -233,63 +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: "Backed up photos and videos", subtitle: "backup_controller_page_backup_sub".tr(),
info: "${backupState.selectedAlbumsBackupAssetsIds.length}", info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
), ),
BackupInfoCard( BackupInfoCard(
title: "Remainder", title: "backup_controller_page_remainder".tr(),
subtitle: "Remaining photos and albums to back up from selection", subtitle: "backup_controller_page_remainder_sub".tr(),
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(),
), ),
), ),
) )

View 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],
),
),
],
),
),
)
],
),
),
);
}),
),
);
}
}

View File

@@ -1,55 +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;
}

View File

@@ -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,26 +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)';
@@ -79,28 +57,6 @@ 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() => String toString() =>
'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)'; 'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)';

View File

@@ -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,28 +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)';

View File

@@ -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()
@@ -14,7 +14,8 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
void addSelectedDateGroup(String dateGroupTitle) { void addSelectedDateGroup(String dateGroupTitle) {
state = state.copyWith( state = state.copyWith(
selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle}); selectedDateGroup: {...state.selectedDateGroup, dateGroupTitle},
);
} }
void removeSelectedDateGroup(String dateGroupTitle) { void removeSelectedDateGroup(String dateGroupTitle) {
@@ -25,36 +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 =
state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems); state.copyWith(isMultiSelectEnable: true, selectedItems: selectedItems);
} }
void disableMultiSelect() { void disableMultiSelect() {
state = state.copyWith( state = state.copyWith(
isMultiSelectEnable: false, selectedItems: {}, selectedDateGroup: {}); 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);
} }
@@ -64,4 +68,5 @@ class HomePageStateNotifier extends StateNotifier<HomePageState> {
final homePageStateProvider = final homePageStateProvider =
StateNotifierProvider<HomePageStateNotifier, HomePageState>( StateNotifierProvider<HomePageStateNotifier, HomePageState>(
((ref) => HomePageStateNotifier())); ((ref) => HomePageStateNotifier()),
);

View File

@@ -73,10 +73,12 @@ class UploadProfileImageState {
class UploadProfileImageNotifier class UploadProfileImageNotifier
extends StateNotifier<UploadProfileImageState> { extends StateNotifier<UploadProfileImageState> {
UploadProfileImageNotifier(this._userSErvice) UploadProfileImageNotifier(this._userSErvice)
: super(UploadProfileImageState( : super(
profileImagePath: '', UploadProfileImageState(
status: UploadProfileStatus.idle, profileImagePath: '',
)); status: UploadProfileStatus.idle,
),
);
final UserService _userSErvice; final UserService _userSErvice;
@@ -88,8 +90,9 @@ class UploadProfileImageNotifier
if (res != null) { if (res != null) {
debugPrint("Succesfully upload profile image"); debugPrint("Succesfully upload profile image");
state = state.copyWith( state = state.copyWith(
status: UploadProfileStatus.success, status: UploadProfileStatus.success,
profileImagePath: res.profileImagePath); profileImagePath: res.profileImagePath,
);
return true; return true;
} }
@@ -100,4 +103,5 @@ class UploadProfileImageNotifier
final uploadProfileImageProvider = final uploadProfileImageProvider =
StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>( StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>(
((ref) => UploadProfileImageNotifier(ref.watch(userServiceProvider)))); ((ref) => UploadProfileImageNotifier(ref.watch(userServiceProvider))),
);

View File

@@ -1,120 +1,51 @@
import 'dart:convert'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/models/delete_asset_response.model.dart'; import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/modules/home/models/get_all_asset_response.model.dart'; import 'package:openapi/api.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/shared/models/immich_asset_with_exif.model.dart';
import 'package:immich_mobile/shared/services/network.service.dart';
final assetServiceProvider = final assetServiceProvider = Provider(
Provider((ref) => AssetService(ref.watch(networkServiceProvider))); (ref) => AssetService(
ref.watch(apiServiceProvider),
),
);
class AssetService { class AssetService {
final NetworkService _networkService; final ApiService _apiService;
AssetService(this._networkService);
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( Future<AssetResponseDto?> getAssetById(String assetId) async {
Set<ImmichAsset> deleteAssets) 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 return await _apiService.assetApi
.deleteRequest(url: "asset/", data: {"ids": payload}); .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;

View File

@@ -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';
@@ -14,7 +15,9 @@ class ControlBottomAppBar extends StatelessWidget {
height: MediaQuery.of(context).size.height * 0.15, height: MediaQuery.of(context).size.height * 0.15,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.circular(15), topRight: Radius.circular(15)), 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(
@@ -26,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,
@@ -47,12 +50,12 @@ class ControlBottomAppBar extends StatelessWidget {
} }
class ControlBoxButton extends StatelessWidget { class ControlBoxButton extends StatelessWidget {
const ControlBoxButton( const ControlBoxButton({
{Key? key, Key? key,
required this.label, required this.label,
required this.iconData, required this.iconData,
required this.onPressed}) required this.onPressed,
: super(key: key); }) : super(key: key);
final String label; final String label;
final IconData iconData; final IconData iconData;

View File

@@ -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,14 +12,15 @@ 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 = var formatDateTemplate = currentYear == groupYear
currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy'; ? "daily_title_text_date".tr()
: "daily_title_text_date_year".tr();
var dateText = var dateText =
DateFormat(formatDateTemplate).format(DateTime.parse(isoDate)); DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
var isMultiSelectEnable = var isMultiSelectEnable =
@@ -73,7 +74,11 @@ class DailyTitleText extends ConsumerWidget {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 29.0, bottom: 29.0, left: 12.0, right: 12.0), top: 29.0,
bottom: 29.0,
left: 12.0,
right: 12.0,
),
child: Row( child: Row(
children: [ children: [
Text( Text(

View File

@@ -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,18 +14,17 @@ 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( content: const Text("delete_dialog_alert").tr(),
"These items will be permanently deleted from Immich and from your device"),
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: () {
@@ -36,9 +36,9 @@ class DeleteDialog extends ConsumerWidget {
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(),
), ),
], ],
); );

View File

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

View File

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

View File

@@ -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,30 +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] style: const TextStyle(
.duration color: Colors.white,
.toString() fontSize: 10,
.substring(0, 7), ),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
),
),
const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
), ),
) const Icon(
Icons.play_circle_outline_rounded,
color: Colors.white,
),
],
),
),
], ],
), ),
); );

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package: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,12 +193,13 @@ 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.watch(authenticationProvider.notifier).logout(); await ref.watch(authenticationProvider.notifier).logout();
@@ -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],

View File

@@ -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,14 +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 = var isMultiSelectEnable =
ref.watch(homePageStateProvider).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,
@@ -61,7 +60,7 @@ class ThumbnailImage extends HookConsumerWidget {
.watch(homePageStateProvider.notifier) .watch(homePageStateProvider.notifier)
.addSingleSelectedItem(asset); .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:
@@ -74,9 +73,10 @@ class ThumbnailImage extends HookConsumerWidget {
} else { } else {
AutoRouter.of(context).push( AutoRouter.of(context).push(
VideoViewerRoute( VideoViewerRoute(
videoUrl: videoUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}', '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
asset: asset), asset: asset,
),
); );
} }
} }
@@ -94,14 +94,16 @@ class ThumbnailImage extends HookConsumerWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
border: isMultiSelectEnable && selectedAsset.contains(asset) border: isMultiSelectEnable && selectedAsset.contains(asset)
? Border.all( ? Border.all(
color: Theme.of(context).primaryColorLight, width: 10) 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: { httpHeaders: {
@@ -112,9 +114,11 @@ class ThumbnailImage extends HookConsumerWidget {
Transform.scale( Transform.scale(
scale: 0.2, scale: 0.2,
child: CircularProgressIndicator( child: CircularProgressIndicator(
value: downloadProgress.progress), 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,
@@ -122,17 +126,14 @@ 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,

View File

@@ -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(),
],
], ],
), ),
); );

View File

@@ -1,10 +1,8 @@
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;
@@ -13,8 +11,7 @@ class AuthenticationState {
final bool isAdmin; final bool isAdmin;
final bool shouldChangePassword; 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,
@@ -31,7 +28,7 @@ class AuthenticationState {
AuthenticationState copyWith({ AuthenticationState copyWith({
String? deviceId, String? deviceId,
String? deviceType, DeviceTypeEnum? deviceType,
String? userId, String? userId,
String? userEmail, String? userEmail,
bool? isAuthenticated, bool? isAuthenticated,
@@ -40,7 +37,7 @@ class AuthenticationState {
bool? isAdmin, bool? isAdmin,
bool? shouldChangePassword, bool? shouldChangePassword,
String? profileImagePath, String? profileImagePath,
DeviceInfoRemote? deviceInfo, DeviceInfoResponseDto? deviceInfo,
}) { }) {
return AuthenticationState( return AuthenticationState(
deviceId: deviceId ?? this.deviceId, deviceId: deviceId ?? this.deviceId,
@@ -57,45 +54,6 @@ class AuthenticationState {
); );
} }
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({'shouldChangePassword': shouldChangePassword});
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,
shouldChangePassword: map['shouldChangePassword'] ?? 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
String toString() { String toString() {
return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, shouldChangePassword: $shouldChangePassword, 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)';

View File

@@ -16,9 +16,10 @@ class HiveSavedLoginInfo {
@HiveField(3) @HiveField(3)
bool isSaveLogin; bool isSaveLogin;
HiveSavedLoginInfo( HiveSavedLoginInfo({
{required this.email, required this.email,
required this.password, required this.password,
required this.serverUrl, required this.serverUrl,
required this.isSaveLogin}); required this.isSaveLogin,
});
} }

View File

@@ -1,23 +1,23 @@
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( AuthenticationNotifier(
this._deviceInfoService, this._backupService, this._networkService) this._deviceInfoService,
: super( this._backupService,
this._apiService,
) : super(
AuthenticationState( AuthenticationState(
deviceId: "", deviceId: "",
deviceType: "", deviceType: DeviceTypeEnum.ANDROID,
userId: "", userId: "",
userEmail: "", userEmail: "",
firstName: '', firstName: '',
@@ -26,12 +26,11 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
isAdmin: false, isAdmin: false,
shouldChangePassword: 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,
), ),
@@ -40,10 +39,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
final DeviceInfoService _deviceInfoService; final DeviceInfoService _deviceInfoService;
final BackupService _backupService; final BackupService _backupService;
final NetworkService _networkService; final ApiService _apiService;
Future<bool> login(String email, String password, String serverEndpoint, Future<bool> login(
bool isSavedLoginInfo) async { 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);
@@ -52,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;
} }
@@ -72,56 +75,73 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
// Make sign-in request // Make sign-in request
try { try {
Response res = await _networkService.postRequest( var loginResponse = await _apiService.authenticationApi.login(
url: 'auth/login', data: {'email': email, 'password': password}); 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,
shouldChangePassword: payload.shouldChangePassword, 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) Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox)
.delete(savedLoginInfoKey); .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.postRequest( DeviceInfoResponseDto? deviceInfo =
url: 'device-info', await _apiService.deviceInfoApi.createDeviceInfo(
data: { CreateDeviceInfoDto(
'deviceId': state.deviceId, deviceId: state.deviceId,
'deviceType': state.deviceType, deviceType: state.deviceType,
}, ),
); );
DeviceInfoRemote deviceInfo = DeviceInfoRemote.fromJson(res.toString()); if (deviceInfo == null) {
debugPrint('Device Info Response is null');
return false;
}
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;
@@ -129,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: '',
shouldChangePassword: false,
isAuthenticated: false,
isAdmin: false,
deviceInfo: DeviceInfoRemote(
id: 0,
userId: "",
deviceId: "",
deviceType: "",
notificationToken: "",
createdAt: "",
isAutoBackup: false,
),
);
return true; return true;
} }
@@ -157,11 +157,13 @@ 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 = DeviceTypeEnum deviceType = deviceInfo["deviceType"];
DeviceInfoResponseDto updatedDeviceInfo =
await _backupService.setAutoBackup(backupState, deviceId, deviceType); await _backupService.setAutoBackup(backupState, deviceId, deviceType);
state = state.copyWith(deviceInfo: deviceInfoRemote);
state = state.copyWith(deviceInfo: updatedDeviceInfo);
} }
updateUserProfileImagePath(String path) { updateUserProfileImagePath(String path) {
@@ -169,19 +171,20 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
} }
Future<bool> changePassword(String newPassword) async { Future<bool> changePassword(String newPassword) async {
Response res = await _networkService.putRequest( try {
url: 'user', await _apiService.userApi.updateUser(
data: { UpdateUserDto(
'id': state.userId, id: state.userId,
'password': newPassword, password: newPassword,
'shouldChangePassword': false, shouldChangePassword: false,
}, ),
); );
if (res.statusCode == 200) {
state = state.copyWith(shouldChangePassword: false); state = state.copyWith(shouldChangePassword: false);
return true; return true;
} else { } catch (e) {
debugPrint("Error changing password $e");
return false; return false;
} }
} }
@@ -192,6 +195,6 @@ final authenticationProvider =
return AuthenticationNotifier( return AuthenticationNotifier(
ref.watch(deviceInfoServiceProvider), ref.watch(deviceInfoServiceProvider),
ref.watch(backupServiceProvider), ref.watch(backupServiceProvider),
ref.watch(networkServiceProvider), ref.watch(apiServiceProvider),
); );
}); });

View File

@@ -18,6 +18,7 @@ class ChangePasswordForm extends HookConsumerWidget {
final confirmPasswordController = final confirmPasswordController =
useTextEditingController.fromValue(TextEditingValue.empty); useTextEditingController.fromValue(TextEditingValue.empty);
final authState = ref.watch(authenticationProvider); final authState = ref.watch(authenticationProvider);
final formKey = GlobalKey<FormState>();
return Center( return Center(
child: ConstrainedBox( child: ConstrainedBox(
@@ -47,15 +48,24 @@ class ChangePasswordForm extends HookConsumerWidget {
), ),
), ),
), ),
PasswordInput(controller: passwordController), Form(
ConfirmPasswordInput( key: formKey,
originalController: passwordController, child: Column(
confirmController: confirmPasswordController, children: [
), PasswordInput(controller: passwordController),
Align( Padding(
alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ChangePasswordButton( child: ConfirmPasswordInput(
passwordController: passwordController), originalController: passwordController,
confirmController: confirmPasswordController,
),
),
ChangePasswordButton(
passwordController: passwordController,
formKey: formKey,
),
],
),
) )
], ],
), ),
@@ -119,23 +129,26 @@ class ConfirmPasswordInput extends StatelessWidget {
class ChangePasswordButton extends ConsumerWidget { class ChangePasswordButton extends ConsumerWidget {
final TextEditingController passwordController; final TextEditingController passwordController;
final GlobalKey<FormState> formKey;
const ChangePasswordButton({ const ChangePasswordButton({
Key? key, Key? key,
required this.passwordController, required this.passwordController,
required this.formKey,
}) : super(key: key); }) : super(key: key);
@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 {
if (formKey.currentState!.validate()) {
var isSuccess = await ref var isSuccess = await ref
.watch(authenticationProvider.notifier) .watch(authenticationProvider.notifier)
.changePassword(passwordController.value.text); .changePassword(passwordController.value.text);
@@ -151,10 +164,12 @@ class ChangePasswordButton extends ConsumerWidget {
AutoRouter.of(context).replace(const LoginRoute()); AutoRouter.of(context).replace(const LoginRoute());
} }
} }
}, }
child: const Text( },
"Change Password", child: const Text(
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), "Change Password",
)); style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
);
} }
} }

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@@ -21,22 +22,25 @@ class LoginForm extends HookConsumerWidget {
final passwordController = final passwordController =
useTextEditingController.fromValue(TextEditingValue.empty); useTextEditingController.fromValue(TextEditingValue.empty);
final serverEndpointController = final serverEndpointController =
useTextEditingController(text: 'http://your-server-ip:2283/api'); 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(
@@ -70,15 +74,17 @@ class LoginForm extends HookConsumerWidget {
dense: true, dense: true,
side: const BorderSide(color: Colors.grey, width: 1.5), side: const BorderSide(color: Colors.grey, width: 1.5),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5)), borderRadius: BorderRadius.circular(5),
),
enableFeedback: true, enableFeedback: true,
title: const Text( title: const Text(
"Stay logged in", "login_form_save_login",
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.grey), color: Colors.grey,
), ),
).tr(),
value: isSaveLoginInfo.value, value: isSaveLoginInfo.value,
onChanged: (switchValue) { onChanged: (switchValue) {
if (switchValue != null) { if (switchValue != null) {
@@ -107,29 +113,21 @@ class ServerEndpointInput extends StatelessWidget {
: super(key: key); : super(key: key);
String? _validateInput(String? url) { String? _validateInput(String? url) {
if (url == null) { if (url?.startsWith(RegExp(r'https?://')) == true) {
return null; return null;
} else {
return 'login_form_err_http'.tr();
} }
if (url.isEmpty) {
return 'Server endpoint is required';
}
if (!url.startsWith(RegExp(r'https?://'))) {
return 'Please specify http:// or https://';
}
return null;
} }
@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', labelText: 'login_form_endpoint_url'.tr(),
border: OutlineInputBorder(), border: const OutlineInputBorder(),
hintText: 'http://your-server-ip:port', hintText: 'login_form_endpoint_hint'.tr(),
), ),
validator: _validateInput, validator: _validateInput,
autovalidateMode: AutovalidateMode.always, autovalidateMode: AutovalidateMode.always,
@@ -144,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;
} }
@@ -154,10 +154,10 @@ class EmailInput extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextFormField( return TextFormField(
controller: controller, controller: controller,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Email', labelText: 'login_form_label_email'.tr(),
border: OutlineInputBorder(), border: const OutlineInputBorder(),
hintText: 'youremail@email.com', hintText: 'login_form_email_hint'.tr(),
), ),
validator: _validateInput, validator: _validateInput,
autovalidateMode: AutovalidateMode.always, autovalidateMode: AutovalidateMode.always,
@@ -175,10 +175,11 @@ class PasswordInput extends StatelessWidget {
return TextFormField( return TextFormField(
obscureText: true, obscureText: true,
controller: controller, controller: controller,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Password', labelText: 'login_form_label_password'.tr(),
border: OutlineInputBorder(), border: const OutlineInputBorder(),
hintText: 'password'), hintText: 'login_form_password_hint'.tr(),
),
); );
} }
} }
@@ -200,43 +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 =
.watch(authenticationProvider.notifier) await ref.watch(authenticationProvider.notifier).login(
.login(emailController.text, passwordController.text, emailController.text,
serverEndpointController.text, isSavedLoginInfo); 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) { if (ref.watch(authenticationProvider).shouldChangePassword &&
AutoRouter.of(context).push(const ChangePasswordRoute()); !ref.watch(authenticationProvider).isAdmin) {
} else { AutoRouter.of(context).push(const ChangePasswordRoute());
ref.watch(backupProvider.notifier).resumeBackup();
AutoRouter.of(context).pushNamed("/tab-controller-page");
}
} else { } else {
ImmichToast.show( ref.watch(backupProvider.notifier).resumeBackup();
context: context, AutoRouter.of(context).pushNamed("/tab-controller-page");
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(),
);
} }
} }

View File

@@ -1,84 +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;
}
}

View File

@@ -1,85 +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;
}
}

View File

@@ -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,8 +44,9 @@ 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( searchResult: List<AssetResponseDto>.from(
map['searchResult']?.map((x) => ImmichAsset.fromMap(x))), map['searchResult']?.map((x) => AssetResponseDto.mapFromJson(x)),
),
); );
} }

View File

@@ -1,9 +1,8 @@
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(this._searchService) SearchPageStateNotifier(this._searchService)
@@ -58,25 +57,18 @@ final searchPageStateProvider =
}); });
final getCuratedLocationProvider = final getCuratedLocationProvider =
FutureProvider.autoDispose<List<CuratedLocation>>((ref) async { FutureProvider.autoDispose<List<CuratedLocationsResponseDto>>((ref) async {
final SearchService searchService = ref.watch(searchServiceProvider); 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 = ref.watch(searchServiceProvider); 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 [];
}
}); });

View File

@@ -3,8 +3,8 @@ 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(this._searchService) SearchResultPageNotifier(this._searchService)
@@ -21,19 +21,29 @@ class SearchResultPageNotifier extends StateNotifier<SearchResultPageState> {
void search(String searchTerm) async { void search(String searchTerm) async {
state = state.copyWith( state = state.copyWith(
searchResult: [], isError: false, isLoading: true, isSuccess: false); 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( state = state.copyWith(
searchResult: assets, searchResult: assets,
isError: false, isError: false,
isLoading: false, isLoading: false,
isSuccess: true); isSuccess: true,
);
} else { } else {
state = state.copyWith( state = state.copyWith(
searchResult: [], isError: true, isLoading: false, isSuccess: false); searchResult: [],
isError: true,
isLoading: false,
isSuccess: false,
);
} }
} }
} }
@@ -48,7 +58,11 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) {
var assets = ref.watch(searchResultPageProvider).searchResult; var assets = ref.watch(searchResultPageProvider).searchResult;
assets.sortByCompare<DateTime>( assets.sortByCompare<DateTime>(
(e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a)); (e) => DateTime.parse(e.createdAt),
return assets.groupListsBy((element) => (a, b) => b.compareTo(a),
DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt))); );
return assets.groupListsBy(
(element) =>
DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)),
);
}); });

View File

@@ -1,79 +1,54 @@
import 'dart:convert';
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/search/models/curated_location.model.dart'; import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/modules/search/models/curated_object.model.dart'; import 'package:openapi/api.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
import 'package:immich_mobile/shared/services/network.service.dart';
final searchServiceProvider = final searchServiceProvider = Provider(
Provider((ref) => SearchService(ref.watch(networkServiceProvider))); (ref) => SearchService(
ref.watch(apiServiceProvider),
),
);
class SearchService { class SearchService {
final NetworkService _networkService; final ApiService _apiService;
SearchService(this._networkService); 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 [];
} }
} }
} }

View File

@@ -1,12 +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( SearchBar({
{Key? key, required this.searchFocusNode, required this.onSubmitted}) Key? key,
: super(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;
@@ -25,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,
@@ -47,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),
), ),
), ),

View File

@@ -2,15 +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( const ThumbnailWithInfo({
{Key? key, Key? key,
required this.textInfo, required this.textInfo,
required this.imageUrl, required this.imageUrl,
required this.onTap}) required this.onTap,
: super(key: key); }) : super(key: key);
final String textInfo; final String textInfo;
final String imageUrl; final String imageUrl;
@@ -55,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,

View File

@@ -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,6 +12,7 @@ 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 {
@@ -24,15 +24,18 @@ class SearchPage extends HookConsumerWidget {
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(),
], ],
), ),
), ),

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package: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(),
], ],
), ),
), ),

View File

@@ -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,8 +14,8 @@ 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(
@@ -28,35 +26,6 @@ class AssetSelectionPageResult {
); );
} }
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)';

View File

@@ -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,9 +22,9 @@ 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,
}) { }) {
@@ -43,49 +41,6 @@ class AssetSelectionState {
); );
} }
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)';
@@ -99,10 +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;
} }

View File

@@ -1,117 +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;
}
}

View File

@@ -13,4 +13,5 @@ class AlbumTitleNotifier extends StateNotifier<String> {
} }
final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>( final albumTitleProvider = StateNotifierProvider<AlbumTitleNotifier, String>(
(ref) => AlbumTitleNotifier()); (ref) => AlbumTitleNotifier(),
);

View File

@@ -30,7 +30,10 @@ class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
} }
Future<bool> changeAlbumTitle( Future<bool> changeAlbumTitle(
String albumId, String ownerId, String newAlbumTitle) async { String albumId,
String ownerId,
String newAlbumTitle,
) async {
SharedAlbumService service = ref.watch(sharedAlbumServiceProvider); SharedAlbumService service = ref.watch(sharedAlbumServiceProvider);
bool isSuccess = bool isSuccess =

View File

@@ -1,41 +1,46 @@
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( void removeAssetsInMonth(
String removedMonth, List<ImmichAsset> assetsInMonth) { String removedMonth,
Set<ImmichAsset> currentAssetList = state.selectedNewAssetsForAlbum; List<AssetResponseDto> assetsInMonth,
) {
Set<AssetResponseDto> currentAssetList = state.selectedNewAssetsForAlbum;
Set<String> currentMonthList = state.selectedMonths; Set<String> currentMonthList = state.selectedMonths;
currentMonthList currentMonthList
.removeWhere((selectedMonth) => selectedMonth == removedMonth); .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( state = state.copyWith(
selectedNewAssetsForAlbum: currentAssetList, selectedNewAssetsForAlbum: currentAssetList,
selectedMonths: currentMonthList); selectedMonths: currentMonthList,
);
} }
void addAdditionalAssets(List<ImmichAsset> assets) { void addAdditionalAssets(List<AssetResponseDto> assets) {
state = state.copyWith( state = state.copyWith(
selectedAdditionalAssetsForAlbum: { selectedAdditionalAssetsForAlbum: {
...state.selectedAdditionalAssetsForAlbum, ...state.selectedAdditionalAssetsForAlbum,
@@ -44,7 +49,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
); );
} }
void addAllAssetsInMonth(String month, List<ImmichAsset> assetsInMonth) { void addAllAssetsInMonth(String month, List<AssetResponseDto> assetsInMonth) {
state = state.copyWith( state = state.copyWith(
selectedMonths: {...state.selectedMonths, month}, selectedMonths: {...state.selectedMonths, month},
selectedNewAssetsForAlbum: { selectedNewAssetsForAlbum: {
@@ -54,7 +59,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
); );
} }
void addNewAssets(List<ImmichAsset> assets) { void addNewAssets(List<AssetResponseDto> assets) {
state = state.copyWith( state = state.copyWith(
selectedNewAssetsForAlbum: { selectedNewAssetsForAlbum: {
...state.selectedNewAssetsForAlbum, ...state.selectedNewAssetsForAlbum,
@@ -63,20 +68,20 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
); );
} }
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);
} }
@@ -104,7 +109,7 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
); );
} }
void addAssetsInAlbumViewer(List<ImmichAsset> assets) { void addAssetsInAlbumViewer(List<AssetResponseDto> assets) {
state = state.copyWith( state = state.copyWith(
selectedAssetsInAlbumViewer: { selectedAssetsInAlbumViewer: {
...state.selectedAssetsInAlbumViewer, ...state.selectedAssetsInAlbumViewer,
@@ -113,10 +118,10 @@ class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
); );
} }
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);
} }

View File

@@ -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(this._sharedAlbumService) : super([]); SharedAlbumNotifier(this._sharedAlbumService) : super([]);
final 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,12 +53,12 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
} }
final sharedAlbumProvider = final sharedAlbumProvider =
StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) { StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
return SharedAlbumNotifier(ref.watch(sharedAlbumServiceProvider)); 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 = final SharedAlbumService sharedAlbumService =
ref.watch(sharedAlbumServiceProvider); ref.watch(sharedAlbumServiceProvider);

View File

@@ -1,10 +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 = final suggestedSharedUsersProvider =
FutureProvider.autoDispose<List<User>>((ref) async { FutureProvider.autoDispose<List<UserResponseDto>>((ref) async {
UserService userService = ref.watch(userServiceProvider); UserService userService = ref.watch(userServiceProvider);
return await userService.getAllUsersInfo(); return await userService.getAllUsersInfo(isAll: false) ?? [];
}); });

View File

@@ -1,47 +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: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/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 sharedAlbumServiceProvider = final sharedAlbumServiceProvider = Provider(
Provider((ref) => SharedAlbumService(ref.watch(networkServiceProvider))); (ref) => SharedAlbumService(
ref.watch(apiServiceProvider),
),
);
class SharedAlbumService { class SharedAlbumService {
final NetworkService _networkService; final ApiService _apiService;
SharedAlbumService(this._networkService); 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) {
@@ -50,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;
@@ -83,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;
@@ -103,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()}");
@@ -118,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) {
@@ -133,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) {
@@ -152,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) {

View File

@@ -5,12 +5,12 @@ class AlbumActionOutlinedButton extends StatelessWidget {
final String labelText; final String labelText;
final IconData iconData; final IconData iconData;
const AlbumActionOutlinedButton( const AlbumActionOutlinedButton({
{Key? key, Key? key,
this.onPressed, this.onPressed,
required this.labelText, required this.labelText,
required this.iconData}) required this.iconData,
: super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -31,7 +31,10 @@ class AlbumActionOutlinedButton extends StatelessWidget {
label: Text( label: Text(
labelText, labelText,
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.bold, color: Colors.black87), fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
), ),
onPressed: onPressed, onPressed: onPressed,
), ),

View File

@@ -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';
@@ -30,7 +31,10 @@ class AlbumTitleTextField extends ConsumerWidget {
}, },
focusNode: albumTitleTextFieldFocusNode, focusNode: albumTitleTextFieldFocusNode,
style: TextStyle( style: TextStyle(
fontSize: 28, color: Colors.grey[700], fontWeight: FontWeight.bold), fontSize: 28,
color: Colors.grey[700],
fontWeight: FontWeight.bold,
),
controller: albumTitleController, controller: albumTitleController,
onTap: () { onTap: () {
isAlbumTitleTextFieldFocus.value = true; isAlbumTitleTextFieldFocus.value = true;
@@ -59,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,

View File

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

View File

@@ -1,15 +1,18 @@
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( const AlbumViewerEditableTitle({
{Key? key, required this.albumInfo, required this.titleFocusNode}) Key? key,
: super(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) {
@@ -23,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) {
@@ -74,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(),
), ),
); );
} }

View File

@@ -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,7 +20,7 @@ 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 = final selectedAssetsInAlbumViewer =
ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer; ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
@@ -28,7 +28,7 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
ref.watch(assetSelectionProvider).isMultiselectEnable; 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:
@@ -41,9 +41,10 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
} else { } else {
AutoRouter.of(context).push( AutoRouter.of(context).push(
VideoViewerRoute( VideoViewerRoute(
videoUrl: videoUrl:
'${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}', '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}',
asset: asset), asset: asset,
),
); );
} }
} }
@@ -71,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() {
@@ -112,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() {
@@ -177,16 +171,13 @@ class AlbumViewerThumbnail extends HookConsumerWidget {
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(),
],
),
), ),
); );
} }

View File

@@ -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/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}) const AssetGridByMonth({Key? key, required this.assetGroup})
: super(key: key); : super(key: key);
@override @override

View File

@@ -2,15 +2,17 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart'; import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart'; import 'package:openapi/api.dart';
class MonthGroupTitle extends HookConsumerWidget { class MonthGroupTitle extends HookConsumerWidget {
final String month; final String month;
final List<ImmichAsset> assetGroup; final List<AssetResponseDto> assetGroup;
const MonthGroupTitle( const MonthGroupTitle({
{Key? key, required this.month, required this.assetGroup}) Key? key,
: super(key: key); required this.month,
required this.assetGroup,
}) : super(key: key);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -75,7 +77,11 @@ class MonthGroupTitle extends HookConsumerWidget {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
top: 29.0, bottom: 29.0, left: 14.0, right: 8.0), top: 29.0,
bottom: 29.0,
left: 14.0,
right: 8.0,
),
child: Row( child: Row(
children: [ children: [
GestureDetector( GestureDetector(

View File

@@ -5,10 +5,10 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart'; import 'package:immich_mobile/modules/sharing/providers/asset_selection.provider.dart';
import 'package:immich_mobile/shared/models/immich_asset.model.dart'; import 'package:openapi/api.dart';
class SelectionThumbnailImage extends HookConsumerWidget { class SelectionThumbnailImage extends HookConsumerWidget {
final ImmichAsset asset; final AssetResponseDto asset;
const SelectionThumbnailImage({Key? key, required this.asset}) const SelectionThumbnailImage({Key? key, required this.asset})
: super(key: key); : super(key: key);
@@ -18,14 +18,14 @@ class SelectionThumbnailImage 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 selectedAsset = var selectedAsset =
ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum; ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
var newAssetsForAlbum = var newAssetsForAlbum =
ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum; ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist; var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
Widget _buildSelectionIcon(ImmichAsset asset) { Widget _buildSelectionIcon(AssetResponseDto asset) {
if (selectedAsset.contains(asset) && !isAlbumExist) { if (selectedAsset.contains(asset) && !isAlbumExist) {
return Icon( return Icon(
Icons.check_circle, Icons.check_circle,
@@ -103,7 +103,7 @@ class SelectionThumbnailImage extends HookConsumerWidget {
cacheKey: "${asset.id}-${cacheKey.value}", cacheKey: "${asset.id}-${cacheKey.value}",
width: 150, width: 150,
height: 150, height: 150,
memCacheHeight: asset.type == 'IMAGE' ? 150 : 150, memCacheHeight: asset.type == AssetTypeEnum.IMAGE ? 150 : 150,
fit: BoxFit.cover, fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl, imageUrl: thumbnailRequestUrl,
httpHeaders: { httpHeaders: {
@@ -131,27 +131,26 @@ class SelectionThumbnailImage extends HookConsumerWidget {
child: _buildSelectionIcon(asset), child: _buildSelectionIcon(asset),
), ),
), ),
asset.type == 'IMAGE' if (asset.type != AssetTypeEnum.IMAGE)
? Container() Positioned(
: Positioned( bottom: 5,
bottom: 5, right: 5,
right: 5, child: Row(
child: Row( children: [
children: [ Text(
Text( asset.duration.substring(0, 7),
asset.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,
),
],
),
),
], ],
), ),
); );

View File

@@ -4,10 +4,10 @@ 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/shared/models/immich_asset.model.dart'; import 'package:openapi/api.dart';
class SharedAlbumThumbnailImage extends HookConsumerWidget { class SharedAlbumThumbnailImage extends HookConsumerWidget {
final ImmichAsset asset; final AssetResponseDto asset;
const SharedAlbumThumbnailImage({Key? key, required this.asset}) const SharedAlbumThumbnailImage({Key? key, required this.asset})
: super(key: key); : super(key: key);
@@ -18,7 +18,7 @@ class SharedAlbumThumbnailImage 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}';
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
@@ -30,7 +30,7 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget {
cacheKey: "${asset.id}-${cacheKey.value}", cacheKey: "${asset.id}-${cacheKey.value}",
width: 500, width: 500,
height: 500, height: 500,
memCacheHeight: asset.type == 'IMAGE' ? 500 : 500, memCacheHeight: 500,
fit: BoxFit.cover, fit: BoxFit.cover,
imageUrl: thumbnailRequestUrl, imageUrl: thumbnailRequestUrl,
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"}, httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
@@ -14,7 +15,8 @@ class SharingSliverAppBar extends StatelessWidget {
floating: false, floating: false,
pinned: true, pinned: true,
snap: false, snap: false,
leading: Container(), automaticallyImplyLeading: false,
// leading: Container(),
// elevation: 0, // elevation: 0,
title: Text( title: Text(
'IMMICH', 'IMMICH',
@@ -38,7 +40,8 @@ class SharingSliverAppBar extends StatelessWidget {
child: TextButton.icon( child: TextButton.icon(
style: ButtonStyle( style: ButtonStyle(
backgroundColor: MaterialStateProperty.all( backgroundColor: MaterialStateProperty.all(
Theme.of(context).primaryColor.withAlpha(20)), Theme.of(context).primaryColor.withAlpha(20),
),
// foregroundColor: MaterialStateProperty.all(Colors.white), // foregroundColor: MaterialStateProperty.all(Colors.white),
), ),
onPressed: () { onPressed: () {
@@ -50,10 +53,10 @@ class SharingSliverAppBar extends StatelessWidget {
size: 20, size: 20,
), ),
label: const Text( label: const Text(
"Create shared album", "sharing_silver_appbar_create_shared_album",
style: style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 12), TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
), ).tr(),
), ),
), ),
), ),
@@ -63,7 +66,8 @@ class SharingSliverAppBar extends StatelessWidget {
child: TextButton.icon( child: TextButton.icon(
style: ButtonStyle( style: ButtonStyle(
backgroundColor: MaterialStateProperty.all( backgroundColor: MaterialStateProperty.all(
Theme.of(context).primaryColor.withAlpha(20)), Theme.of(context).primaryColor.withAlpha(20),
),
// foregroundColor: MaterialStateProperty.all(Colors.white), // foregroundColor: MaterialStateProperty.all(Colors.white),
), ),
onPressed: null, onPressed: null,
@@ -72,10 +76,10 @@ class SharingSliverAppBar extends StatelessWidget {
size: 20, size: 20,
), ),
label: const Text( label: const Text(
"Share with partner", "sharing_silver_appbar_share_partner",
style: style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 12), TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
), ).tr(),
), ),
), ),
) )

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -6,7 +7,6 @@ import 'package:immich_mobile/constants/immich_colors.dart';
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart'; import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart'; import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/sharing/models/asset_selection_page_result.model.dart';
import 'package:immich_mobile/modules/sharing/models/shared_album.model.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/modules/sharing/services/shared_album.service.dart'; import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
@@ -18,7 +18,7 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart'; import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
import 'package:intl/intl.dart'; import 'package:openapi/api.dart';
class AlbumViewerPage extends HookConsumerWidget { class AlbumViewerPage extends HookConsumerWidget {
final String albumId; final String albumId;
@@ -29,18 +29,18 @@ class AlbumViewerPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
FocusNode titleFocusNode = useFocusNode(); FocusNode titleFocusNode = useFocusNode();
ScrollController scrollController = useScrollController(); ScrollController scrollController = useScrollController();
AsyncValue<SharedAlbum> albumInfo = AsyncValue<AlbumResponseDto?> albumInfo =
ref.watch(sharedAlbumDetailProvider(albumId)); ref.watch(sharedAlbumDetailProvider(albumId));
final userId = ref.watch(authenticationProvider).userId; final userId = ref.watch(authenticationProvider).userId;
/// Find out if the assets in album exist on the device /// Find out if the assets in album exist on the device
/// If they exist, add to selected asset state to show they are already selected. /// If they exist, add to selected asset state to show they are already selected.
void _onAddPhotosPressed(SharedAlbum albumInfo) async { void _onAddPhotosPressed(AlbumResponseDto albumInfo) async {
if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) { if (albumInfo.assets.isNotEmpty == true) {
ref ref
.watch(assetSelectionProvider.notifier) .watch(assetSelectionProvider.notifier)
.addNewAssets(albumInfo.assets!.toList()); .addNewAssets(albumInfo.assets.toList());
} }
ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true); ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
@@ -56,7 +56,9 @@ class AlbumViewerPage extends HookConsumerWidget {
var isSuccess = await ref var isSuccess = await ref
.watch(sharedAlbumServiceProvider) .watch(sharedAlbumServiceProvider)
.addAdditionalAssetToAlbum( .addAdditionalAssetToAlbum(
returnPayload.selectedAdditionalAsset, albumId); returnPayload.selectedAdditionalAsset,
albumId,
);
if (isSuccess) { if (isSuccess) {
ref.refresh(sharedAlbumDetailProvider(albumId)); ref.refresh(sharedAlbumDetailProvider(albumId));
@@ -71,10 +73,11 @@ class AlbumViewerPage extends HookConsumerWidget {
} }
} }
void _onAddUsersPressed(SharedAlbum albumInfo) async { void _onAddUsersPressed(AlbumResponseDto albumInfo) async {
List<String>? sharedUserIds = await AutoRouter.of(context) List<String>? sharedUserIds =
.push<List<String>?>( await AutoRouter.of(context).push<List<String>?>(
SelectAdditionalUserForSharingRoute(albumInfo: albumInfo)); SelectAdditionalUserForSharingRoute(albumInfo: albumInfo),
);
if (sharedUserIds != null) { if (sharedUserIds != null) {
ImmichLoadingOverlayController.appLoader.show(); ImmichLoadingOverlayController.appLoader.show();
@@ -91,7 +94,7 @@ class AlbumViewerPage extends HookConsumerWidget {
} }
} }
Widget _buildTitle(SharedAlbum albumInfo) { Widget _buildTitle(AlbumResponseDto albumInfo) {
return Padding( return Padding(
padding: const EdgeInsets.only(left: 8, right: 8, top: 16), padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
child: userId == albumInfo.ownerId child: userId == albumInfo.ownerId
@@ -101,49 +104,54 @@ class AlbumViewerPage extends HookConsumerWidget {
) )
: Padding( : Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0),
child: Text(albumInfo.albumName, child: Text(
style: const TextStyle( albumInfo.albumName,
fontSize: 24, fontWeight: FontWeight.bold)), style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
), ),
); );
} }
Widget _buildAlbumDateRange(SharedAlbum albumInfo) { Widget _buildAlbumDateRange(AlbumResponseDto albumInfo) {
if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) { String startDate = "";
String startDate = ""; DateTime parsedStartDate =
DateTime parsedStartDate = DateTime.parse(albumInfo.assets.first.createdAt);
DateTime.parse(albumInfo.assets!.first.createdAt); DateTime parsedEndDate = DateTime.parse(
DateTime parsedEndDate = albumInfo.assets.last.createdAt,
DateTime.parse(albumInfo.assets!.last.createdAt); ); //Need default.
if (parsedStartDate.year == parsedEndDate.year) { if (parsedStartDate.year == parsedEndDate.year) {
startDate = DateFormat('LLL d').format(parsedStartDate); startDate = DateFormat('LLL d').format(parsedStartDate);
} else {
startDate = DateFormat('LLL d, y').format(parsedStartDate);
}
String endDate = DateFormat('LLL d, y').format(parsedEndDate);
return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8),
child: Text(
"$startDate-$endDate",
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold, color: Colors.grey),
),
);
} else { } else {
return Container(); startDate = DateFormat('LLL d, y').format(parsedStartDate);
} }
String endDate = DateFormat('LLL d, y').format(parsedEndDate);
return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8),
child: Text(
"$startDate-$endDate",
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
);
} }
Widget _buildHeader(SharedAlbum albumInfo) { Widget _buildHeader(AlbumResponseDto albumInfo) {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildTitle(albumInfo), _buildTitle(albumInfo),
_buildAlbumDateRange(albumInfo), if (albumInfo.assets.isNotEmpty == true)
_buildAlbumDateRange(albumInfo),
SizedBox( SizedBox(
height: 60, height: 60,
child: ListView.builder( child: ListView.builder(
@@ -174,8 +182,8 @@ class AlbumViewerPage extends HookConsumerWidget {
); );
} }
Widget _buildImageGrid(SharedAlbum albumInfo) { Widget _buildImageGrid(AlbumResponseDto albumInfo) {
if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) { if (albumInfo.assets.isNotEmpty) {
return SliverPadding( return SliverPadding(
padding: const EdgeInsets.only(top: 10.0), padding: const EdgeInsets.only(top: 10.0),
sliver: SliverGrid( sliver: SliverGrid(
@@ -186,9 +194,9 @@ class AlbumViewerPage extends HookConsumerWidget {
), ),
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {
return AlbumViewerThumbnail(asset: albumInfo.assets![index]); return AlbumViewerThumbnail(asset: albumInfo.assets[index]);
}, },
childCount: albumInfo.assets?.length, childCount: albumInfo.assets.length,
), ),
), ),
); );
@@ -196,7 +204,7 @@ class AlbumViewerPage extends HookConsumerWidget {
return const SliverToBoxAdapter(); return const SliverToBoxAdapter();
} }
Widget _buildControlButton(SharedAlbum albumInfo) { Widget _buildControlButton(AlbumResponseDto albumInfo) {
return Padding( return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8), padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8),
child: SizedBox( child: SizedBox(
@@ -207,22 +215,21 @@ class AlbumViewerPage extends HookConsumerWidget {
AlbumActionOutlinedButton( AlbumActionOutlinedButton(
iconData: Icons.add_photo_alternate_outlined, iconData: Icons.add_photo_alternate_outlined,
onPressed: () => _onAddPhotosPressed(albumInfo), onPressed: () => _onAddPhotosPressed(albumInfo),
labelText: "Add photos", labelText: "share_add_photos".tr(),
), ),
userId == albumInfo.ownerId if (userId == albumInfo.ownerId)
? AlbumActionOutlinedButton( AlbumActionOutlinedButton(
iconData: Icons.person_add_alt_rounded, iconData: Icons.person_add_alt_rounded,
onPressed: () => _onAddUsersPressed(albumInfo), onPressed: () => _onAddUsersPressed(albumInfo),
labelText: "Add users", labelText: "album_viewer_page_share_add_users".tr(),
) ),
: Container(),
], ],
), ),
), ),
); );
} }
Widget _buildBody(SharedAlbum albumInfo) { Widget _buildBody(AlbumResponseDto albumInfo) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
titleFocusNode.unfocus(); titleFocusNode.unfocus();
@@ -255,9 +262,16 @@ class AlbumViewerPage extends HookConsumerWidget {
return Scaffold( return Scaffold(
appBar: AlbumViewerAppbar( appBar: AlbumViewerAppbar(
albumInfo: albumInfo, userId: userId, albumId: albumId), albumInfo: albumInfo,
userId: userId,
albumId: albumId,
),
body: albumInfo.when( body: albumInfo.when(
data: (albumInfo) => _buildBody(albumInfo), data: (albumInfo) => albumInfo != null
? _buildBody(albumInfo)
: const Center(
child: CircularProgressIndicator(),
),
error: (e, _) => Center(child: Text("Error loading album info $e")), error: (e, _) => Center(child: Text("Error loading album info $e")),
loading: () => const Center( loading: () => const Center(
child: ImmichLoadingIndicator(), child: ImmichLoadingIndicator(),

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -65,32 +66,31 @@ class AssetSelectionPage extends HookConsumerWidget {
), ),
title: selectedAssets.isEmpty title: selectedAssets.isEmpty
? const Text( ? const Text(
'Add photos', 'share_add_photos',
style: TextStyle(fontSize: 18), style: TextStyle(fontSize: 18),
) ).tr()
: Text( : Text(
_buildAssetCountText(), _buildAssetCountText(),
style: const TextStyle(fontSize: 18), style: const TextStyle(fontSize: 18),
), ),
centerTitle: false, centerTitle: false,
actions: [ actions: [
(!isAlbumExist && selectedAssets.isNotEmpty) || if ((!isAlbumExist && selectedAssets.isNotEmpty) ||
(isAlbumExist && newAssetsForAlbum.isNotEmpty) (isAlbumExist && newAssetsForAlbum.isNotEmpty))
? TextButton( TextButton(
onPressed: () { onPressed: () {
var payload = AssetSelectionPageResult( var payload = AssetSelectionPageResult(
isAlbumExist: isAlbumExist, isAlbumExist: isAlbumExist,
selectedAdditionalAsset: newAssetsForAlbum, selectedAdditionalAsset: newAssetsForAlbum,
selectedNewAsset: selectedAssets, selectedNewAsset: selectedAssets,
); );
AutoRouter.of(context).pop(payload); AutoRouter.of(context).pop(payload);
}, },
child: const Text( child: const Text(
"Add", "share_add",
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ).tr(),
) ),
: Container()
], ],
), ),
body: _buildBody(), body: _buildBody(),

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -55,22 +56,23 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
left: 10, left: 10,
), ),
child: AlbumTitleTextField( child: AlbumTitleTextField(
isAlbumTitleEmpty: isAlbumTitleEmpty, isAlbumTitleEmpty: isAlbumTitleEmpty,
albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode, albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode,
albumTitleController: albumTitleController, albumTitleController: albumTitleController,
isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus), isAlbumTitleTextFieldFocus: isAlbumTitleTextFieldFocus,
),
); );
} }
_buildTitle() { _buildTitle() {
if (selectedAssets.isEmpty) { if (selectedAssets.isEmpty) {
return const SliverToBoxAdapter( return SliverToBoxAdapter(
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 200, left: 18), padding: const EdgeInsets.only(top: 200, left: 18),
child: Text( child: const Text(
'ADD ASSETS', 'create_shared_album_page_share_add_assets',
style: TextStyle(fontSize: 12), style: TextStyle(fontSize: 12),
), ).tr(),
), ),
); );
} }
@@ -85,24 +87,28 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 16, left: 18, right: 18), padding: const EdgeInsets.only(top: 16, left: 18, right: 18),
child: OutlinedButton.icon( child: OutlinedButton.icon(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
padding: padding:
const EdgeInsets.symmetric(vertical: 22, horizontal: 16), const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
side: const BorderSide( side: const BorderSide(
color: Color.fromARGB(255, 206, 206, 206)), color: Color.fromARGB(255, 206, 206, 206),
shape: RoundedRectangleBorder( ),
borderRadius: BorderRadius.circular(5))), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
),
onPressed: _onSelectPhotosButtonPressed, onPressed: _onSelectPhotosButtonPressed,
icon: const Icon(Icons.add_rounded), icon: const Icon(Icons.add_rounded),
label: Padding( label: Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0),
child: Text( child: Text(
'Select Photos', 'create_shared_album_page_share_select_photos',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Colors.grey[700], color: Colors.grey[700],
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold,
), ),
).tr(),
), ),
), ),
), ),
@@ -113,26 +119,22 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
} }
_buildControlButton() { _buildControlButton() {
if (selectedAssets.isNotEmpty) { return Padding(
return Padding( padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16),
padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16), child: SizedBox(
child: SizedBox( height: 30,
height: 30, child: ListView(
child: ListView( scrollDirection: Axis.horizontal,
scrollDirection: Axis.horizontal, children: [
children: [ AlbumActionOutlinedButton(
AlbumActionOutlinedButton( iconData: Icons.add_photo_alternate_outlined,
iconData: Icons.add_photo_alternate_outlined, onPressed: _onSelectPhotosButtonPressed,
onPressed: _onSelectPhotosButtonPressed, labelText: "share_add_photos".tr(),
labelText: "Add photos", ),
), ],
],
),
), ),
); ),
} );
return Container();
} }
_buildSelectedImageGrid() { _buildSelectedImageGrid() {
@@ -150,7 +152,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
return GestureDetector( return GestureDetector(
onTap: _onBackgroundTapped, onTap: _onBackgroundTapped,
child: SharedAlbumThumbnailImage( child: SharedAlbumThumbnailImage(
asset: selectedAssets.toList()[index]), asset: selectedAssets.toList()[index],
),
); );
}, },
childCount: selectedAssets.length, childCount: selectedAssets.length,
@@ -163,57 +166,60 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
} }
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
elevation: 0, elevation: 0,
centerTitle: false, centerTitle: false,
leading: IconButton( leading: IconButton(
onPressed: () { onPressed: () {
ref.watch(assetSelectionProvider.notifier).removeAll(); ref.watch(assetSelectionProvider.notifier).removeAll();
AutoRouter.of(context).pop(); AutoRouter.of(context).pop();
}, },
icon: const Icon(Icons.close_rounded)), icon: const Icon(Icons.close_rounded),
title: const Text( ),
'Create album', title: const Text(
style: TextStyle(color: Colors.black), 'share_create_album',
style: TextStyle(color: Colors.black),
).tr(),
actions: [
TextButton(
onPressed: albumTitleController.text.isNotEmpty
? _showSelectUserPage
: null,
child: Text(
'create_shared_album_page_share'.tr(),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
), ),
actions: [ ],
TextButton( ),
onPressed: albumTitleController.text.isNotEmpty body: GestureDetector(
? _showSelectUserPage onTap: _onBackgroundTapped,
: null, child: CustomScrollView(
child: const Text( slivers: [
'Share', SliverAppBar(
style: TextStyle( elevation: 5,
fontWeight: FontWeight.bold, automaticallyImplyLeading: false,
// leading: Container(),
pinned: true,
floating: false,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(66.0),
child: Column(
children: [
_buildTitleInputField(),
if (selectedAssets.isNotEmpty) _buildControlButton(),
],
), ),
), ),
), ),
_buildTitle(),
_buildSelectPhotosButton(),
_buildSelectedImageGrid(),
], ],
), ),
body: GestureDetector( ),
onTap: _onBackgroundTapped, );
child: CustomScrollView(
slivers: [
SliverAppBar(
elevation: 5,
leading: Container(),
pinned: true,
floating: false,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(66.0),
child: Column(
children: [
_buildTitleInputField(),
_buildControlButton(),
],
),
),
),
_buildTitle(),
_buildSelectPhotosButton(),
_buildSelectedImageGrid(),
],
),
));
} }
} }

View File

@@ -1,30 +1,30 @@
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/sharing/models/shared_album.model.dart';
import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart'; import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/shared/models/user.model.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:openapi/api.dart';
class SelectAdditionalUserForSharingPage extends HookConsumerWidget { class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
final SharedAlbum albumInfo; final AlbumResponseDto albumInfo;
const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo}) const SelectAdditionalUserForSharingPage({Key? key, required this.albumInfo})
: super(key: key); : super(key: key);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<List<User>> suggestedShareUsers = AsyncValue<List<UserResponseDto>> suggestedShareUsers =
ref.watch(suggestedSharedUsersProvider); ref.watch(suggestedSharedUsersProvider);
final sharedUsersList = useState<Set<User>>({}); final sharedUsersList = useState<Set<UserResponseDto>>({});
_addNewUsersHandler() { _addNewUsersHandler() {
AutoRouter.of(context) AutoRouter.of(context)
.pop(sharedUsersList.value.map((e) => e.id).toList()); .pop(sharedUsersList.value.map((e) => e.id).toList());
} }
_buildTileIcon(User user) { _buildTileIcon(UserResponseDto user) {
if (sharedUsersList.value.contains(user)) { if (sharedUsersList.value.contains(user)) {
return CircleAvatar( return CircleAvatar(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
@@ -42,7 +42,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
} }
} }
_buildUserList(List<User> users) { _buildUserList(List<UserResponseDto> users) {
List<Widget> usersChip = []; List<Widget> usersChip = [];
for (var user in sharedUsersList.value) { for (var user in sharedUsersList.value) {
@@ -54,9 +54,10 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
label: Text( label: Text(
user.email, user.email,
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.black87, color: Colors.black87,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold,
),
), ),
), ),
), ),
@@ -68,14 +69,15 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
Wrap( Wrap(
children: [...usersChip], children: [...usersChip],
), ),
const Padding( Padding(
padding: EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Text( child: Text(
'Suggestions', 'select_additional_user_for_sharing_page_suggestions'.tr(),
style: TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.grey, color: Colors.grey,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold,
),
), ),
), ),
ListView.builder( ListView.builder(
@@ -86,13 +88,16 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
title: Text( title: Text(
users[index].email, users[index].email,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold), fontSize: 14,
fontWeight: FontWeight.bold,
),
), ),
onTap: () { onTap: () {
if (sharedUsersList.value.contains(users[index])) { if (sharedUsersList.value.contains(users[index])) {
sharedUsersList.value = sharedUsersList.value sharedUsersList.value = sharedUsersList.value
.where((selectedUser) => .where(
selectedUser.id != users[index].id) (selectedUser) => selectedUser.id != users[index].id,
)
.toSet(); .toSet();
} else { } else {
sharedUsersList.value = { sharedUsersList.value = {
@@ -112,9 +117,9 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text( title: const Text(
'Invite to album', 'share_invite',
style: TextStyle(color: Colors.black), style: TextStyle(color: Colors.black),
), ).tr(),
elevation: 0, elevation: 0,
centerTitle: false, centerTitle: false,
leading: IconButton( leading: IconButton(
@@ -128,9 +133,9 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
onPressed: onPressed:
sharedUsersList.value.isEmpty ? null : _addNewUsersHandler, sharedUsersList.value.isEmpty ? null : _addNewUsersHandler,
child: const Text( child: const Text(
"Add", "share_add",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
), ).tr(),
) )
], ],
), ),
@@ -138,7 +143,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
data: (users) { data: (users) {
for (var sharedUsers in albumInfo.sharedUsers) { for (var sharedUsers in albumInfo.sharedUsers) {
users.removeWhere( users.removeWhere(
(u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId); (u) => u.id == sharedUsers.id || u.id == albumInfo.ownerId,
);
} }
return _buildUserList(users); return _buildUserList(users);

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -8,15 +9,16 @@ import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.da
import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart'; import 'package:immich_mobile/modules/sharing/providers/suggested_shared_users.provider.dart';
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart'; import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/user.model.dart';
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
import 'package:openapi/api.dart';
class SelectUserForSharingPage extends HookConsumerWidget { class SelectUserForSharingPage extends HookConsumerWidget {
const SelectUserForSharingPage({Key? key}) : super(key: key); const SelectUserForSharingPage({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final sharedUsersList = useState<Set<User>>({}); final sharedUsersList = useState<Set<UserResponseDto>>({});
AsyncValue<List<User>> suggestedShareUsers = AsyncValue<List<UserResponseDto>> suggestedShareUsers =
ref.watch(suggestedSharedUsersProvider); ref.watch(suggestedSharedUsersProvider);
_createSharedAlbum() async { _createSharedAlbum() async {
@@ -36,11 +38,14 @@ class SelectUserForSharingPage extends HookConsumerWidget {
.navigate(const TabControllerRoute(children: [SharingRoute()])); .navigate(const TabControllerRoute(children: [SharingRoute()]));
} }
const ScaffoldMessenger( ScaffoldMessenger(
child: SnackBar(content: Text('Failed to create album'))); child: SnackBar(
content: const Text('select_user_for_sharing_page_err_album').tr(),
),
);
} }
_buildTileIcon(User user) { _buildTileIcon(UserResponseDto user) {
if (sharedUsersList.value.contains(user)) { if (sharedUsersList.value.contains(user)) {
return CircleAvatar( return CircleAvatar(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
@@ -58,7 +63,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
} }
} }
_buildUserList(List<User> users) { _buildUserList(List<UserResponseDto> users) {
List<Widget> usersChip = []; List<Widget> usersChip = [];
for (var user in sharedUsersList.value) { for (var user in sharedUsersList.value) {
@@ -70,9 +75,10 @@ class SelectUserForSharingPage extends HookConsumerWidget {
label: Text( label: Text(
user.email, user.email,
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.black87, color: Colors.black87,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold,
),
), ),
), ),
), ),
@@ -84,15 +90,16 @@ class SelectUserForSharingPage extends HookConsumerWidget {
Wrap( Wrap(
children: [...usersChip], children: [...usersChip],
), ),
const Padding( Padding(
padding: EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Text( child: const Text(
'Suggestions', 'select_user_for_sharing_page_share_suggestions',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Colors.grey, color: Colors.grey,
fontWeight: FontWeight.bold), fontWeight: FontWeight.bold,
), ),
).tr(),
), ),
ListView.builder( ListView.builder(
shrinkWrap: true, shrinkWrap: true,
@@ -102,13 +109,16 @@ class SelectUserForSharingPage extends HookConsumerWidget {
title: Text( title: Text(
users[index].email, users[index].email,
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.bold), fontSize: 14,
fontWeight: FontWeight.bold,
),
), ),
onTap: () { onTap: () {
if (sharedUsersList.value.contains(users[index])) { if (sharedUsersList.value.contains(users[index])) {
sharedUsersList.value = sharedUsersList.value sharedUsersList.value = sharedUsersList.value
.where((selectedUser) => .where(
selectedUser.id != users[index].id) (selectedUser) => selectedUser.id != users[index].id,
)
.toSet(); .toSet();
} else { } else {
sharedUsersList.value = { sharedUsersList.value = {
@@ -128,9 +138,9 @@ class SelectUserForSharingPage extends HookConsumerWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text( title: const Text(
'Invite to album', 'share_invite',
style: TextStyle(color: Colors.black), style: TextStyle(color: Colors.black),
), ).tr(),
elevation: 0, elevation: 0,
centerTitle: false, centerTitle: false,
leading: IconButton( leading: IconButton(
@@ -141,12 +151,13 @@ class SelectUserForSharingPage extends HookConsumerWidget {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: onPressed:
sharedUsersList.value.isEmpty ? null : _createSharedAlbum, sharedUsersList.value.isEmpty ? null : _createSharedAlbum,
child: const Text( child: const Text(
"Create Album", "share_create_album",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
)) ).tr(),
)
], ],
), ),
body: suggestedShareUsers.when( body: suggestedShareUsers.when(

View File

@@ -1,13 +1,14 @@
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/sharing/models/shared_album.model.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/modules/sharing/ui/sharing_sliver_appbar.dart'; import 'package:immich_mobile/modules/sharing/ui/sharing_sliver_appbar.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:openapi/api.dart';
import 'package:transparent_image/transparent_image.dart'; import 'package:transparent_image/transparent_image.dart';
class SharingPage extends HookConsumerWidget { class SharingPage extends HookConsumerWidget {
@@ -17,13 +18,16 @@ class SharingPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
var box = Hive.box(userInfoBox); var box = Hive.box(userInfoBox);
var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail'; var thumbnailRequestUrl = '${box.get(serverEndpointKey)}/asset/thumbnail';
final List<SharedAlbum> sharedAlbums = ref.watch(sharedAlbumProvider); final List<AlbumResponseDto> sharedAlbums = ref.watch(sharedAlbumProvider);
useEffect(() { useEffect(
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); () {
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
return null; return null;
}, []); },
[],
);
_buildAlbumList() { _buildAlbumList() {
return SliverList( return SliverList(
@@ -59,9 +63,10 @@ class SharingPage extends HookConsumerWidget {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.grey.shade800), color: Colors.grey.shade800,
),
), ),
onTap: () { onTap: () {
AutoRouter.of(context) AutoRouter.of(context)
@@ -104,20 +109,20 @@ class SharingPage extends HookConsumerWidget {
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
'EMPTY LIST', 'sharing_page_empty_list',
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(),
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
'Create shared albums to share photos and videos with people in your network.', 'sharing_page_description',
style: TextStyle(fontSize: 12, color: Colors.grey[700]), style: TextStyle(fontSize: 12, color: Colors.grey[700]),
), ).tr(),
), ),
], ],
), ),
@@ -131,15 +136,15 @@ class SharingPage extends HookConsumerWidget {
body: CustomScrollView( body: CustomScrollView(
slivers: [ slivers: [
const SharingSliverAppBar(), const SharingSliverAppBar(),
const SliverPadding( SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
sliver: SliverToBoxAdapter( sliver: SliverToBoxAdapter(
child: Text( child: const Text(
"Shared albums", "sharing_page_album",
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ).tr(),
), ),
), ),
sharedAlbums.isNotEmpty sharedAlbums.isNotEmpty

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