Compare commits
44 Commits
v1.7.0_11-
...
v1.10.0_15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b34de624ce | ||
|
|
7886c42742 | ||
|
|
d476b15312 | ||
|
|
bdf38e7668 | ||
|
|
e33566a04a | ||
|
|
c28251b8b4 | ||
|
|
337db1c508 | ||
|
|
ad2a1ba901 | ||
|
|
fa6f6f8e9f | ||
|
|
a44043a4e5 | ||
|
|
87b15c60c0 | ||
|
|
2c83e52c15 | ||
|
|
55c5027539 | ||
|
|
ce06af0c9b | ||
|
|
baaf7ad153 | ||
|
|
4a25e7dc22 | ||
|
|
f90563d18c | ||
|
|
8352ecd3b9 | ||
|
|
69b34a4364 | ||
|
|
6023c3c624 | ||
|
|
171e7ffa77 | ||
|
|
d9f918005a | ||
|
|
e8ade4866b | ||
|
|
bbfa789a4e | ||
|
|
a779c3803c | ||
|
|
79dea504b0 | ||
|
|
4900fecd10 | ||
|
|
adfaab7eb2 | ||
|
|
c5adbea6e1 | ||
|
|
bb89fa4aab | ||
|
|
43d639104d | ||
|
|
a1792a7d94 | ||
|
|
373b6918f8 | ||
|
|
f1396761b0 | ||
|
|
335bb0707c | ||
|
|
7a51e0dd4d | ||
|
|
5b42899dde | ||
|
|
229357df2b | ||
|
|
8d5626620b | ||
|
|
2c4243b3d0 | ||
|
|
38e0178c81 | ||
|
|
c5c7a134dd | ||
|
|
da9eb61532 | ||
|
|
c1ccf026f0 |
1
.github/FUNDING.yml
vendored
@@ -1,3 +1,4 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
github: alextran1502
|
github: alextran1502
|
||||||
|
custom: https://www.buymeacoffee.com/altran1502?new=1
|
||||||
15
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,15 +1,26 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: "[BUG]"
|
title: '[BUG] <title>'
|
||||||
labels: bug
|
labels: bug, need triage
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Note: Please search to see if an issue already exists for the bug you encountered.
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**Task List**
|
||||||
|
[ ] I have read thoroughly the README setup and installation instructions.
|
||||||
|
[ ] If my setup is different, I have included my docker-compose file.
|
||||||
|
[ ] I have included my redacted `.env` file.
|
||||||
|
[ ] I have included information on my machine, and environment.
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
|
|||||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
32
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Request a feature that you would like for the app
|
||||||
|
title: "[Feature]: "
|
||||||
|
labels: ["feature", "need triage"]
|
||||||
|
assignees:
|
||||||
|
- ""
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this feature request!
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: feature-detail
|
||||||
|
attributes:
|
||||||
|
label: Feature detail
|
||||||
|
placeholder: Describe the feature you would like to see for the app
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform
|
||||||
|
description: Choose the platform for the feature
|
||||||
|
options:
|
||||||
|
- Web
|
||||||
|
- Mobile App
|
||||||
|
- Server
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
94
.github/workflows/build_push_docker_latest.yml
vendored
@@ -8,26 +8,27 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build_and_push_server_latest:
|
build_and_push_server_latest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: "main" # branch
|
# ref: "main" # branch
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1.2.0
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v1.6.0
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Build and push Immich
|
- name: Build and push Immich
|
||||||
uses: docker/build-push-action@v2.10.0
|
uses: docker/build-push-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
context: ./server
|
context: ./server
|
||||||
file: ./server/Dockerfile
|
file: ./server/Dockerfile
|
||||||
@@ -37,28 +38,59 @@ jobs:
|
|||||||
altran1502/immich-server:latest
|
altran1502/immich-server:latest
|
||||||
|
|
||||||
build_and_push_microservice_latest:
|
build_and_push_microservice_latest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: "main" # branch
|
# ref: "main" # branch
|
||||||
- name: Set up QEMU
|
fetch-depth: 0
|
||||||
uses: docker/setup-qemu-action@v1.2.0
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up QEMU
|
||||||
id: buildx
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
uses: docker/setup-buildx-action@v1.6.0
|
- name: Set up Docker Buildx
|
||||||
- name: Login to Docker Hub
|
id: buildx
|
||||||
uses: docker/login-action@v1
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
with:
|
- name: Login to Docker Hub
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
uses: docker/login-action@v2
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
with:
|
||||||
- name: Build and Push Microservices
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
uses: docker/build-push-action@v2.10.0
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
with:
|
- name: Build and Push Microservices
|
||||||
context: ./microservices
|
uses: docker/build-push-action@v3.0.0
|
||||||
file: ./microservices/Dockerfile
|
with:
|
||||||
platforms: linux/arm/v7,linux/amd64
|
context: ./microservices
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
file: ./microservices/Dockerfile
|
||||||
tags: |
|
platforms: linux/arm/v7,linux/amd64
|
||||||
altran1502/immich-microservices:latest
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: |
|
||||||
|
altran1502/immich-microservices:latest
|
||||||
|
|
||||||
|
build_and_push_web_latest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
# ref: "main" # branch
|
||||||
|
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 Web
|
||||||
|
uses: docker/build-push-action@v3.0.0
|
||||||
|
with:
|
||||||
|
context: ./web
|
||||||
|
file: ./web/Dockerfile
|
||||||
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
|
target: prod
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: |
|
||||||
|
altran1502/immich-web:latest
|
||||||
|
|||||||
71
.github/workflows/build_push_server_release.yml
vendored
@@ -12,30 +12,30 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: "main"
|
ref: "main"
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: 'Get Previous tag'
|
- name: "Get Previous tag"
|
||||||
id: previoustag
|
id: previoustag
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||||
with:
|
with:
|
||||||
fallback: latest
|
fallback: latest
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1.2.0
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v1.6.0
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push immich-server release
|
- name: Build and push immich-server release
|
||||||
uses: docker/build-push-action@v2.10.0
|
uses: docker/build-push-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
context: ./server
|
context: ./server
|
||||||
file: ./server/Dockerfile
|
file: ./server/Dockerfile
|
||||||
@@ -50,34 +50,73 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: "main"
|
ref: "main"
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: 'Get Previous tag'
|
- name: "Get Previous tag"
|
||||||
id: previoustag
|
id: previoustag
|
||||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||||
with:
|
with:
|
||||||
fallback: latest
|
fallback: latest
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1.2.0
|
uses: docker/setup-qemu-action@v2.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v1.6.0
|
uses: docker/setup-buildx-action@v2.0.0
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push immich-microservices release
|
- name: Build and push immich-microservices release
|
||||||
uses: docker/build-push-action@v2.10.0
|
uses: docker/build-push-action@v3.0.0
|
||||||
with:
|
with:
|
||||||
context: ./microservices
|
context: ./microservices
|
||||||
file: ./microservices/Dockerfile
|
file: ./microservices/Dockerfile
|
||||||
platforms: linux/arm/v7,linux/amd64
|
platforms: linux/arm/v7,linux/amd64
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-microservices:${{ steps.previoustag.outputs.tag }}
|
altran1502/immich-microservices:${{ steps.previoustag.outputs.tag }}
|
||||||
|
|
||||||
|
build_and_push_web_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-web release
|
||||||
|
uses: docker/build-push-action@v3.0.0
|
||||||
|
with:
|
||||||
|
context: ./web
|
||||||
|
file: ./web/Dockerfile
|
||||||
|
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
target: prod
|
||||||
|
tags: |
|
||||||
|
altran1502/immich-web:${{ steps.previoustag.outputs.tag }}
|
||||||
|
|||||||
8
Makefile
@@ -5,7 +5,13 @@ dev-update:
|
|||||||
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
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
|
docker-compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich_server=3 --remove-orphans
|
||||||
|
|
||||||
|
stage:
|
||||||
|
docker-compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
|
test-e2e:
|
||||||
|
docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich_server_test
|
||||||
|
|
||||||
prod:
|
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
|
||||||
|
|||||||
9
NOTES.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
Server scenario with web
|
||||||
|
|
||||||
|
[ ] 1 user exist without admin right -> make admin on first check
|
||||||
|
|
||||||
|
[ ] 2 users exist without admin right -> ask user to choose which account will be the admin
|
||||||
|
|
||||||
|
[ X ] No users exist -> prompt signup form for Admin
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
[ ] Up version in [docker/docker-compose.gpu.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
|
[ ] Up version in [docker/docker-compose.gpu.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
|
||||||
|
|
||||||
[ ] Up version in [docker/docker-compose.dev.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
|
[ ] Up version in [docker/docker-compose.dev.yml](/docker/docker-compose.dev.yml) for `immich_server` service
|
||||||
|
|
||||||
[ ] Up version in [server/src/constants/server_version.constant.ts](/server/src/constants/server_version.constant.ts)
|
[ ] Up version in [server/src/constants/server_version.constant.ts](/server/src/constants/server_version.constant.ts)
|
||||||
|
|
||||||
|
|||||||
158
README.md
@@ -31,29 +31,35 @@ Loading ~4000 images/videos
|
|||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
### Mobile client
|
||||||
<p align="left">
|
<p align="left">
|
||||||
<img src="design/nsc1.png" width="150" title="Login With Custom URL">
|
<img src="design/login-screen.png" width="150" title="Login With Custom URL">
|
||||||
<img src="design/nsc2.png" width="150" title="Backup Setting Info">
|
<img src="design/backup-screen.png" width="150" title="Backup Setting Info">
|
||||||
|
<img src="design/selective-backup-screen.png" width="150" title="Backup Setting Info">
|
||||||
<img src="design/home-screen.jpeg" width="150" title="Home Screen">
|
<img src="design/home-screen.jpeg" width="150" title="Home Screen">
|
||||||
<img src="design/search-screen.jpeg" width="150" title="Curated Search Info">
|
<img src="design/search-screen.jpeg" width="150" title="Curated Search Info">
|
||||||
<img src="design/shared-albums.png" width="150" title="Shared Albums">
|
<img src="design/shared-albums.png" width="150" title="Shared Albums">
|
||||||
<img src="design/nsc6.png" width="150" title="EXIF Info">
|
<img src="design/nsc6.png" width="150" title="EXIF Info">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### Web client
|
||||||
|
<p align="center">
|
||||||
|
<img src="design/dashboard_photos.jpeg" width="100%" title="Home Dashboard">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# Note
|
# Note
|
||||||
|
|
||||||
**!! NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS !!**
|
**!! NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS !!**
|
||||||
|
|
||||||
This project is under heavy development, there will be continous functions, features and api changes.
|
This project is under heavy development, there will be continuous functions, features and api changes.
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
- Upload and view assets (videos/images).
|
- Upload and view assets (videos/images).
|
||||||
|
- Auto Backup.
|
||||||
- Download asset to local device.
|
- Download asset to local device.
|
||||||
- Multi-user supported.
|
- Multi-user supported.
|
||||||
- Quick navigation with drag scroll bar.
|
- Quick navigation with drag scroll bar.
|
||||||
- Auto Backup.
|
|
||||||
- Support HEIC/HEIF Backup.
|
- Support HEIC/HEIF Backup.
|
||||||
- Extract and display EXIF info.
|
- Extract and display EXIF info.
|
||||||
- Real-time render from multi-device upload event.
|
- Real-time render from multi-device upload event.
|
||||||
@@ -65,18 +71,33 @@ This project is under heavy development, there will be continous functions, feat
|
|||||||
- Show curated places on the search page
|
- Show curated places on the search page
|
||||||
- Show curated objects on the search page
|
- Show curated objects on the search page
|
||||||
- Shared album with users on the same server
|
- Shared album with users on the same server
|
||||||
|
- Selective backup - albums can be included and excluded during the backup process.
|
||||||
|
- Web interface is available for administrative tasks (creating new users) and viewing assets on the server - additional features are coming.
|
||||||
|
|
||||||
# Development
|
# System Requirement
|
||||||
|
|
||||||
You can use docker compose for development, there are several services that compose Immich
|
**OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS...etc).
|
||||||
|
|
||||||
1. NestJs
|
I haven't tested with `Docker for Windows` as well as `WSL` on Windows
|
||||||
2. PostgreSQL
|
|
||||||
3. Redis
|
|
||||||
4. Nginx
|
|
||||||
5. TensorFlow
|
|
||||||
|
|
||||||
## Populate .env file
|
*Raspberry Pi can be used but `microservices` container has to be comment out in `docker-compose` since TensorFlow has not been supported in Docker image on arm64v7 yet.*
|
||||||
|
|
||||||
|
**RAM**: At least 2GB, preffered 4GB.
|
||||||
|
|
||||||
|
**Core**: At least 2 cores, preffered 4 cores.
|
||||||
|
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
You can use docker compose for development and testing out the application, there are several services that compose Immich:
|
||||||
|
|
||||||
|
1. **NestJs** - Backend of the application
|
||||||
|
2. **SvelteKit** - Web frontend of the application
|
||||||
|
3. **PostgreSQL** - Main database of the application
|
||||||
|
4. **Redis** - For sharing websocket instance between docker instances and background tasks message queue.
|
||||||
|
5. **Nginx** - Load balancing and optimized file uploading.
|
||||||
|
6. **TensorFlow** - Object Detection and Image Classification.
|
||||||
|
|
||||||
|
## Step 1: Populate .env file
|
||||||
|
|
||||||
Navigate to `docker` directory and run
|
Navigate to `docker` directory and run
|
||||||
|
|
||||||
@@ -90,31 +111,85 @@ Notice that if set `ENABLE_MAPBOX` to `true`, you will have to provide `MAPBOX_K
|
|||||||
|
|
||||||
Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is owned by the user that run the `docker-compose` command below.
|
Pay attention to the key `UPLOAD_LOCATION`, this directory must exist and is owned by the user that run the `docker-compose` command below.
|
||||||
|
|
||||||
To start, run
|
**Example**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose -f ./docker/docker-compose.yml up --build -V
|
###################################################################################
|
||||||
|
# Database
|
||||||
|
###################################################################################
|
||||||
|
DB_USERNAME=postgres
|
||||||
|
DB_PASSWORD=postgres
|
||||||
|
DB_DATABASE_NAME=immich
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
|
# Upload File Config
|
||||||
|
###################################################################################
|
||||||
|
UPLOAD_LOCATION=<put-the-path-of-the-upload-folder-here>
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
|
# JWT SECRET
|
||||||
|
###################################################################################
|
||||||
|
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
|
# MAPBOX
|
||||||
|
####################################################################################
|
||||||
|
# ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
||||||
|
ENABLE_MAPBOX=false
|
||||||
|
MAPBOX_KEY=
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
|
# WEB
|
||||||
|
###################################################################################
|
||||||
|
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
||||||
|
# know where can it make the request to.
|
||||||
|
# For example: If your server IP address is 10.1.11.50, the environment variable will
|
||||||
|
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283
|
||||||
|
VITE_SERVER_ENDPOINT=http://192.168.1.216:2283
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 2: Start the server
|
||||||
|
|
||||||
|
To **start**, run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f ./docker/docker-compose.yml pull && docker-compose -f ./docker/docker-compose.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
The server will be running at `http://your-ip:2283` through `Nginx`
|
The server will be running at `http://your-ip:2283` through `Nginx`
|
||||||
|
|
||||||
## Register User
|
## Step 3: Register User
|
||||||
|
|
||||||
Use the command below on your terminal to create user as we don't have user interface for this function yet.
|
Access the web interface at `http://your-ip:2285` to register an admin account.
|
||||||
|
|
||||||
```bash
|
<p align="left">
|
||||||
curl --location --request POST 'http://your-server-ip:2283/auth/signUp' \
|
<img src="design/admin-registration-form.png" width="300" title="Admin Registration">
|
||||||
--header 'Content-Type: application/json' \
|
<p/>
|
||||||
--data-raw '{
|
|
||||||
"email": "testuser@email.com",
|
|
||||||
"password": "password"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run mobile app
|
Additional accounts on the server can be created by the admin account.
|
||||||
|
|
||||||
|
<p align="left">
|
||||||
|
<img src="design/admin-interface.png" width="500" title="Admin User Management">
|
||||||
|
<p/>
|
||||||
|
|
||||||
|
## Step 4: Run mobile app
|
||||||
|
|
||||||
|
The app is distributed on several platforms below.
|
||||||
|
|
||||||
## F-Droid
|
## F-Droid
|
||||||
You can get the app on F-droid by cliking the image below.
|
You can get the app on F-droid by clicking the image below.
|
||||||
|
|
||||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||||
alt="Get it on F-Droid"
|
alt="Get it on F-Droid"
|
||||||
@@ -123,11 +198,7 @@ You can get the app on F-droid by cliking the image below.
|
|||||||
|
|
||||||
## Android
|
## Android
|
||||||
|
|
||||||
#### Download latest `apk` in release tab and run on your phone. You can follow this guide on how to do that
|
#### Get the app on Google Play Store [here](https://play.google.com/store/apps/details?id=app.alextran.immich)
|
||||||
|
|
||||||
- [Run APK on Android](https://www.lifewire.com/install-apk-on-android-4177185)
|
|
||||||
|
|
||||||
#### You can also download the app from Google Play Store [here](https://play.google.com/store/apps/details?id=app.alextran.immich)
|
|
||||||
|
|
||||||
*The App version might be lagging behind the latest release due to the review process.*
|
*The App version might be lagging behind the latest release due to the review process.*
|
||||||
|
|
||||||
@@ -137,7 +208,7 @@ You can get the app on F-droid by cliking the image below.
|
|||||||
|
|
||||||
## iOS
|
## iOS
|
||||||
|
|
||||||
#### You can download the app from Apple AppStore [here](https://apps.apple.com/us/app/immich/id1613945652):
|
#### Get the app on Apple AppStore [here](https://apps.apple.com/us/app/immich/id1613945652):
|
||||||
|
|
||||||
*The App version might be lagging behind the latest release due to the review process.*
|
*The App version might be lagging behind the latest release due to the review process.*
|
||||||
|
|
||||||
@@ -146,13 +217,26 @@ You can get the app on F-droid by cliking the image below.
|
|||||||
<img src="design/ios-qr-code.png" width="200" title="Apple App Store">
|
<img src="design/ios-qr-code.png" width="200" title="Apple App Store">
|
||||||
<p/>
|
<p/>
|
||||||
|
|
||||||
|
|
||||||
|
# Development
|
||||||
|
|
||||||
|
The development environment can be started from the root of the project after populating the `.env` file with the command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make dev # required Makefile installed on the system.
|
||||||
|
```
|
||||||
|
|
||||||
|
All servers and web container are hot reload for quick feedback loop.
|
||||||
|
|
||||||
# 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 Sponsore**](https://github.com/sponsors/alextran1502).
|
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.
|
||||||
|
|
||||||
This is also a meaningful way to give me motivation and encounragment to continue working on the app.
|
[](https://www.buymeacoffee.com/altran1502)
|
||||||
|
|
||||||
Cheer! 🎉
|
This is also a meaningful way to give me motivation and encouragement to continue working on the app.
|
||||||
|
|
||||||
|
Cheers! 🎉
|
||||||
|
|
||||||
# Known Issue
|
# Known Issue
|
||||||
|
|
||||||
@@ -160,13 +244,13 @@ Cheer! 🎉
|
|||||||
|
|
||||||
*This is a known issue on RaspberryPi 4 arm64-v7 and incorrect Promox setup*
|
*This is a known issue on RaspberryPi 4 arm64-v7 and incorrect Promox setup*
|
||||||
|
|
||||||
TensorFlow doesn't run with older CPU architecture, it requires CPU with AVX and AVX2 instruction set. If you encounter the error `illegal instruction core dump` when running the docker-compose command above, check for your CPU flags with the command and make sure you see `AVX` and `AVX2`:
|
TensorFlow doesn't run with older CPU architecture, it requires a CPU with AVX and AVX2 instruction set. If you encounter the error `illegal instruction core dump` when running the docker-compose command above, check for your CPU flags with the command and make sure you see `AVX` and `AVX2`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
more /proc/cpuinfo | grep flags
|
more /proc/cpuinfo | grep flags
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are running virtualization in Promox, the VM doesn't have the flag enable.
|
If you are running virtualization in Promox, the VM doesn't have the flag enabled.
|
||||||
|
|
||||||
You need to change the CPU type from `kvm64` to `host` under VMs hardware tab.
|
You need to change the CPU type from `kvm64` to `host` under VMs hardware tab.
|
||||||
|
|
||||||
|
|||||||
BIN
design/admin-interface.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
design/admin-registration-form.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
design/backup-screen.png
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
design/dashboard_photos.jpeg
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
design/login-screen.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
design/nsc1.png
|
Before Width: | Height: | Size: 176 KiB |
BIN
design/nsc2.png
|
Before Width: | Height: | Size: 303 KiB |
BIN
design/selective-backup-screen.png
Normal file
|
After Width: | Height: | Size: 570 KiB |
@@ -1,15 +1,63 @@
|
|||||||
|
###################################################################################
|
||||||
# Database
|
# Database
|
||||||
|
###################################################################################
|
||||||
|
|
||||||
|
DB_HOSTNAME=immich_postgres
|
||||||
DB_USERNAME=postgres
|
DB_USERNAME=postgres
|
||||||
DB_PASSWORD=postgres
|
DB_PASSWORD=postgres
|
||||||
DB_DATABASE_NAME=
|
DB_DATABASE_NAME=immich
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
|
# Redis
|
||||||
|
###################################################################################
|
||||||
|
|
||||||
|
REDIS_HOSTNAME=immich_redis
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
# Upload File Config
|
# Upload File Config
|
||||||
|
###################################################################################
|
||||||
|
|
||||||
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
||||||
|
|
||||||
# JWT SECRET
|
|
||||||
JWT_SECRET=
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
|
# JWT SECRET
|
||||||
|
###################################################################################
|
||||||
|
|
||||||
|
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
# MAPBOX
|
# MAPBOX
|
||||||
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
####################################################################################
|
||||||
ENABLE_MAPBOX=
|
|
||||||
MAPBOX_KEY=
|
# ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
||||||
|
ENABLE_MAPBOX=false
|
||||||
|
MAPBOX_KEY=
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
###################################################################################
|
||||||
|
# WEB
|
||||||
|
###################################################################################
|
||||||
|
|
||||||
|
# This is the URL of your vm/server where you host Immich, so that the web frontend
|
||||||
|
# know where can it make the request to.
|
||||||
|
# For example: If your server IP address is 10.1.11.50, the environment variable will
|
||||||
|
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283
|
||||||
|
# !CAUTION! THERE IS NO FORWARD SLASH AT THE END
|
||||||
|
|
||||||
|
VITE_SERVER_ENDPOINT=
|
||||||
|
|||||||
16
docker/.env.test
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
DB_HOSTNAME=immich_postgres_test
|
||||||
|
# Database
|
||||||
|
DB_USERNAME=postgres
|
||||||
|
DB_PASSWORD=postgres
|
||||||
|
DB_DATABASE_NAME=e2e_test
|
||||||
|
|
||||||
|
# Upload File Config
|
||||||
|
UPLOAD_LOCATION=./upload
|
||||||
|
|
||||||
|
# JWT SECRET
|
||||||
|
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
||||||
|
|
||||||
|
# MAPBOX
|
||||||
|
## ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
||||||
|
ENABLE_MAPBOX=false
|
||||||
|
MAPBOX_KEY=
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich-server:
|
||||||
image: immich-server-dev:1.7.0
|
image: immich-server-dev:1.9.0
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -21,10 +21,10 @@ services:
|
|||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
immich_microservices:
|
immich-microservices:
|
||||||
image: immich-microservices-dev:1.7.0
|
image: immich-microservices-dev:1.9.0
|
||||||
build:
|
build:
|
||||||
context: ../microservices
|
context: ../microservices
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -42,14 +42,32 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
|
immich-web:
|
||||||
|
image: immich-web-dev:1.9.0
|
||||||
|
build:
|
||||||
|
context: ../web
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: dev
|
||||||
|
command: npm run dev --host
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- 3002:3002
|
||||||
|
- 24678:24678
|
||||||
|
volumes:
|
||||||
|
- ../web:/usr/src/app
|
||||||
|
- /usr/src/app/node_modules
|
||||||
|
networks:
|
||||||
|
- immich-network
|
||||||
|
restart: always
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
@@ -66,7 +84,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
container_name: proxy_nginx
|
container_name: proxy_nginx
|
||||||
@@ -79,11 +97,11 @@ services:
|
|||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich_server
|
- immich-server
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
immich_network:
|
immich-network:
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich-server:
|
||||||
image: immich-server-dev:1.7.0
|
image: immich-server-dev:1.9.0
|
||||||
build:
|
build:
|
||||||
context: ../server
|
context: ../server
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -19,10 +19,10 @@ services:
|
|||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
immich_microservices:
|
immich-microservices:
|
||||||
image: immich-microservices-dev:1.7.0
|
image: immich-microservices-dev:1.9.0
|
||||||
build:
|
build:
|
||||||
context: ../microservices
|
context: ../microservices
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -46,13 +46,13 @@ services:
|
|||||||
- database
|
- database
|
||||||
- immich_server
|
- immich_server
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
@@ -69,7 +69,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
container_name: proxy_nginx
|
container_name: proxy_nginx
|
||||||
@@ -82,11 +82,11 @@ services:
|
|||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich_server
|
- immich-server
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
immich_network:
|
immich-network:
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|||||||
105
docker/docker-compose.staging.yml
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
immich-server:
|
||||||
|
image: immich-server-staging:latest
|
||||||
|
build:
|
||||||
|
context: ../server
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
|
expose:
|
||||||
|
- "3000"
|
||||||
|
volumes:
|
||||||
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- database
|
||||||
|
networks:
|
||||||
|
- immich-network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
immich-microservices:
|
||||||
|
image: immich-microservices-staging:latest
|
||||||
|
build:
|
||||||
|
context: ../microservices
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
|
expose:
|
||||||
|
- "3001"
|
||||||
|
volumes:
|
||||||
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
networks:
|
||||||
|
- immich-network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
immich-web:
|
||||||
|
image: immich-web-staging:latest
|
||||||
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
|
build:
|
||||||
|
context: ../web
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: prod
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- 2285:3000
|
||||||
|
networks:
|
||||||
|
- immich-network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
|
||||||
|
redis:
|
||||||
|
container_name: immich_redis
|
||||||
|
image: redis:6.2
|
||||||
|
networks:
|
||||||
|
- immich-network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
|
||||||
|
database:
|
||||||
|
container_name: immich_postgres
|
||||||
|
image: postgres:14
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
|
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||||
|
PG_DATA: /var/lib/postgresql/data
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
networks:
|
||||||
|
- immich-network
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
container_name: proxy_nginx
|
||||||
|
image: nginx:latest
|
||||||
|
volumes:
|
||||||
|
- ./settings/nginx-conf:/etc/nginx/conf.d
|
||||||
|
ports:
|
||||||
|
- 2283:80
|
||||||
|
- 2284:443
|
||||||
|
logging:
|
||||||
|
driver: none
|
||||||
|
networks:
|
||||||
|
- immich-network
|
||||||
|
depends_on:
|
||||||
|
- immich-server
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
networks:
|
||||||
|
immich-network:
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
52
docker/docker-compose.test.yml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
immich_server_test:
|
||||||
|
image: immich-server-dev:1.9.0
|
||||||
|
build:
|
||||||
|
context: ../server
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
command: npm run test:e2e
|
||||||
|
expose:
|
||||||
|
- "3000"
|
||||||
|
volumes:
|
||||||
|
- ../server:/usr/src/app
|
||||||
|
- /usr/src/app/node_modules
|
||||||
|
env_file:
|
||||||
|
- .env.test
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
depends_on:
|
||||||
|
- redis
|
||||||
|
- database
|
||||||
|
networks:
|
||||||
|
- immich_network_test
|
||||||
|
|
||||||
|
|
||||||
|
redis:
|
||||||
|
container_name: immich_redis_test
|
||||||
|
image: redis:6.2
|
||||||
|
networks:
|
||||||
|
- immich_network_test
|
||||||
|
|
||||||
|
database:
|
||||||
|
container_name: immich_postgres_test
|
||||||
|
image: postgres:14
|
||||||
|
env_file:
|
||||||
|
- .env.test
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
|
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||||
|
PG_DATA: /var/lib/postgresql/data
|
||||||
|
volumes:
|
||||||
|
- pgdata-test:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
networks:
|
||||||
|
- immich_network_test
|
||||||
|
|
||||||
|
networks:
|
||||||
|
immich_network_test:
|
||||||
|
volumes:
|
||||||
|
pgdata-test:
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
immich_server:
|
immich-server:
|
||||||
image: immich-server:1.7.0
|
image: altran1502/immich-server:latest
|
||||||
build:
|
|
||||||
context: ../server
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
@@ -19,14 +16,11 @@ services:
|
|||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
restart: unless-stopped
|
restart: always
|
||||||
|
|
||||||
immich_microservices:
|
immich-microservices:
|
||||||
image: immich-microservices:1.7.0
|
image: altran1502/immich-microservices:latest
|
||||||
build:
|
|
||||||
context: ../microservices
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
expose:
|
expose:
|
||||||
- "3001"
|
- "3001"
|
||||||
@@ -39,14 +33,28 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
restart: unless-stopped
|
restart: always
|
||||||
|
|
||||||
|
immich-web:
|
||||||
|
image: altran1502/immich-web:latest
|
||||||
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- 2285:3000
|
||||||
|
networks:
|
||||||
|
- immich-network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2
|
image: redis:6.2
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
@@ -63,8 +71,9 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
|
restart: always
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
container_name: proxy_nginx
|
container_name: proxy_nginx
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
@@ -76,11 +85,12 @@ services:
|
|||||||
logging:
|
logging:
|
||||||
driver: none
|
driver: none
|
||||||
networks:
|
networks:
|
||||||
- immich_network
|
- immich-network
|
||||||
depends_on:
|
depends_on:
|
||||||
- immich_server
|
- immich-server
|
||||||
|
restart: always
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
immich_network:
|
immich-network:
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
@@ -41,6 +41,6 @@ server {
|
|||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|
||||||
proxy_pass http://immich_server:3000;
|
proxy_pass http://immich-server:3000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
export const databaseConfig: TypeOrmModuleOptions = {
|
export const databaseConfig: TypeOrmModuleOptions = {
|
||||||
type: 'postgres',
|
type: 'postgres',
|
||||||
host: 'immich_postgres',
|
host: process.env.DB_HOSTNAME || 'immich_postgres',
|
||||||
port: 5432,
|
port: 5432,
|
||||||
username: process.env.DB_USERNAME,
|
username: process.env.DB_USERNAME,
|
||||||
password: process.env.DB_PASSWORD,
|
password: process.env.DB_PASSWORD,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ImageClassifierService } from './image-classifier.service';
|
|||||||
export class ImageClassifierController {
|
export class ImageClassifierController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly imageClassifierService: ImageClassifierService,
|
private readonly imageClassifierService: ImageClassifierService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
@Post('/tagImage')
|
@Post('/tagImage')
|
||||||
async tagImage(@Body('thumbnailPath') thumbnailPath: string) {
|
async tagImage(@Body('thumbnailPath') thumbnailPath: string) {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Body, Controller, Post } from '@nestjs/common';
|
import { Body, Controller, Post } from '@nestjs/common';
|
||||||
import { ObjectDetectionService } from './object-detection.service';
|
import { ObjectDetectionService } from './object-detection.service';
|
||||||
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
@Controller('object-detection')
|
@Controller('object-detection')
|
||||||
export class ObjectDetectionController {
|
export class ObjectDetectionController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly objectDetectionService: ObjectDetectionService,
|
private readonly objectDetectionService: ObjectDetectionService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
@Post('/detectObject')
|
@Post('/detectObject')
|
||||||
async detectObject(@Body('thumbnailPath') thumbnailPath: string) {
|
async detectObject(@Body('thumbnailPath') thumbnailPath: string) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich">
|
||||||
<application android:label="Immich" android:name="${applicationName}" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher">
|
<application android:label="Immich" android:name="${applicationName}" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true">
|
||||||
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
// Generated file.
|
// Generated file.
|
||||||
|
//
|
||||||
// If you wish to remove Flutter's multidex support, delete this entire file.
|
// If you wish to remove Flutter's multidex support, delete this entire file.
|
||||||
|
//
|
||||||
|
// Modifications to this file should be done in a copy under a different name
|
||||||
|
// as this file may be regenerated.
|
||||||
|
|
||||||
package io.flutter.app;
|
package io.flutter.app;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.CallSuper;
|
import androidx.annotation.CallSuper;
|
||||||
import androidx.multidex.MultiDex;
|
import androidx.multidex.MultiDex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension of {@link io.flutter.app.FlutterApplication}, adding multidex support.
|
* Extension of {@link android.app.Application}, adding multidex support.
|
||||||
*/
|
*/
|
||||||
public class FlutterMultiDexApplication extends FlutterApplication {
|
public class FlutterMultiDexApplication extends Application {
|
||||||
@Override
|
@Override
|
||||||
@CallSuper
|
@CallSuper
|
||||||
protected void attachBaseContext(Context base) {
|
protected void attachBaseContext(Context base) {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
* Album name is now editable
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* New Feature - Selection backup. User can now select a combination of albums to be included or excluded during the backup process, and only unique photos, and videos that are not overlapping between the two groups will be backup.
|
||||||
|
* Bug fix - Show correct count of backup and remainder assets.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Hotfix: Permission is being requested now when open backup screen on Android10
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* User can now upload profile picture from the home page control drawer.
|
||||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 570 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 183 KiB |
@@ -9,6 +9,8 @@ PODS:
|
|||||||
- FMDB (2.7.5):
|
- FMDB (2.7.5):
|
||||||
- FMDB/standard (= 2.7.5)
|
- FMDB/standard (= 2.7.5)
|
||||||
- FMDB/standard (2.7.5)
|
- FMDB/standard (2.7.5)
|
||||||
|
- image_picker_ios (0.0.1):
|
||||||
|
- Flutter
|
||||||
- package_info_plus (0.4.5):
|
- package_info_plus (0.4.5):
|
||||||
- Flutter
|
- Flutter
|
||||||
- path_provider_ios (0.0.1):
|
- path_provider_ios (0.0.1):
|
||||||
@@ -30,6 +32,7 @@ DEPENDENCIES:
|
|||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||||
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||||
@@ -50,6 +53,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||||
fluttertoast:
|
fluttertoast:
|
||||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||||
|
image_picker_ios:
|
||||||
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
@@ -66,10 +71,11 @@ EXTERNAL SOURCES:
|
|||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
|
||||||
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
|
flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
|
||||||
fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58
|
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
|
image_picker_ios: b786a5dcf033a8336a657191401bfdf12017dabb
|
||||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||||
path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||||
|
|||||||
@@ -360,7 +360,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 14;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 14;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -522,7 +522,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 14;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>1.10.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>2</string>
|
<string>14</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true />
|
<true />
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
@@ -43,6 +43,12 @@
|
|||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>We need to manage backup your photos album</string>
|
<string>We need to manage backup your photos album</string>
|
||||||
|
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||||
|
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
||||||
|
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
@@ -68,5 +74,7 @@
|
|||||||
<true />
|
<true />
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false />
|
<false />
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true />
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.7.0"
|
version_number: "1.10.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
|
|||||||
[bundle exec] fastlane ios beta
|
[bundle exec] fastlane ios beta
|
||||||
```
|
```
|
||||||
|
|
||||||
iOS deployment
|
iOS Beta
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|||||||
@@ -5,27 +5,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000332">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000946">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: latest_testflight_build_number" time="4.608292">
|
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="16.3225">
|
||||||
|
|
||||||
</testcase>
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: increment_build_number" time="0.747162">
|
|
||||||
|
|
||||||
</testcase>
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: build_app" time="88.727281">
|
|
||||||
|
|
||||||
</testcase>
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="4: upload_to_testflight" time="7.79397">
|
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ const String userInfoBox = "immichBoxUserInfo"; // Box
|
|||||||
const String accessTokenKey = "immichBoxAccessTokenKey"; // Key 1
|
const String accessTokenKey = "immichBoxAccessTokenKey"; // Key 1
|
||||||
const String deviceIdKey = 'immichBoxDeviceIdKey'; // Key 2
|
const String deviceIdKey = 'immichBoxDeviceIdKey'; // Key 2
|
||||||
|
|
||||||
// SERVER ENDPOINT
|
// Server endpoint
|
||||||
const String serverEndpointKey = 'immichBoxServerEndpoint';
|
const String serverEndpointKey = 'immichBoxServerEndpoint';
|
||||||
|
|
||||||
// KEY
|
// Login Info
|
||||||
const String hiveAllAsssetKey = "allAssets";
|
const String hiveLoginInfoBox = "immichLoginInfoBox";
|
||||||
const String hiveBackupProgressKey = "backupProgressAssets";
|
const String savedLoginInfoKey = "immichSavedLoginInfoKey";
|
||||||
|
|
||||||
|
// Backup Info
|
||||||
|
const String hiveBackupInfoBox = "immichBackupAlbumInfoBox";
|
||||||
|
const String backupInfoKey = "immichBackupAlbumInfoKey";
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/immich_colors.dart';
|
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||||
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
|
||||||
@@ -15,7 +17,13 @@ import 'constants/hive_box.dart';
|
|||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
await Hive.initFlutter();
|
await Hive.initFlutter();
|
||||||
|
|
||||||
|
Hive.registerAdapter(HiveSavedLoginInfoAdapter());
|
||||||
|
Hive.registerAdapter(HiveBackupAlbumsAdapter());
|
||||||
|
|
||||||
await Hive.openBox(userInfoBox);
|
await Hive.openBox(userInfoBox);
|
||||||
|
await Hive.openBox<HiveSavedLoginInfo>(hiveLoginInfoBox);
|
||||||
|
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
|
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
const SystemUiOverlayStyle(
|
const SystemUiOverlayStyle(
|
||||||
@@ -68,7 +76,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initApp() async {
|
Future<void> initApp() async {
|
||||||
WidgetsBinding.instance?.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -79,7 +87,7 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance?.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
35
mobile/lib/modules/backup/models/available_album.model.dart
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
class AvailableAlbum {
|
||||||
|
final AssetPathEntity albumEntity;
|
||||||
|
final Uint8List? thumbnailData;
|
||||||
|
AvailableAlbum({
|
||||||
|
required this.albumEntity,
|
||||||
|
this.thumbnailData,
|
||||||
|
});
|
||||||
|
|
||||||
|
AvailableAlbum copyWith({
|
||||||
|
AssetPathEntity? albumEntity,
|
||||||
|
Uint8List? thumbnailData,
|
||||||
|
}) {
|
||||||
|
return AvailableAlbum(
|
||||||
|
albumEntity: albumEntity ?? this.albumEntity,
|
||||||
|
thumbnailData: thumbnailData ?? this.thumbnailData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AvailableAlbum(albumEntity: $albumEntity, thumbnailData: $thumbnailData)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is AvailableAlbum && other.albumEntity == albumEntity && other.thumbnailData == thumbnailData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => albumEntity.hashCode ^ thumbnailData.hashCode;
|
||||||
|
}
|
||||||
88
mobile/lib/modules/backup/models/backup_state.model.dart
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
||||||
|
|
||||||
|
enum BackUpProgressEnum { idle, inProgress, done }
|
||||||
|
|
||||||
|
class BackUpState extends Equatable {
|
||||||
|
// enum
|
||||||
|
final BackUpProgressEnum backupProgress;
|
||||||
|
final List<String> allAssetOnDatabase;
|
||||||
|
final double progressInPercentage;
|
||||||
|
final CancelToken cancelToken;
|
||||||
|
final ServerInfo serverInfo;
|
||||||
|
|
||||||
|
/// All available albums on the device
|
||||||
|
final List<AvailableAlbum> availableAlbums;
|
||||||
|
final Set<AssetPathEntity> selectedBackupAlbums;
|
||||||
|
final Set<AssetPathEntity> excludedBackupAlbums;
|
||||||
|
|
||||||
|
/// Assets that are not overlapping in selected backup albums and excluded backup albums
|
||||||
|
final Set<AssetEntity> allUniqueAssets;
|
||||||
|
|
||||||
|
/// All assets from the selected albums that have been backup
|
||||||
|
final Set<String> selectedAlbumsBackupAssetsIds;
|
||||||
|
|
||||||
|
const BackUpState({
|
||||||
|
required this.backupProgress,
|
||||||
|
required this.allAssetOnDatabase,
|
||||||
|
required this.progressInPercentage,
|
||||||
|
required this.cancelToken,
|
||||||
|
required this.serverInfo,
|
||||||
|
required this.availableAlbums,
|
||||||
|
required this.selectedBackupAlbums,
|
||||||
|
required this.excludedBackupAlbums,
|
||||||
|
required this.allUniqueAssets,
|
||||||
|
required this.selectedAlbumsBackupAssetsIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
BackUpState copyWith({
|
||||||
|
BackUpProgressEnum? backupProgress,
|
||||||
|
List<String>? allAssetOnDatabase,
|
||||||
|
double? progressInPercentage,
|
||||||
|
CancelToken? cancelToken,
|
||||||
|
ServerInfo? serverInfo,
|
||||||
|
List<AvailableAlbum>? availableAlbums,
|
||||||
|
Set<AssetPathEntity>? selectedBackupAlbums,
|
||||||
|
Set<AssetPathEntity>? excludedBackupAlbums,
|
||||||
|
Set<AssetEntity>? allUniqueAssets,
|
||||||
|
Set<String>? selectedAlbumsBackupAssetsIds,
|
||||||
|
}) {
|
||||||
|
return BackUpState(
|
||||||
|
backupProgress: backupProgress ?? this.backupProgress,
|
||||||
|
allAssetOnDatabase: allAssetOnDatabase ?? this.allAssetOnDatabase,
|
||||||
|
progressInPercentage: progressInPercentage ?? this.progressInPercentage,
|
||||||
|
cancelToken: cancelToken ?? this.cancelToken,
|
||||||
|
serverInfo: serverInfo ?? this.serverInfo,
|
||||||
|
availableAlbums: availableAlbums ?? this.availableAlbums,
|
||||||
|
selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
|
||||||
|
excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
|
||||||
|
allUniqueAssets: allUniqueAssets ?? this.allUniqueAssets,
|
||||||
|
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssetsIds ?? this.selectedAlbumsBackupAssetsIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'BackUpState(backupProgress: $backupProgress, allAssetOnDatabase: $allAssetOnDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props {
|
||||||
|
return [
|
||||||
|
backupProgress,
|
||||||
|
allAssetOnDatabase,
|
||||||
|
progressInPercentage,
|
||||||
|
cancelToken,
|
||||||
|
serverInfo,
|
||||||
|
availableAlbums,
|
||||||
|
selectedBackupAlbums,
|
||||||
|
excludedBackupAlbums,
|
||||||
|
allUniqueAssets,
|
||||||
|
selectedAlbumsBackupAssetsIds,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
|
||||||
|
part 'hive_backup_albums.model.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: 1)
|
||||||
|
class HiveBackupAlbums {
|
||||||
|
@HiveField(0)
|
||||||
|
List<String> selectedAlbumIds;
|
||||||
|
|
||||||
|
@HiveField(1)
|
||||||
|
List<String> excludedAlbumsIds;
|
||||||
|
|
||||||
|
HiveBackupAlbums({
|
||||||
|
required this.selectedAlbumIds,
|
||||||
|
required this.excludedAlbumsIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'HiveBackupAlbums(selectedAlbumIds: $selectedAlbumIds, excludedAlbumsIds: $excludedAlbumsIds)';
|
||||||
|
|
||||||
|
HiveBackupAlbums copyWith({
|
||||||
|
List<String>? selectedAlbumIds,
|
||||||
|
List<String>? excludedAlbumsIds,
|
||||||
|
}) {
|
||||||
|
return HiveBackupAlbums(
|
||||||
|
selectedAlbumIds: selectedAlbumIds ?? this.selectedAlbumIds,
|
||||||
|
excludedAlbumsIds: excludedAlbumsIds ?? this.excludedAlbumsIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
|
result.addAll({'selectedAlbumIds': selectedAlbumIds});
|
||||||
|
result.addAll({'excludedAlbumsIds': excludedAlbumsIds});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory HiveBackupAlbums.fromMap(Map<String, dynamic> map) {
|
||||||
|
return HiveBackupAlbums(
|
||||||
|
selectedAlbumIds: List<String>.from(map['selectedAlbumIds']),
|
||||||
|
excludedAlbumsIds: List<String>.from(map['excludedAlbumsIds']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory HiveBackupAlbums.fromJson(String source) => HiveBackupAlbums.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
final listEquals = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
|
return other is HiveBackupAlbums &&
|
||||||
|
listEquals(other.selectedAlbumIds, selectedAlbumIds) &&
|
||||||
|
listEquals(other.excludedAlbumsIds, excludedAlbumsIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => selectedAlbumIds.hashCode ^ excludedAlbumsIds.hashCode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'hive_backup_albums.model.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// TypeAdapterGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
class HiveBackupAlbumsAdapter extends TypeAdapter<HiveBackupAlbums> {
|
||||||
|
@override
|
||||||
|
final int typeId = 1;
|
||||||
|
|
||||||
|
@override
|
||||||
|
HiveBackupAlbums read(BinaryReader reader) {
|
||||||
|
final numOfFields = reader.readByte();
|
||||||
|
final fields = <int, dynamic>{
|
||||||
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
|
};
|
||||||
|
return HiveBackupAlbums(
|
||||||
|
selectedAlbumIds: (fields[0] as List).cast<String>(),
|
||||||
|
excludedAlbumsIds: (fields[1] as List).cast<String>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(BinaryWriter writer, HiveBackupAlbums obj) {
|
||||||
|
writer
|
||||||
|
..writeByte(2)
|
||||||
|
..writeByte(0)
|
||||||
|
..write(obj.selectedAlbumIds)
|
||||||
|
..writeByte(1)
|
||||||
|
..write(obj.excludedAlbumsIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => typeId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is HiveBackupAlbumsAdapter &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
typeId == other.typeId;
|
||||||
|
}
|
||||||
347
mobile/lib/modules/backup/providers/backup.provider.dart
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
import 'package:dio/dio.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/modules/backup/models/available_album.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
|
BackupNotifier({this.ref})
|
||||||
|
: super(
|
||||||
|
BackUpState(
|
||||||
|
backupProgress: BackUpProgressEnum.idle,
|
||||||
|
allAssetOnDatabase: const [],
|
||||||
|
progressInPercentage: 0,
|
||||||
|
cancelToken: CancelToken(),
|
||||||
|
serverInfo: ServerInfo(
|
||||||
|
diskAvailable: "0",
|
||||||
|
diskAvailableRaw: 0,
|
||||||
|
diskSize: "0",
|
||||||
|
diskSizeRaw: 0,
|
||||||
|
diskUsagePercentage: 0.0,
|
||||||
|
diskUse: "0",
|
||||||
|
diskUseRaw: 0,
|
||||||
|
),
|
||||||
|
availableAlbums: const [],
|
||||||
|
selectedBackupAlbums: const {},
|
||||||
|
excludedBackupAlbums: const {},
|
||||||
|
allUniqueAssets: const {},
|
||||||
|
selectedAlbumsBackupAssetsIds: const {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ref? ref;
|
||||||
|
final BackupService _backupService = BackupService();
|
||||||
|
final ServerInfoService _serverInfoService = ServerInfoService();
|
||||||
|
|
||||||
|
///
|
||||||
|
/// UI INTERACTION
|
||||||
|
///
|
||||||
|
/// Album selection
|
||||||
|
/// Due to the overlapping assets across multiple albums on the device
|
||||||
|
/// We have method to include and exclude albums
|
||||||
|
/// The total unique assets will be used for backing mechanism
|
||||||
|
///
|
||||||
|
void addAlbumForBackup(AssetPathEntity album) {
|
||||||
|
if (state.excludedBackupAlbums.contains(album)) {
|
||||||
|
removeExcludedAlbumForBackup(album);
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
|
||||||
|
_updateBackupAssetCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addExcludedAlbumForBackup(AssetPathEntity album) {
|
||||||
|
if (state.selectedBackupAlbums.contains(album)) {
|
||||||
|
removeAlbumForBackup(album);
|
||||||
|
}
|
||||||
|
state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
|
||||||
|
_updateBackupAssetCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeAlbumForBackup(AssetPathEntity album) {
|
||||||
|
Set<AssetPathEntity> currentSelectedAlbums = state.selectedBackupAlbums;
|
||||||
|
|
||||||
|
currentSelectedAlbums.removeWhere((a) => a == album);
|
||||||
|
|
||||||
|
state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums);
|
||||||
|
_updateBackupAssetCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeExcludedAlbumForBackup(AssetPathEntity album) {
|
||||||
|
Set<AssetPathEntity> currentExcludedAlbums = state.excludedBackupAlbums;
|
||||||
|
|
||||||
|
currentExcludedAlbums.removeWhere((a) => a == album);
|
||||||
|
|
||||||
|
state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums);
|
||||||
|
_updateBackupAssetCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Get all album on the device
|
||||||
|
/// Get all selected and excluded album from the user's persistent storage
|
||||||
|
/// If this is the first time performing backup - set the default selected album to be
|
||||||
|
/// the one that has all assets (Recent on Android, Recents on iOS)
|
||||||
|
///
|
||||||
|
Future<void> getBackupAlbumsInfo() async {
|
||||||
|
// Get all albums on the device
|
||||||
|
List<AvailableAlbum> availableAlbums = [];
|
||||||
|
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(hasAll: true, type: RequestType.common);
|
||||||
|
|
||||||
|
for (AssetPathEntity album in albums) {
|
||||||
|
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
||||||
|
|
||||||
|
var assetList = await album.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
|
|
||||||
|
if (assetList.isNotEmpty) {
|
||||||
|
var thumbnailAsset = assetList.first;
|
||||||
|
var thumbnailData = await thumbnailAsset.thumbnailDataWithSize(const ThumbnailSize(512, 512));
|
||||||
|
availableAlbum = availableAlbum.copyWith(thumbnailData: thumbnailData);
|
||||||
|
}
|
||||||
|
|
||||||
|
availableAlbums.add(availableAlbum);
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(availableAlbums: availableAlbums);
|
||||||
|
|
||||||
|
// Put persistent storage info into local state of the app
|
||||||
|
// Get local storage on selected backup album
|
||||||
|
Box<HiveBackupAlbums> backupAlbumInfoBox = Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
|
HiveBackupAlbums? backupAlbumInfo = backupAlbumInfoBox.get(
|
||||||
|
backupInfoKey,
|
||||||
|
defaultValue: HiveBackupAlbums(
|
||||||
|
selectedAlbumIds: [],
|
||||||
|
excludedAlbumsIds: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (backupAlbumInfo == null) {
|
||||||
|
debugPrint("[ERROR] getting Hive backup album infomation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First time backup - set isAll album is the default one for backup.
|
||||||
|
if (backupAlbumInfo.selectedAlbumIds.isEmpty) {
|
||||||
|
debugPrint("First time backup setup recent album as default");
|
||||||
|
|
||||||
|
// Get album that contains all assets
|
||||||
|
var list = await PhotoManager.getAssetPathList(hasAll: true, onlyAll: true, type: RequestType.common);
|
||||||
|
AssetPathEntity albumHasAllAssets = list.first;
|
||||||
|
|
||||||
|
backupAlbumInfoBox.put(
|
||||||
|
backupInfoKey,
|
||||||
|
HiveBackupAlbums(
|
||||||
|
selectedAlbumIds: [albumHasAllAssets.id],
|
||||||
|
excludedAlbumsIds: [],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
backupAlbumInfo = backupAlbumInfoBox.get(backupInfoKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate AssetPathEntity from id to add to local state
|
||||||
|
try {
|
||||||
|
for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
|
||||||
|
var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
|
||||||
|
state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
|
||||||
|
var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
|
||||||
|
state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("[ERROR] Failed to generate album from id $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// From all the selected and albums assets
|
||||||
|
/// Find the assets that are not overlapping between the two sets
|
||||||
|
/// Those assets are unique and are used as the total assets
|
||||||
|
///
|
||||||
|
void _updateBackupAssetCount() async {
|
||||||
|
Set<AssetEntity> assetsFromSelectedAlbums = {};
|
||||||
|
Set<AssetEntity> assetsFromExcludedAlbums = {};
|
||||||
|
|
||||||
|
for (var album in state.selectedBackupAlbums) {
|
||||||
|
var assets = await album.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
|
assetsFromSelectedAlbums.addAll(assets);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var album in state.excludedBackupAlbums) {
|
||||||
|
var assets = await album.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
|
assetsFromExcludedAlbums.addAll(assets);
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<AssetEntity> allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
|
||||||
|
List<String> allAssetOnDatabase = await _backupService.getDeviceBackupAsset();
|
||||||
|
|
||||||
|
// Find asset that were backup from selected albums
|
||||||
|
Set<String> selectedAlbumsBackupAssets = Set.from(allUniqueAssets.map((e) => e.id));
|
||||||
|
selectedAlbumsBackupAssets.removeWhere((assetId) => !allAssetOnDatabase.contains(assetId));
|
||||||
|
|
||||||
|
if (allUniqueAssets.isEmpty) {
|
||||||
|
debugPrint("No Asset On Device");
|
||||||
|
state = state.copyWith(
|
||||||
|
backupProgress: BackUpProgressEnum.idle,
|
||||||
|
allAssetOnDatabase: allAssetOnDatabase,
|
||||||
|
allUniqueAssets: {},
|
||||||
|
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
state = state.copyWith(
|
||||||
|
allAssetOnDatabase: allAssetOnDatabase,
|
||||||
|
allUniqueAssets: allUniqueAssets,
|
||||||
|
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to persistent storage
|
||||||
|
_updatePersistentAlbumsSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Get all necessary information for calculating the available albums,
|
||||||
|
/// which albums are selected or excluded
|
||||||
|
/// and then update the UI according to those information
|
||||||
|
///
|
||||||
|
void getBackupInfo() async {
|
||||||
|
await getBackupAlbumsInfo();
|
||||||
|
_updateServerInfo();
|
||||||
|
_updateBackupAssetCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Save user selection of selected albums and excluded albums to
|
||||||
|
/// Hive database
|
||||||
|
///
|
||||||
|
void _updatePersistentAlbumsSelection() {
|
||||||
|
Box<HiveBackupAlbums> backupAlbumInfoBox = Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
|
backupAlbumInfoBox.put(
|
||||||
|
backupInfoKey,
|
||||||
|
HiveBackupAlbums(
|
||||||
|
selectedAlbumIds: state.selectedBackupAlbums.map((e) => e.id).toList(),
|
||||||
|
excludedAlbumsIds: state.excludedBackupAlbums.map((e) => e.id).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Invoke backup process
|
||||||
|
///
|
||||||
|
void startBackupProcess() async {
|
||||||
|
_updateServerInfo();
|
||||||
|
_updateBackupAssetCount();
|
||||||
|
|
||||||
|
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
|
||||||
|
|
||||||
|
var authResult = await PhotoManager.requestPermissionExtend();
|
||||||
|
if (authResult.isAuth) {
|
||||||
|
await PhotoManager.clearFileCache();
|
||||||
|
|
||||||
|
if (state.allUniqueAssets.isEmpty) {
|
||||||
|
debugPrint("No Asset On Device - Abort Backup Process");
|
||||||
|
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<AssetEntity> assetsWillBeBackup = state.allUniqueAssets;
|
||||||
|
|
||||||
|
// Remove item that has already been backed up
|
||||||
|
for (var assetId in state.allAssetOnDatabase) {
|
||||||
|
assetsWillBeBackup.removeWhere((e) => e.id == assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assetsWillBeBackup.isEmpty) {
|
||||||
|
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform Backup
|
||||||
|
state = state.copyWith(cancelToken: CancelToken());
|
||||||
|
_backupService.backupAsset(assetsWillBeBackup, state.cancelToken, _onAssetUploaded, _onUploadProgress);
|
||||||
|
} else {
|
||||||
|
PhotoManager.openSetting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancelBackup() {
|
||||||
|
state.cancelToken.cancel('Cancel Backup');
|
||||||
|
state = state.copyWith(backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
||||||
|
state = state.copyWith(
|
||||||
|
selectedAlbumsBackupAssetsIds: {...state.selectedAlbumsBackupAssetsIds, deviceAssetId},
|
||||||
|
allAssetOnDatabase: [...state.allAssetOnDatabase, deviceAssetId]);
|
||||||
|
|
||||||
|
if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) {
|
||||||
|
state = state.copyWith(backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateServerInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onUploadProgress(int sent, int total) {
|
||||||
|
state = state.copyWith(progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateServerInfo() async {
|
||||||
|
var serverInfo = await _serverInfoService.getServerInfo();
|
||||||
|
|
||||||
|
// Update server info
|
||||||
|
state = state.copyWith(
|
||||||
|
serverInfo: ServerInfo(
|
||||||
|
diskSize: serverInfo.diskSize,
|
||||||
|
diskUse: serverInfo.diskUse,
|
||||||
|
diskAvailable: serverInfo.diskAvailable,
|
||||||
|
diskSizeRaw: serverInfo.diskSizeRaw,
|
||||||
|
diskUseRaw: serverInfo.diskUseRaw,
|
||||||
|
diskAvailableRaw: serverInfo.diskAvailableRaw,
|
||||||
|
diskUsagePercentage: serverInfo.diskUsagePercentage,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resumeBackup() {
|
||||||
|
var authState = ref?.read(authenticationProvider);
|
||||||
|
|
||||||
|
// Check if user is login
|
||||||
|
var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
|
||||||
|
|
||||||
|
// User has been logged out return
|
||||||
|
if (authState != null) {
|
||||||
|
if (accessKey == null || !authState.isAuthenticated) {
|
||||||
|
debugPrint("[resumeBackup] not authenticated - abort");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this device is enable backup by the user
|
||||||
|
if ((authState.deviceInfo.deviceId == authState.deviceId) && authState.deviceInfo.isAutoBackup) {
|
||||||
|
// check if backup is alreayd in process - then return
|
||||||
|
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
||||||
|
debugPrint("[resumeBackup] Backup is already in progress - abort");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run backup
|
||||||
|
debugPrint("[resumeBackup] Start back up");
|
||||||
|
startBackupProcess();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
||||||
|
return BackupNotifier(ref: ref);
|
||||||
|
});
|
||||||
@@ -26,7 +26,7 @@ class BackupService {
|
|||||||
return result.cast<String>();
|
return result.cast<String>();
|
||||||
}
|
}
|
||||||
|
|
||||||
backupAsset(List<AssetEntity> assetList, CancelToken cancelToken, Function(String, String) singleAssetDoneCb,
|
backupAsset(Set<AssetEntity> assetList, CancelToken cancelToken, Function(String, String) singleAssetDoneCb,
|
||||||
Function(int, int) uploadProgress) async {
|
Function(int, int) uploadProgress) async {
|
||||||
var dio = Dio();
|
var dio = Dio();
|
||||||
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
||||||
@@ -73,7 +73,7 @@ class BackupService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Build thumbnail multipart data
|
// Build thumbnail multipart data
|
||||||
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(720, 1280));
|
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(1440, 2560));
|
||||||
if (thumbnailData != null) {
|
if (thumbnailData != null) {
|
||||||
thumbnailUploadData = MultipartFile.fromBytes(
|
thumbnailUploadData = MultipartFile.fromBytes(
|
||||||
List.from(thumbnailData),
|
List.from(thumbnailData),
|
||||||
185
mobile/lib/modules/backup/ui/album_info_card.dart
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
class AlbumInfoCard extends HookConsumerWidget {
|
||||||
|
final Uint8List? imageData;
|
||||||
|
final AssetPathEntity albumInfo;
|
||||||
|
|
||||||
|
const AlbumInfoCard({Key? key, this.imageData, required this.albumInfo}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final bool isSelected = ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
|
||||||
|
final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
|
||||||
|
|
||||||
|
ColorFilter selectedFilter = ColorFilter.mode(Theme.of(context).primaryColor.withAlpha(100), BlendMode.darken);
|
||||||
|
ColorFilter excludedFilter = ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
|
||||||
|
ColorFilter unselectedFilter = const ColorFilter.mode(Colors.black, BlendMode.color);
|
||||||
|
|
||||||
|
_buildSelectedTextBox() {
|
||||||
|
if (isSelected) {
|
||||||
|
return Chip(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
|
label: const Text(
|
||||||
|
"INCLUDED",
|
||||||
|
style: TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
);
|
||||||
|
} else if (isExcluded) {
|
||||||
|
return Chip(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
|
label: const Text(
|
||||||
|
"EXCLUDED",
|
||||||
|
style: TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.red[300],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildImageFilter() {
|
||||||
|
if (isSelected) {
|
||||||
|
return selectedFilter;
|
||||||
|
} else if (isExcluded) {
|
||||||
|
return excludedFilter;
|
||||||
|
} else {
|
||||||
|
return unselectedFilter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "Cannot remove the only album",
|
||||||
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.watch(backupProvider.notifier).removeAlbumForBackup(albumInfo);
|
||||||
|
} else {
|
||||||
|
ref.watch(backupProvider.notifier).addAlbumForBackup(albumInfo);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDoubleTap: () {
|
||||||
|
HapticFeedback.selectionClick();
|
||||||
|
|
||||||
|
if (isExcluded) {
|
||||||
|
ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(albumInfo);
|
||||||
|
} else {
|
||||||
|
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1 &&
|
||||||
|
ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo)) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "Cannot exclude the only album",
|
||||||
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.watch(backupProvider.notifier).addExcludedAlbumForBackup(albumInfo);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Card(
|
||||||
|
margin: const EdgeInsets.all(1),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12), // if you need this
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Color(0xFFC9C9C9),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
borderOnForeground: false,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)),
|
||||||
|
image: DecorationImage(
|
||||||
|
colorFilter: _buildImageFilter(),
|
||||||
|
image: imageData != null
|
||||||
|
? MemoryImage(imageData!)
|
||||||
|
: const AssetImage('assets/immich-logo-no-outline.png') as ImageProvider,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: null,
|
||||||
|
),
|
||||||
|
Positioned(bottom: 10, left: 25, child: _buildSelectedTextBox())
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 140,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 25.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
albumInfo.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14, color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 2.0),
|
||||||
|
child: Text(
|
||||||
|
albumInfo.assetCount.toString() + (albumInfo.isAll ? " (ALL)" : ""),
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
AutoRouter.of(context).push(AlbumPreviewRoute(album: albumInfo));
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.image_outlined,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
splashRadius: 25,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
mobile/lib/modules/backup/ui/backup_info_card.dart
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class BackupInfoCard extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final String info;
|
||||||
|
const BackupInfoCard({Key? key, required this.title, required this.subtitle, required this.info}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5), // if you need this
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Colors.black12,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
borderOnForeground: false,
|
||||||
|
child: ListTile(
|
||||||
|
minVerticalPadding: 15,
|
||||||
|
isThreeLine: true,
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||||||
|
),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
subtitle,
|
||||||
|
style: const TextStyle(color: Color(0xFF808080), fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
info,
|
||||||
|
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Text("assets"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
mobile/lib/modules/backup/views/album_preview_page.dart
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
|
class AlbumPreviewPage extends HookConsumerWidget {
|
||||||
|
final AssetPathEntity album;
|
||||||
|
const AlbumPreviewPage({Key? key, required this.album}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final assets = useState<List<AssetEntity>>([]);
|
||||||
|
|
||||||
|
_getAssetsInAlbum() async {
|
||||||
|
assets.value = await album.getAssetListRange(start: 0, end: album.assetCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
_getAssetsInAlbum();
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
title: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"${album.name} (${album.assetCount})",
|
||||||
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4.0),
|
||||||
|
child: Text(
|
||||||
|
"ID ${album.id}",
|
||||||
|
style: TextStyle(fontSize: 10, color: Colors.grey[600], fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () => AutoRouter.of(context).pop(),
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: GridView.builder(
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 5,
|
||||||
|
crossAxisSpacing: 2,
|
||||||
|
mainAxisSpacing: 2,
|
||||||
|
),
|
||||||
|
itemCount: assets.value.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
Future<Uint8List?> thumbData =
|
||||||
|
assets.value[index].thumbnailDataWithSize(const ThumbnailSize(200, 200), quality: 50);
|
||||||
|
|
||||||
|
return FutureBuilder<Uint8List?>(
|
||||||
|
future: thumbData,
|
||||||
|
builder: ((context, snapshot) {
|
||||||
|
if (snapshot.hasData && snapshot.data != null) {
|
||||||
|
return Image.memory(
|
||||||
|
snapshot.data!,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const SizedBox(
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
child: ImmichLoadingIndicator(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
244
mobile/lib/modules/backup/views/backup_album_selection_page.dart
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
|
||||||
|
class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||||
|
const BackupAlbumSelectionPage({Key? key}) : super(key: key);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final availableAlbums = ref.watch(backupProvider).availableAlbums;
|
||||||
|
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
|
||||||
|
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
ref.read(backupProvider.notifier).getBackupAlbumsInfo();
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
_buildAlbumSelectionList() {
|
||||||
|
if (availableAlbums.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: ImmichLoadingIndicator(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
height: 265,
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: availableAlbums.length,
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
itemBuilder: ((context, index) {
|
||||||
|
var thumbnailData = availableAlbums[index].thumbnailData;
|
||||||
|
return Padding(
|
||||||
|
padding: index == 0 ? const EdgeInsets.only(left: 16.00) : const EdgeInsets.all(0),
|
||||||
|
child: AlbumInfoCard(imageData: thumbnailData, albumInfo: availableAlbums[index].albumEntity),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildSelectedAlbumNameChip() {
|
||||||
|
return selectedBackupAlbums.map((album) {
|
||||||
|
void removeSelection() {
|
||||||
|
if (ref.watch(backupProvider).selectedBackupAlbums.length == 1) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "Cannot remove the only album",
|
||||||
|
toastType: ToastType.error,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.watch(backupProvider.notifier).removeAlbumForBackup(album);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: removeSelection,
|
||||||
|
child: Chip(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
|
label: Text(
|
||||||
|
album.name,
|
||||||
|
style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
deleteIconColor: Colors.white,
|
||||||
|
deleteIcon: const Icon(
|
||||||
|
Icons.cancel_rounded,
|
||||||
|
size: 15,
|
||||||
|
),
|
||||||
|
onDeleted: removeSelection,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildExcludedAlbumNameChip() {
|
||||||
|
return excludedBackupAlbums.map((album) {
|
||||||
|
void removeSelection() {
|
||||||
|
ref.watch(backupProvider.notifier).removeExcludedAlbumForBackup(album);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: removeSelection,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: Chip(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
|
label: Text(
|
||||||
|
album.name,
|
||||||
|
style: const TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.red[300],
|
||||||
|
deleteIconColor: Colors.white,
|
||||||
|
deleteIcon: const Icon(
|
||||||
|
Icons.cancel_rounded,
|
||||||
|
size: 15,
|
||||||
|
),
|
||||||
|
onDeleted: removeSelection,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () => AutoRouter.of(context).pop(),
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
|
),
|
||||||
|
title: const Text(
|
||||||
|
"Select Albums",
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||||
|
child: Text(
|
||||||
|
"Selection Info",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Selected Album Chips
|
||||||
|
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Wrap(
|
||||||
|
children: [..._buildSelectedAlbumNameChip(), ..._buildExcludedAlbumNameChip()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
|
||||||
|
child: Card(
|
||||||
|
margin: const EdgeInsets.all(0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5), // if you need this
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Color.fromARGB(255, 235, 235, 235),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
borderOnForeground: false,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
title: Text(
|
||||||
|
"Total unique assets",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: Colors.grey[700]),
|
||||||
|
),
|
||||||
|
trailing: Text(
|
||||||
|
ref.watch(backupProvider).allUniqueAssets.length.toString(),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
"Albums on device (${availableAlbums.length.toString()})",
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
|
),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Text(
|
||||||
|
"Tap to include, double tap to exclude",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
splashRadius: 16,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.info,
|
||||||
|
size: 20,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
// show the dialog
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
elevation: 5,
|
||||||
|
title: Text(
|
||||||
|
'Selection Info',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: ListBody(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
|
child: _buildAlbumSelectionList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,12 @@ 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/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/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';
|
||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/ui/backup_info_card.dart';
|
||||||
import 'package:percent_indicator/linear_percent_indicator.dart';
|
import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||||
|
|
||||||
class BackupControllerPage extends HookConsumerWidget {
|
class BackupControllerPage extends HookConsumerWidget {
|
||||||
@@ -14,13 +16,13 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
BackUpState _backupState = ref.watch(backupProvider);
|
BackUpState backupState = ref.watch(backupProvider);
|
||||||
AuthenticationState _authenticationState = ref.watch(authenticationProvider);
|
AuthenticationState _authenticationState = ref.watch(authenticationProvider);
|
||||||
|
bool shouldBackup =
|
||||||
bool shouldBackup = _backupState.totalAssetCount - _backupState.assetOnDatabase == 0 ? false : true;
|
backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length == 0 ? false : true;
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (_backupState.backupProgress != BackUpProgressEnum.inProgress) {
|
if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
|
||||||
ref.read(backupProvider.notifier).getBackupInfo();
|
ref.read(backupProvider.notifier).getBackupInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,16 +45,20 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
LinearPercentIndicator(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
lineHeight: 5.0,
|
child: LinearPercentIndicator(
|
||||||
percent: _backupState.serverInfo.diskUsagePercentage / 100.0,
|
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
|
||||||
backgroundColor: Colors.grey,
|
barRadius: const Radius.circular(2),
|
||||||
progressColor: Theme.of(context).primaryColor,
|
lineHeight: 6.0,
|
||||||
|
percent: backupState.serverInfo.diskUsagePercentage / 100.0,
|
||||||
|
backgroundColor: Colors.grey,
|
||||||
|
progressColor: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 12.0),
|
padding: const EdgeInsets.only(top: 12.0),
|
||||||
child: Text('${_backupState.serverInfo.diskUse} of ${_backupState.serverInfo.diskSize} used'),
|
child: Text('${backupState.serverInfo.diskUse} of ${backupState.serverInfo.diskSize} used'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -104,18 +110,120 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSelectedAlbumName() {
|
||||||
|
var text = "Selected: ";
|
||||||
|
var albums = ref.watch(backupProvider).selectedBackupAlbums;
|
||||||
|
|
||||||
|
if (albums.isNotEmpty) {
|
||||||
|
for (var album in albums) {
|
||||||
|
if (album.name == "Recent" || album.name == "Recents") {
|
||||||
|
text += "${album.name} (All), ";
|
||||||
|
} else {
|
||||||
|
text += "${album.name}, ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
text.trim().substring(0, text.length - 2),
|
||||||
|
style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 12, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
"None selected",
|
||||||
|
style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 12, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildExcludedAlbumName() {
|
||||||
|
var text = "Excluded: ";
|
||||||
|
var albums = ref.watch(backupProvider).excludedBackupAlbums;
|
||||||
|
|
||||||
|
if (albums.isNotEmpty) {
|
||||||
|
for (var album in albums) {
|
||||||
|
text += "${album.name}, ";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
text.trim().substring(0, text.length - 2),
|
||||||
|
style: TextStyle(color: Colors.red[300], fontSize: 12, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildFolderSelectionTile() {
|
||||||
|
return Card(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5), // if you need this
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Colors.black12,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
borderOnForeground: false,
|
||||||
|
child: ListTile(
|
||||||
|
minVerticalPadding: 15,
|
||||||
|
title: const Text("Backup Albums", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||||
|
subtitle: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"Albums to be backup",
|
||||||
|
style: TextStyle(color: Color(0xFF808080), fontSize: 12),
|
||||||
|
),
|
||||||
|
_buildSelectedAlbumName(),
|
||||||
|
_buildExcludedAlbumName()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
|
||||||
|
},
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: 16.0,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
"Select",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Backup",
|
"Backup",
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
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);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.arrow_back_ios_rounded)),
|
splashRadius: 24,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back_ios_rounded,
|
||||||
|
)),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
@@ -129,20 +237,21 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
_buildFolderSelectionTile(),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Total",
|
title: "Total",
|
||||||
subtitle: "All images and videos on the device",
|
subtitle: "All unique photos and videos from selected albums",
|
||||||
info: "${_backupState.totalAssetCount}",
|
info: "${backupState.allUniqueAssets.length}",
|
||||||
),
|
),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Backup",
|
title: "Backup",
|
||||||
subtitle: "Images and videos of the device that are backup on server",
|
subtitle: "Photos and videos from selected albums that are backup",
|
||||||
info: "${_backupState.assetOnDatabase}",
|
info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
|
||||||
),
|
),
|
||||||
BackupInfoCard(
|
BackupInfoCard(
|
||||||
title: "Remainder",
|
title: "Remainder",
|
||||||
subtitle: "Images and videos that has not been backing up",
|
subtitle: "Photos and videos that has not been backing up from selected albums",
|
||||||
info: "${_backupState.totalAssetCount - _backupState.assetOnDatabase}",
|
info: "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
_buildBackupController(),
|
_buildBackupController(),
|
||||||
@@ -152,14 +261,14 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"Asset that were being backup: ${_backupState.backingUpAssetCount} [${_backupState.progressInPercentage.toStringAsFixed(0)}%]"),
|
"Asset that were being backup: ${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length} [${backupState.progressInPercentage.toStringAsFixed(0)}%]"),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
const Text("Backup Progress:"),
|
const Text("Backup Progress:"),
|
||||||
const Padding(padding: EdgeInsets.symmetric(horizontal: 2)),
|
const Padding(padding: EdgeInsets.symmetric(horizontal: 2)),
|
||||||
_backupState.backupProgress == BackUpProgressEnum.inProgress
|
backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||||
? const CircularProgressIndicator.adaptive()
|
? const CircularProgressIndicator.adaptive()
|
||||||
: const Text("Done"),
|
: const Text("Done"),
|
||||||
]),
|
]),
|
||||||
@@ -167,7 +276,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Container(
|
child: Container(
|
||||||
child: _backupState.backupProgress == BackUpProgressEnum.inProgress
|
child: backupState.backupProgress == BackUpProgressEnum.inProgress
|
||||||
? ElevatedButton(
|
? ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(primary: Colors.red[300]),
|
style: ElevatedButton.styleFrom(primary: Colors.red[300]),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -191,50 +300,3 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BackupInfoCard extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
final String subtitle;
|
|
||||||
final String info;
|
|
||||||
const BackupInfoCard({Key? key, required this.title, required this.subtitle, required this.info}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Card(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(5), // if you need this
|
|
||||||
side: const BorderSide(
|
|
||||||
color: Colors.black12,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
borderOnForeground: false,
|
|
||||||
child: ListTile(
|
|
||||||
minVerticalPadding: 15,
|
|
||||||
isThreeLine: true,
|
|
||||||
title: Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
|
||||||
),
|
|
||||||
subtitle: Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Text(
|
|
||||||
subtitle,
|
|
||||||
style: const TextStyle(color: Color(0xFF808080), fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
trailing: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
info,
|
|
||||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
const Text("assets"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/shared/services/user.service.dart';
|
||||||
|
|
||||||
|
enum UploadProfileStatus {
|
||||||
|
idle,
|
||||||
|
loading,
|
||||||
|
success,
|
||||||
|
failure,
|
||||||
|
}
|
||||||
|
|
||||||
|
class UploadProfileImageState {
|
||||||
|
// enum
|
||||||
|
final UploadProfileStatus status;
|
||||||
|
final String profileImagePath;
|
||||||
|
UploadProfileImageState({
|
||||||
|
required this.status,
|
||||||
|
required this.profileImagePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
UploadProfileImageState copyWith({
|
||||||
|
UploadProfileStatus? status,
|
||||||
|
String? profileImagePath,
|
||||||
|
}) {
|
||||||
|
return UploadProfileImageState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
|
result.addAll({'status': status.index});
|
||||||
|
result.addAll({'profileImagePath': profileImagePath});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory UploadProfileImageState.fromMap(Map<String, dynamic> map) {
|
||||||
|
return UploadProfileImageState(
|
||||||
|
status: UploadProfileStatus.values[map['status'] ?? 0],
|
||||||
|
profileImagePath: map['profileImagePath'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory UploadProfileImageState.fromJson(String source) => UploadProfileImageState.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'UploadProfileImageState(status: $status, profileImagePath: $profileImagePath)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is UploadProfileImageState && other.status == status && other.profileImagePath == profileImagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => status.hashCode ^ profileImagePath.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UploadProfileImageNotifier extends StateNotifier<UploadProfileImageState> {
|
||||||
|
UploadProfileImageNotifier()
|
||||||
|
: super(UploadProfileImageState(
|
||||||
|
profileImagePath: '',
|
||||||
|
status: UploadProfileStatus.idle,
|
||||||
|
));
|
||||||
|
|
||||||
|
Future<bool> upload(XFile file) async {
|
||||||
|
state = state.copyWith(status: UploadProfileStatus.loading);
|
||||||
|
|
||||||
|
var res = await UserService().uploadProfileImage(file);
|
||||||
|
|
||||||
|
if (res != null) {
|
||||||
|
debugPrint("Succesfully upload profile image");
|
||||||
|
state = state.copyWith(status: UploadProfileStatus.success, profileImagePath: res.profileImagePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(status: UploadProfileStatus.failure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final uploadProfileImageProvider =
|
||||||
|
StateNotifierProvider<UploadProfileImageNotifier, UploadProfileImageState>(((ref) => UploadProfileImageNotifier()));
|
||||||
@@ -12,7 +12,7 @@ class ImageGrid extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return SliverGrid(
|
return SliverGrid(
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 3,
|
crossAxisCount: 4,
|
||||||
crossAxisSpacing: 5.0,
|
crossAxisSpacing: 5.0,
|
||||||
mainAxisSpacing: 5,
|
mainAxisSpacing: 5,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/models/backup_state.model.dart';
|
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
|
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
|
|
||||||
class ImmichSliverAppBar extends ConsumerWidget {
|
class ImmichSliverAppBar extends ConsumerWidget {
|
||||||
@@ -29,7 +29,6 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
floating: true,
|
floating: true,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
snap: false,
|
snap: false,
|
||||||
// backgroundColor: Colors.grey[200],
|
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
|
||||||
leading: Builder(
|
leading: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
@@ -131,7 +130,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
? Positioned(
|
? Positioned(
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
child: Text(
|
child: Text(
|
||||||
_backupState.backingUpAssetCount.toString(),
|
(_backupState.allUniqueAssets.length - _backupState.selectedAlbumsBackupAssetsIds.length)
|
||||||
|
.toString(),
|
||||||
style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,25 +1,33 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/providers/upload_profile_image.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
|
import 'package:immich_mobile/modules/login/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_state.model.dart';
|
import 'package:immich_mobile/shared/models/server_info_state.model.dart';
|
||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
class ProfileDrawer extends HookConsumerWidget {
|
class ProfileDrawer extends HookConsumerWidget {
|
||||||
const ProfileDrawer({Key? key}) : super(key: key);
|
const ProfileDrawer({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
AuthenticationState _authState = ref.watch(authenticationProvider);
|
AuthenticationState _authState = ref.watch(authenticationProvider);
|
||||||
ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
|
ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
|
||||||
|
final uploadProfileImageStatus = ref.watch(uploadProfileImageProvider).status;
|
||||||
final appInfo = useState({});
|
final appInfo = useState({});
|
||||||
|
var dummmy = Random().nextInt(1024);
|
||||||
|
|
||||||
_getPackageInfo() async {
|
_getPackageInfo() async {
|
||||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||||
@@ -30,19 +38,74 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buildUserProfileImage() {
|
||||||
|
if (_authState.profileImagePath.isEmpty) {
|
||||||
|
return const CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadProfileImageStatus == UploadProfileStatus.idle) {
|
||||||
|
if (_authState.profileImagePath.isNotEmpty) {
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: NetworkImage('$endpoint/user/profile-image/${_authState.userId}?d=${dummmy++}'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadProfileImageStatus == UploadProfileStatus.success) {
|
||||||
|
return CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: NetworkImage('$endpoint/user/profile-image/${_authState.userId}?d=${dummmy++}'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadProfileImageStatus == UploadProfileStatus.failure) {
|
||||||
|
return const CircleAvatar(
|
||||||
|
radius: 35,
|
||||||
|
backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadProfileImageStatus == UploadProfileStatus.loading) {
|
||||||
|
return const ImmichLoadingIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
_pickUserProfileImage() async {
|
||||||
|
final XFile? image = await ImagePicker().pickImage(source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024);
|
||||||
|
|
||||||
|
if (image != null) {
|
||||||
|
var success = await ref.watch(uploadProfileImageProvider.notifier).upload(image);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
ref
|
||||||
|
.watch(authenticationProvider.notifier)
|
||||||
|
.updateUserProfileImagePath(ref.read(uploadProfileImageProvider).profileImagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
_getPackageInfo();
|
_getPackageInfo();
|
||||||
|
_buildUserProfileImage();
|
||||||
return null;
|
return null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return Drawer(
|
return Drawer(
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topRight: Radius.circular(5),
|
|
||||||
bottomRight: Radius.circular(5),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -51,22 +114,60 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
children: [
|
children: [
|
||||||
DrawerHeader(
|
DrawerHeader(
|
||||||
decoration: BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.grey[200],
|
gradient: LinearGradient(
|
||||||
|
colors: [Color.fromARGB(255, 216, 219, 238), Color.fromARGB(255, 226, 230, 231)],
|
||||||
|
begin: Alignment.centerRight,
|
||||||
|
end: Alignment.centerLeft,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Image(
|
Stack(
|
||||||
image: AssetImage('assets/immich-logo-no-outline.png'),
|
clipBehavior: Clip.none,
|
||||||
width: 50,
|
children: [
|
||||||
filterQuality: FilterQuality.high,
|
_buildUserProfileImage(),
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
right: -5,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: _pickUserProfileImage,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.grey[50],
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(50.0),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(5.0),
|
||||||
|
child: Icon(
|
||||||
|
Icons.edit,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const Padding(padding: EdgeInsets.all(8)),
|
const Padding(padding: EdgeInsets.all(8)),
|
||||||
Text(
|
Text(
|
||||||
_authState.userEmail,
|
"${_authState.firstName} ${_authState.lastName}",
|
||||||
style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4.0),
|
||||||
|
child: Text(
|
||||||
|
_authState.userEmail,
|
||||||
|
style: TextStyle(color: Colors.grey[800], fontSize: 12),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -97,7 +198,15 @@ class ProfileDrawer extends HookConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Card(
|
child: Card(
|
||||||
|
elevation: 0,
|
||||||
color: Colors.grey[100],
|
color: Colors.grey[100],
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5), // if you need this
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Color.fromARGB(101, 201, 201, 201),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
|
|||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'package:sliver_tools/sliver_tools.dart';
|
|
||||||
|
|
||||||
class HomePage extends HookConsumerWidget {
|
class HomePage extends HookConsumerWidget {
|
||||||
const HomePage({Key? key}) : super(key: key);
|
const HomePage({Key? key}) : super(key: key);
|
||||||
@@ -37,6 +36,19 @@ class HomePage extends HookConsumerWidget {
|
|||||||
ref.read(assetProvider.notifier).getAllAsset();
|
ref.read(assetProvider.notifier).getAllAsset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buildSelectedItemCountIndicator() {
|
||||||
|
return isMultiSelectEnable
|
||||||
|
? DisableMultiSelectButton(
|
||||||
|
onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
|
||||||
|
selectedItemCount: homePageState.selectedItems.length,
|
||||||
|
)
|
||||||
|
: Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildBottomAppBar() {
|
||||||
|
return isMultiSelectEnable ? const ControlBottomAppBar() : Container();
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
if (assetGroupByDateTime.isNotEmpty) {
|
if (assetGroupByDateTime.isNotEmpty) {
|
||||||
int? lastMonth;
|
int? lastMonth;
|
||||||
@@ -70,49 +82,51 @@ class HomePage extends HookConsumerWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buildSliverAppBar() {
|
||||||
|
return isMultiSelectEnable
|
||||||
|
? const SliverToBoxAdapter(
|
||||||
|
child: SizedBox(
|
||||||
|
height: 70,
|
||||||
|
child: null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ImmichSliverAppBar(
|
||||||
|
onPopBack: reloadAllAsset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: !isMultiSelectEnable,
|
bottom: !isMultiSelectEnable,
|
||||||
top: !isMultiSelectEnable,
|
top: !isMultiSelectEnable,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
DraggableScrollbar.semicircle(
|
CustomScrollView(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
slivers: [
|
||||||
controller: _scrollController,
|
_buildSliverAppBar(),
|
||||||
heightScrollThumb: 48.0,
|
],
|
||||||
child: CustomScrollView(
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 50.0),
|
||||||
|
child: DraggableScrollbar.semicircle(
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
slivers: [
|
heightScrollThumb: 48.0,
|
||||||
SliverAnimatedSwitcher(
|
child: CustomScrollView(
|
||||||
child: isMultiSelectEnable
|
controller: _scrollController,
|
||||||
? const SliverToBoxAdapter(
|
slivers: [
|
||||||
child: SizedBox(
|
..._imageGridGroup,
|
||||||
height: 70,
|
],
|
||||||
child: null,
|
),
|
||||||
),
|
|
||||||
)
|
|
||||||
: ImmichSliverAppBar(
|
|
||||||
onPopBack: reloadAllAsset,
|
|
||||||
),
|
|
||||||
duration: const Duration(milliseconds: 350),
|
|
||||||
),
|
|
||||||
..._imageGridGroup
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
isMultiSelectEnable
|
_buildSelectedItemCountIndicator(),
|
||||||
? DisableMultiSelectButton(
|
_buildBottomAppBar(),
|
||||||
onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
|
|
||||||
selectedItemCount: homePageState.selectedItems.length,
|
|
||||||
)
|
|
||||||
: Container(),
|
|
||||||
isMultiSelectEnable ? const ControlBottomAppBar() : Container(),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// key: _scaffoldKey,
|
|
||||||
drawer: const ProfileDrawer(),
|
drawer: const ProfileDrawer(),
|
||||||
body: _buildBody(),
|
body: _buildBody(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ class AuthenticationState {
|
|||||||
final String userId;
|
final String userId;
|
||||||
final String userEmail;
|
final String userEmail;
|
||||||
final bool isAuthenticated;
|
final bool isAuthenticated;
|
||||||
|
final String firstName;
|
||||||
|
final String lastName;
|
||||||
|
final bool isAdmin;
|
||||||
|
final bool isFirstLogin;
|
||||||
|
final String profileImagePath;
|
||||||
final DeviceInfoRemote deviceInfo;
|
final DeviceInfoRemote deviceInfo;
|
||||||
|
|
||||||
AuthenticationState({
|
AuthenticationState({
|
||||||
@@ -16,6 +21,11 @@ class AuthenticationState {
|
|||||||
required this.userId,
|
required this.userId,
|
||||||
required this.userEmail,
|
required this.userEmail,
|
||||||
required this.isAuthenticated,
|
required this.isAuthenticated,
|
||||||
|
required this.firstName,
|
||||||
|
required this.lastName,
|
||||||
|
required this.isAdmin,
|
||||||
|
required this.isFirstLogin,
|
||||||
|
required this.profileImagePath,
|
||||||
required this.deviceInfo,
|
required this.deviceInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -25,6 +35,11 @@ class AuthenticationState {
|
|||||||
String? userId,
|
String? userId,
|
||||||
String? userEmail,
|
String? userEmail,
|
||||||
bool? isAuthenticated,
|
bool? isAuthenticated,
|
||||||
|
String? firstName,
|
||||||
|
String? lastName,
|
||||||
|
bool? isAdmin,
|
||||||
|
bool? isFirstLoggedIn,
|
||||||
|
String? profileImagePath,
|
||||||
DeviceInfoRemote? deviceInfo,
|
DeviceInfoRemote? deviceInfo,
|
||||||
}) {
|
}) {
|
||||||
return AuthenticationState(
|
return AuthenticationState(
|
||||||
@@ -33,24 +48,36 @@ class AuthenticationState {
|
|||||||
userId: userId ?? this.userId,
|
userId: userId ?? this.userId,
|
||||||
userEmail: userEmail ?? this.userEmail,
|
userEmail: userEmail ?? this.userEmail,
|
||||||
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
|
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
|
||||||
|
firstName: firstName ?? this.firstName,
|
||||||
|
lastName: lastName ?? this.lastName,
|
||||||
|
isAdmin: isAdmin ?? this.isAdmin,
|
||||||
|
isFirstLogin: isFirstLoggedIn ?? isFirstLogin,
|
||||||
|
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||||
deviceInfo: deviceInfo ?? this.deviceInfo,
|
deviceInfo: deviceInfo ?? this.deviceInfo,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, deviceInfo: $deviceInfo)';
|
return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, firstName: $firstName, lastName: $lastName, isAdmin: $isAdmin, isFirstLoggedIn: $isFirstLogin, profileImagePath: $profileImagePath, deviceInfo: $deviceInfo)';
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
final result = <String, dynamic>{};
|
||||||
'deviceId': deviceId,
|
|
||||||
'deviceType': deviceType,
|
result.addAll({'deviceId': deviceId});
|
||||||
'userId': userId,
|
result.addAll({'deviceType': deviceType});
|
||||||
'userEmail': userEmail,
|
result.addAll({'userId': userId});
|
||||||
'isAuthenticated': isAuthenticated,
|
result.addAll({'userEmail': userEmail});
|
||||||
'deviceInfo': deviceInfo.toMap(),
|
result.addAll({'isAuthenticated': isAuthenticated});
|
||||||
};
|
result.addAll({'firstName': firstName});
|
||||||
|
result.addAll({'lastName': lastName});
|
||||||
|
result.addAll({'isAdmin': isAdmin});
|
||||||
|
result.addAll({'isFirstLogin': isFirstLogin});
|
||||||
|
result.addAll({'profileImagePath': profileImagePath});
|
||||||
|
result.addAll({'deviceInfo': deviceInfo.toMap()});
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
factory AuthenticationState.fromMap(Map<String, dynamic> map) {
|
factory AuthenticationState.fromMap(Map<String, dynamic> map) {
|
||||||
@@ -60,6 +87,11 @@ class AuthenticationState {
|
|||||||
userId: map['userId'] ?? '',
|
userId: map['userId'] ?? '',
|
||||||
userEmail: map['userEmail'] ?? '',
|
userEmail: map['userEmail'] ?? '',
|
||||||
isAuthenticated: map['isAuthenticated'] ?? false,
|
isAuthenticated: map['isAuthenticated'] ?? false,
|
||||||
|
firstName: map['firstName'] ?? '',
|
||||||
|
lastName: map['lastName'] ?? '',
|
||||||
|
isAdmin: map['isAdmin'] ?? false,
|
||||||
|
isFirstLogin: map['isFirstLogin'] ?? false,
|
||||||
|
profileImagePath: map['profileImagePath'] ?? '',
|
||||||
deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']),
|
deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -78,6 +110,11 @@ class AuthenticationState {
|
|||||||
other.userId == userId &&
|
other.userId == userId &&
|
||||||
other.userEmail == userEmail &&
|
other.userEmail == userEmail &&
|
||||||
other.isAuthenticated == isAuthenticated &&
|
other.isAuthenticated == isAuthenticated &&
|
||||||
|
other.firstName == firstName &&
|
||||||
|
other.lastName == lastName &&
|
||||||
|
other.isAdmin == isAdmin &&
|
||||||
|
other.isFirstLogin == isFirstLogin &&
|
||||||
|
other.profileImagePath == profileImagePath &&
|
||||||
other.deviceInfo == deviceInfo;
|
other.deviceInfo == deviceInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +125,11 @@ class AuthenticationState {
|
|||||||
userId.hashCode ^
|
userId.hashCode ^
|
||||||
userEmail.hashCode ^
|
userEmail.hashCode ^
|
||||||
isAuthenticated.hashCode ^
|
isAuthenticated.hashCode ^
|
||||||
|
firstName.hashCode ^
|
||||||
|
lastName.hashCode ^
|
||||||
|
isAdmin.hashCode ^
|
||||||
|
isFirstLogin.hashCode ^
|
||||||
|
profileImagePath.hashCode ^
|
||||||
deviceInfo.hashCode;
|
deviceInfo.hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:hive/hive.dart';
|
||||||
|
|
||||||
|
part 'hive_saved_login_info.model.g.dart';
|
||||||
|
|
||||||
|
@HiveType(typeId: 0)
|
||||||
|
class HiveSavedLoginInfo {
|
||||||
|
@HiveField(0)
|
||||||
|
String email;
|
||||||
|
|
||||||
|
@HiveField(1)
|
||||||
|
String password;
|
||||||
|
|
||||||
|
@HiveField(2)
|
||||||
|
String serverUrl;
|
||||||
|
|
||||||
|
@HiveField(3)
|
||||||
|
bool isSaveLogin;
|
||||||
|
|
||||||
|
HiveSavedLoginInfo({required this.email, required this.password, required this.serverUrl, required this.isSaveLogin});
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'hive_saved_login_info.model.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// TypeAdapterGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
class HiveSavedLoginInfoAdapter extends TypeAdapter<HiveSavedLoginInfo> {
|
||||||
|
@override
|
||||||
|
final int typeId = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
HiveSavedLoginInfo read(BinaryReader reader) {
|
||||||
|
final numOfFields = reader.readByte();
|
||||||
|
final fields = <int, dynamic>{
|
||||||
|
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||||
|
};
|
||||||
|
return HiveSavedLoginInfo(
|
||||||
|
email: fields[0] as String,
|
||||||
|
password: fields[1] as String,
|
||||||
|
serverUrl: fields[2] as String,
|
||||||
|
isSaveLogin: fields[3] as bool,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void write(BinaryWriter writer, HiveSavedLoginInfo obj) {
|
||||||
|
writer
|
||||||
|
..writeByte(4)
|
||||||
|
..writeByte(0)
|
||||||
|
..write(obj.email)
|
||||||
|
..writeByte(1)
|
||||||
|
..write(obj.password)
|
||||||
|
..writeByte(2)
|
||||||
|
..write(obj.serverUrl)
|
||||||
|
..writeByte(3)
|
||||||
|
..write(obj.isSaveLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => typeId.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is HiveSavedLoginInfoAdapter &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
typeId == other.typeId;
|
||||||
|
}
|
||||||
@@ -4,31 +4,58 @@ class LogInReponse {
|
|||||||
final String accessToken;
|
final String accessToken;
|
||||||
final String userId;
|
final String userId;
|
||||||
final String userEmail;
|
final String userEmail;
|
||||||
|
final String firstName;
|
||||||
|
final String lastName;
|
||||||
|
final String profileImagePath;
|
||||||
|
final bool isAdmin;
|
||||||
|
final bool isFirstLogin;
|
||||||
|
|
||||||
LogInReponse({
|
LogInReponse({
|
||||||
required this.accessToken,
|
required this.accessToken,
|
||||||
required this.userId,
|
required this.userId,
|
||||||
required this.userEmail,
|
required this.userEmail,
|
||||||
|
required this.firstName,
|
||||||
|
required this.lastName,
|
||||||
|
required this.profileImagePath,
|
||||||
|
required this.isAdmin,
|
||||||
|
required this.isFirstLogin,
|
||||||
});
|
});
|
||||||
|
|
||||||
LogInReponse copyWith({
|
LogInReponse copyWith({
|
||||||
String? accessToken,
|
String? accessToken,
|
||||||
String? userId,
|
String? userId,
|
||||||
String? userEmail,
|
String? userEmail,
|
||||||
|
String? firstName,
|
||||||
|
String? lastName,
|
||||||
|
String? profileImagePath,
|
||||||
|
bool? isAdmin,
|
||||||
|
bool? isFirstLogin,
|
||||||
}) {
|
}) {
|
||||||
return LogInReponse(
|
return LogInReponse(
|
||||||
accessToken: accessToken ?? this.accessToken,
|
accessToken: accessToken ?? this.accessToken,
|
||||||
userId: userId ?? this.userId,
|
userId: userId ?? this.userId,
|
||||||
userEmail: userEmail ?? this.userEmail,
|
userEmail: userEmail ?? this.userEmail,
|
||||||
|
firstName: firstName ?? this.firstName,
|
||||||
|
lastName: lastName ?? this.lastName,
|
||||||
|
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||||
|
isAdmin: isAdmin ?? this.isAdmin,
|
||||||
|
isFirstLogin: isFirstLogin ?? this.isFirstLogin,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
final result = <String, dynamic>{};
|
||||||
'accessToken': accessToken,
|
|
||||||
'userId': userId,
|
result.addAll({'accessToken': accessToken});
|
||||||
'userEmail': userEmail,
|
result.addAll({'userId': userId});
|
||||||
};
|
result.addAll({'userEmail': userEmail});
|
||||||
|
result.addAll({'firstName': firstName});
|
||||||
|
result.addAll({'lastName': lastName});
|
||||||
|
result.addAll({'profileImagePath': profileImagePath});
|
||||||
|
result.addAll({'isAdmin': isAdmin});
|
||||||
|
result.addAll({'isFirstLogin': isFirstLogin});
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
factory LogInReponse.fromMap(Map<String, dynamic> map) {
|
factory LogInReponse.fromMap(Map<String, dynamic> map) {
|
||||||
@@ -36,6 +63,11 @@ class LogInReponse {
|
|||||||
accessToken: map['accessToken'] ?? '',
|
accessToken: map['accessToken'] ?? '',
|
||||||
userId: map['userId'] ?? '',
|
userId: map['userId'] ?? '',
|
||||||
userEmail: map['userEmail'] ?? '',
|
userEmail: map['userEmail'] ?? '',
|
||||||
|
firstName: map['firstName'] ?? '',
|
||||||
|
lastName: map['lastName'] ?? '',
|
||||||
|
profileImagePath: map['profileImagePath'] ?? '',
|
||||||
|
isAdmin: map['isAdmin'] ?? false,
|
||||||
|
isFirstLogin: map['isFirstLogin'] ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +76,9 @@ class LogInReponse {
|
|||||||
factory LogInReponse.fromJson(String source) => LogInReponse.fromMap(json.decode(source));
|
factory LogInReponse.fromJson(String source) => LogInReponse.fromMap(json.decode(source));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail)';
|
String toString() {
|
||||||
|
return 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail, firstName: $firstName, lastName: $lastName, profileImagePath: $profileImagePath, isAdmin: $isAdmin, isFirstLogin: $isFirstLogin)';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
@@ -53,9 +87,23 @@ class LogInReponse {
|
|||||||
return other is LogInReponse &&
|
return other is LogInReponse &&
|
||||||
other.accessToken == accessToken &&
|
other.accessToken == accessToken &&
|
||||||
other.userId == userId &&
|
other.userId == userId &&
|
||||||
other.userEmail == userEmail;
|
other.userEmail == userEmail &&
|
||||||
|
other.firstName == firstName &&
|
||||||
|
other.lastName == lastName &&
|
||||||
|
other.profileImagePath == profileImagePath &&
|
||||||
|
other.isAdmin == isAdmin &&
|
||||||
|
other.isFirstLogin == isFirstLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => accessToken.hashCode ^ userId.hashCode ^ userEmail.hashCode;
|
int get hashCode {
|
||||||
|
return accessToken.hashCode ^
|
||||||
|
userId.hashCode ^
|
||||||
|
userEmail.hashCode ^
|
||||||
|
firstName.hashCode ^
|
||||||
|
lastName.hashCode ^
|
||||||
|
profileImagePath.hashCode ^
|
||||||
|
isAdmin.hashCode ^
|
||||||
|
isFirstLogin.hashCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ 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/login_response.model.dart';
|
import 'package:immich_mobile/modules/login/models/login_response.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/backup.service.dart';
|
import 'package:immich_mobile/modules/backup/services/backup.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:immich_mobile/shared/services/network.service.dart';
|
||||||
import 'package:immich_mobile/shared/models/device_info.model.dart';
|
import 'package:immich_mobile/shared/models/device_info.model.dart';
|
||||||
@@ -16,9 +17,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
AuthenticationState(
|
AuthenticationState(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
deviceType: "",
|
deviceType: "",
|
||||||
isAuthenticated: false,
|
|
||||||
userId: "",
|
userId: "",
|
||||||
userEmail: "",
|
userEmail: "",
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
profileImagePath: '',
|
||||||
|
isAdmin: false,
|
||||||
|
isFirstLogin: false,
|
||||||
|
isAuthenticated: false,
|
||||||
deviceInfo: DeviceInfoRemote(
|
deviceInfo: DeviceInfoRemote(
|
||||||
id: 0,
|
id: 0,
|
||||||
userId: "",
|
userId: "",
|
||||||
@@ -36,7 +42,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
final BackupService _backupService = BackupService();
|
final BackupService _backupService = BackupService();
|
||||||
final NetworkService _networkService = NetworkService();
|
final NetworkService _networkService = NetworkService();
|
||||||
|
|
||||||
Future<bool> login(String email, String password, String serverEndpoint) async {
|
Future<bool> login(String email, String password, String serverEndpoint, bool isSavedLoginInfo) async {
|
||||||
// Store server endpoint to Hive and test endpoint
|
// Store server endpoint to Hive and test endpoint
|
||||||
if (serverEndpoint[serverEndpoint.length - 1] == "/") {
|
if (serverEndpoint[serverEndpoint.length - 1] == "/") {
|
||||||
var validUrl = serverEndpoint.substring(0, serverEndpoint.length - 1);
|
var validUrl = serverEndpoint.substring(0, serverEndpoint.length - 1);
|
||||||
@@ -75,7 +81,26 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
userId: payload.userId,
|
userId: payload.userId,
|
||||||
userEmail: payload.userEmail,
|
userEmail: payload.userEmail,
|
||||||
|
firstName: payload.firstName,
|
||||||
|
lastName: payload.lastName,
|
||||||
|
profileImagePath: payload.profileImagePath,
|
||||||
|
isAdmin: payload.isAdmin,
|
||||||
|
isFirstLoggedIn: payload.isFirstLogin,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isSavedLoginInfo) {
|
||||||
|
// Save login info to local storage
|
||||||
|
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).put(
|
||||||
|
savedLoginInfoKey,
|
||||||
|
HiveSavedLoginInfo(
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
isSaveLogin: true,
|
||||||
|
serverUrl: Hive.box(userInfoBox).get(serverEndpointKey)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).delete(savedLoginInfoKey);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -99,9 +124,14 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
state = AuthenticationState(
|
state = AuthenticationState(
|
||||||
deviceId: "",
|
deviceId: "",
|
||||||
deviceType: "",
|
deviceType: "",
|
||||||
isAuthenticated: false,
|
|
||||||
userId: "",
|
userId: "",
|
||||||
userEmail: "",
|
userEmail: "",
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
profileImagePath: '',
|
||||||
|
isFirstLogin: false,
|
||||||
|
isAuthenticated: false,
|
||||||
|
isAdmin: false,
|
||||||
deviceInfo: DeviceInfoRemote(
|
deviceInfo: DeviceInfoRemote(
|
||||||
id: 0,
|
id: 0,
|
||||||
userId: "",
|
userId: "",
|
||||||
@@ -124,6 +154,10 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||||||
DeviceInfoRemote deviceInfoRemote = await _backupService.setAutoBackup(backupState, deviceId, deviceType);
|
DeviceInfoRemote deviceInfoRemote = await _backupService.setAutoBackup(backupState, deviceId, deviceType);
|
||||||
state = state.copyWith(deviceInfo: deviceInfoRemote);
|
state = state.copyWith(deviceInfo: deviceInfoRemote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateUserProfileImagePath(String path) {
|
||||||
|
state = state.copyWith(profileImagePath: path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
|
final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/backup.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
|
||||||
class LoginForm extends HookConsumerWidget {
|
class LoginForm extends HookConsumerWidget {
|
||||||
@@ -12,22 +15,36 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final usernameController = useTextEditingController(text: 'testuser@email.com');
|
final usernameController = useTextEditingController.fromValue(TextEditingValue.empty);
|
||||||
final passwordController = useTextEditingController(text: 'password');
|
final passwordController = useTextEditingController.fromValue(TextEditingValue.empty);
|
||||||
final serverEndpointController = useTextEditingController(text: 'http://192.168.1.216:2283');
|
final serverEndpointController = useTextEditingController(text: 'http://your-server-ip:2283');
|
||||||
|
final isSaveLoginInfo = useState<bool>(false);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
var loginInfo = Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).get(savedLoginInfoKey);
|
||||||
|
|
||||||
|
if (loginInfo != null) {
|
||||||
|
usernameController.text = loginInfo.email;
|
||||||
|
passwordController.text = loginInfo.password;
|
||||||
|
serverEndpointController.text = loginInfo.serverUrl;
|
||||||
|
isSaveLoginInfo.value = loginInfo.isSaveLogin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 300),
|
constraints: const BoxConstraints(maxWidth: 300),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 32,
|
spacing: 16,
|
||||||
runSpacing: 32,
|
runSpacing: 16,
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Image(
|
const Image(
|
||||||
image: AssetImage('assets/immich-logo-no-outline.png'),
|
image: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
width: 128,
|
width: 100,
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -42,10 +59,29 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
EmailInput(controller: usernameController),
|
EmailInput(controller: usernameController),
|
||||||
PasswordInput(controller: passwordController),
|
PasswordInput(controller: passwordController),
|
||||||
ServerEndpointInput(controller: serverEndpointController),
|
ServerEndpointInput(controller: serverEndpointController),
|
||||||
|
CheckboxListTile(
|
||||||
|
activeColor: Theme.of(context).primaryColor,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
dense: true,
|
||||||
|
side: const BorderSide(color: Colors.grey, width: 1.5),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)),
|
||||||
|
enableFeedback: true,
|
||||||
|
title: const Text(
|
||||||
|
"Save login",
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey),
|
||||||
|
),
|
||||||
|
value: isSaveLoginInfo.value,
|
||||||
|
onChanged: (switchValue) {
|
||||||
|
if (switchValue != null) {
|
||||||
|
isSaveLoginInfo.value = switchValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
LoginButton(
|
LoginButton(
|
||||||
emailController: usernameController,
|
emailController: usernameController,
|
||||||
passwordController: passwordController,
|
passwordController: passwordController,
|
||||||
serverEndpointController: serverEndpointController,
|
serverEndpointController: serverEndpointController,
|
||||||
|
isSavedLoginInfo: isSaveLoginInfo.value,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -104,29 +140,34 @@ class LoginButton extends ConsumerWidget {
|
|||||||
final TextEditingController emailController;
|
final TextEditingController emailController;
|
||||||
final TextEditingController passwordController;
|
final TextEditingController passwordController;
|
||||||
final TextEditingController serverEndpointController;
|
final TextEditingController serverEndpointController;
|
||||||
|
final bool isSavedLoginInfo;
|
||||||
|
|
||||||
const LoginButton(
|
const LoginButton({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.emailController,
|
required this.emailController,
|
||||||
required this.passwordController,
|
required this.passwordController,
|
||||||
required this.serverEndpointController})
|
required this.serverEndpointController,
|
||||||
: super(key: key);
|
required this.isSavedLoginInfo,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
|
style: ButtonStyle(
|
||||||
|
visualDensity: VisualDensity.standard,
|
||||||
|
padding: MaterialStateProperty.all<EdgeInsets>(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 = await ref
|
||||||
.read(authenticationProvider.notifier)
|
.read(authenticationProvider.notifier)
|
||||||
.login(emailController.text, passwordController.text, serverEndpointController.text);
|
.login(emailController.text, passwordController.text, serverEndpointController.text, isSavedLoginInfo);
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
// Resume backup (if enable) then navigate
|
// Resume backup (if enable) then navigate
|
||||||
ref.watch(backupProvider.notifier).resumeBackup();
|
ref.watch(backupProvider.notifier).resumeBackup();
|
||||||
// AutoRouter.of(context).pushNamed("/home-page");
|
|
||||||
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
AutoRouter.of(context).pushNamed("/tab-controller-page");
|
||||||
} else {
|
} else {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
@@ -136,6 +177,9 @@ class LoginButton extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text("Login"));
|
child: const Text(
|
||||||
|
"Login",
|
||||||
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class AlbumViewerPageState {
|
||||||
|
final bool isEditAlbum;
|
||||||
|
final String editTitleText;
|
||||||
|
AlbumViewerPageState({
|
||||||
|
required this.isEditAlbum,
|
||||||
|
required this.editTitleText,
|
||||||
|
});
|
||||||
|
|
||||||
|
AlbumViewerPageState copyWith({
|
||||||
|
bool? isEditAlbum,
|
||||||
|
String? editTitleText,
|
||||||
|
}) {
|
||||||
|
return AlbumViewerPageState(
|
||||||
|
isEditAlbum: isEditAlbum ?? this.isEditAlbum,
|
||||||
|
editTitleText: editTitleText ?? this.editTitleText,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
|
result.addAll({'isEditAlbum': isEditAlbum});
|
||||||
|
result.addAll({'editTitleText': editTitleText});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory AlbumViewerPageState.fromMap(Map<String, dynamic> map) {
|
||||||
|
return AlbumViewerPageState(
|
||||||
|
isEditAlbum: map['isEditAlbum'] ?? false,
|
||||||
|
editTitleText: map['editTitleText'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory AlbumViewerPageState.fromJson(String source) => AlbumViewerPageState.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'AlbumViewerPageState(isEditAlbum: $isEditAlbum, editTitleText: $editTitleText)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is AlbumViewerPageState && other.isEditAlbum == isEditAlbum && other.editTitleText == editTitleText;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => isEditAlbum.hashCode ^ editTitleText.hashCode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/sharing/models/album_viewer_page_state.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
||||||
|
|
||||||
|
class AlbumViewerNotifier extends StateNotifier<AlbumViewerPageState> {
|
||||||
|
AlbumViewerNotifier(this.ref) : super(AlbumViewerPageState(editTitleText: "", isEditAlbum: false));
|
||||||
|
|
||||||
|
final Ref ref;
|
||||||
|
|
||||||
|
void enableEditAlbum() {
|
||||||
|
state = state.copyWith(isEditAlbum: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void disableEditAlbum() {
|
||||||
|
state = state.copyWith(isEditAlbum: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setEditTitleText(String newTitle) {
|
||||||
|
state = state.copyWith(editTitleText: newTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void remoteEditTitleText() {
|
||||||
|
state = state.copyWith(editTitleText: "");
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetState() {
|
||||||
|
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> changeAlbumTitle(String albumId, String ownerId, String newAlbumTitle) async {
|
||||||
|
SharedAlbumService service = SharedAlbumService();
|
||||||
|
|
||||||
|
bool isSuccess = await service.changeTitleAlbum(albumId, ownerId, newAlbumTitle);
|
||||||
|
|
||||||
|
if (isSuccess) {
|
||||||
|
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
||||||
|
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = state.copyWith(editTitleText: "", isEditAlbum: false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final albumViewerProvider = StateNotifierProvider<AlbumViewerNotifier, AlbumViewerPageState>((ref) {
|
||||||
|
return AlbumViewerNotifier(ref);
|
||||||
|
});
|
||||||
@@ -138,4 +138,23 @@ class SharedAlbumService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> changeTitleAlbum(String albumId, String ownerId, String newAlbumTitle) async {
|
||||||
|
try {
|
||||||
|
Response res = await _networkService.patchRequest(url: 'shared/updateInfo', data: {
|
||||||
|
"albumId": albumId,
|
||||||
|
"ownerId": ownerId,
|
||||||
|
"albumName": newAlbumTitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.statusCode != 200) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error deleteAlbum ${e.toString()}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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/models/shared_album.model.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';
|
||||||
@@ -27,6 +28,8 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable;
|
final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable;
|
||||||
final selectedAssetsInAlbum = ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
final selectedAssetsInAlbum = ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
|
||||||
|
final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
|
||||||
|
final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
|
||||||
|
|
||||||
void _onDeleteAlbumPressed(String albumId) async {
|
void _onDeleteAlbumPressed(String albumId) async {
|
||||||
ImmichLoadingOverlayController.appLoader.show();
|
ImmichLoadingOverlayController.appLoader.show();
|
||||||
@@ -135,11 +138,13 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
isScrollControlled: false,
|
isScrollControlled: false,
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Column(
|
return SafeArea(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
_buildBottomSheetActionButton(),
|
children: [
|
||||||
],
|
_buildBottomSheetActionButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -152,6 +157,24 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
|
|||||||
icon: const Icon(Icons.close_rounded),
|
icon: const Icon(Icons.close_rounded),
|
||||||
splashRadius: 25,
|
splashRadius: 25,
|
||||||
);
|
);
|
||||||
|
} else if (isEditAlbum) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
bool isSuccess =
|
||||||
|
await ref.watch(albumViewerProvider.notifier).changeAlbumTitle(albumId, userId, newAlbumTitle);
|
||||||
|
|
||||||
|
if (!isSuccess) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "Failed to change album title",
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.check_rounded),
|
||||||
|
splashRadius: 25,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
onPressed: () async => await AutoRouter.of(context).pop(),
|
onPressed: () async => await AutoRouter.of(context).pop(),
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/sharing/models/shared_album.model.dart';
|
||||||
|
import 'package:immich_mobile/modules/sharing/providers/album_viewer.provider.dart';
|
||||||
|
|
||||||
|
class AlbumViewerEditableTitle extends HookConsumerWidget {
|
||||||
|
final SharedAlbum albumInfo;
|
||||||
|
final FocusNode titleFocusNode;
|
||||||
|
const AlbumViewerEditableTitle({Key? key, required this.albumInfo, required this.titleFocusNode}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final titleTextEditController = useTextEditingController(text: albumInfo.albumName);
|
||||||
|
|
||||||
|
void onFocusModeChange() {
|
||||||
|
if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
|
||||||
|
ref.watch(albumViewerProvider.notifier).setEditTitleText("Untitled");
|
||||||
|
titleTextEditController.text = "Untitled";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
titleFocusNode.addListener(onFocusModeChange);
|
||||||
|
return () {
|
||||||
|
titleFocusNode.removeListener(onFocusModeChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return TextField(
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value.isEmpty) {
|
||||||
|
} else {
|
||||||
|
ref.watch(albumViewerProvider.notifier).setEditTitleText(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focusNode: titleFocusNode,
|
||||||
|
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
|
controller: titleTextEditController,
|
||||||
|
onTap: () {
|
||||||
|
FocusScope.of(context).requestFocus(titleFocusNode);
|
||||||
|
|
||||||
|
ref.watch(albumViewerProvider.notifier).setEditTitleText(albumInfo.albumName);
|
||||||
|
ref.watch(albumViewerProvider.notifier).enableEditAlbum();
|
||||||
|
|
||||||
|
if (titleTextEditController.text == 'Untitled') {
|
||||||
|
titleTextEditController.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
suffixIcon: titleFocusNode.hasFocus
|
||||||
|
? IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
titleTextEditController.clear();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.cancel_rounded),
|
||||||
|
splashRadius: 10,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderSide: const BorderSide(color: Colors.transparent),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderSide: const BorderSide(color: Colors.transparent),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
focusColor: Colors.grey[300],
|
||||||
|
fillColor: Colors.grey[200],
|
||||||
|
filled: titleFocusNode.hasFocus,
|
||||||
|
hintText: 'Add a title',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import 'package:immich_mobile/modules/sharing/providers/shared_album.provider.da
|
|||||||
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
import 'package:immich_mobile/modules/sharing/services/shared_album.service.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/album_action_outlined_button.dart';
|
import 'package:immich_mobile/modules/sharing/ui/album_action_outlined_button.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/album_viewer_appbar.dart';
|
import 'package:immich_mobile/modules/sharing/ui/album_viewer_appbar.dart';
|
||||||
|
import 'package:immich_mobile/modules/sharing/ui/album_viewer_editable_title.dart';
|
||||||
import 'package:immich_mobile/modules/sharing/ui/album_viewer_thumbnail.dart';
|
import 'package:immich_mobile/modules/sharing/ui/album_viewer_thumbnail.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
@@ -26,6 +27,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
FocusNode titleFocusNode = useFocusNode();
|
||||||
ScrollController _scrollController = useScrollController();
|
ScrollController _scrollController = useScrollController();
|
||||||
AsyncValue<SharedAlbum> _albumInfo = ref.watch(sharedAlbumDetailProvider(albumId));
|
AsyncValue<SharedAlbum> _albumInfo = ref.watch(sharedAlbumDetailProvider(albumId));
|
||||||
|
|
||||||
@@ -83,13 +85,18 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTitle(String title) {
|
Widget _buildTitle(SharedAlbum albumInfo) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(left: 16.0, top: 16),
|
padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
|
||||||
child: Text(
|
child: userId == albumInfo.ownerId
|
||||||
title,
|
? AlbumViewerEditableTitle(
|
||||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
albumInfo: albumInfo,
|
||||||
),
|
titleFocusNode: titleFocusNode,
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
|
child: Text(albumInfo.albumName, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +131,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildTitle(albumInfo.albumName),
|
_buildTitle(albumInfo),
|
||||||
_buildAlbumDateRange(albumInfo),
|
_buildAlbumDateRange(albumInfo),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 60,
|
height: 60,
|
||||||
@@ -204,8 +211,11 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody(SharedAlbum albumInfo) {
|
Widget _buildBody(SharedAlbum albumInfo) {
|
||||||
return Stack(children: [
|
return GestureDetector(
|
||||||
DraggableScrollbar.semicircle(
|
onTap: () {
|
||||||
|
titleFocusNode.unfocus();
|
||||||
|
},
|
||||||
|
child: DraggableScrollbar.semicircle(
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
heightScrollThumb: 48.0,
|
heightScrollThumb: 48.0,
|
||||||
@@ -228,7 +238,7 @@ class AlbumViewerPage extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
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:immich_mobile/modules/backup/views/album_preview_page.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/views/backup_album_selection_page.dart';
|
||||||
import 'package:immich_mobile/modules/login/views/login_page.dart';
|
import 'package:immich_mobile/modules/login/views/login_page.dart';
|
||||||
import 'package:immich_mobile/modules/home/views/home_page.dart';
|
import 'package:immich_mobile/modules/home/views/home_page.dart';
|
||||||
import 'package:immich_mobile/modules/search/views/search_page.dart';
|
import 'package:immich_mobile/modules/search/views/search_page.dart';
|
||||||
@@ -14,10 +16,11 @@ import 'package:immich_mobile/modules/sharing/views/select_user_for_sharing_page
|
|||||||
import 'package:immich_mobile/modules/sharing/views/sharing_page.dart';
|
import 'package:immich_mobile/modules/sharing/views/sharing_page.dart';
|
||||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
|
||||||
import 'package:immich_mobile/shared/views/backup_controller_page.dart';
|
import 'package:immich_mobile/modules/backup/views/backup_controller_page.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/image_viewer_page.dart';
|
||||||
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
import 'package:immich_mobile/shared/views/tab_controller_page.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||||
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
part 'router.gr.dart';
|
part 'router.gr.dart';
|
||||||
|
|
||||||
@@ -55,6 +58,8 @@ part 'router.gr.dart';
|
|||||||
guards: [AuthGuard],
|
guards: [AuthGuard],
|
||||||
transitionsBuilder: TransitionsBuilders.slideBottom,
|
transitionsBuilder: TransitionsBuilders.slideBottom,
|
||||||
),
|
),
|
||||||
|
AutoRoute(page: BackupAlbumSelectionPage, guards: [AuthGuard]),
|
||||||
|
AutoRoute(page: AlbumPreviewPage, guards: [AuthGuard]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
class AppRouter extends _$AppRouter {
|
class AppRouter extends _$AppRouter {
|
||||||
|
|||||||
@@ -93,6 +93,16 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
opaque: true,
|
opaque: true,
|
||||||
barrierDismissible: false);
|
barrierDismissible: false);
|
||||||
},
|
},
|
||||||
|
BackupAlbumSelectionRoute.name: (routeData) {
|
||||||
|
return MaterialPageX<dynamic>(
|
||||||
|
routeData: routeData, child: const BackupAlbumSelectionPage());
|
||||||
|
},
|
||||||
|
AlbumPreviewRoute.name: (routeData) {
|
||||||
|
final args = routeData.argsAs<AlbumPreviewRouteArgs>();
|
||||||
|
return MaterialPageX<dynamic>(
|
||||||
|
routeData: routeData,
|
||||||
|
child: AlbumPreviewPage(key: args.key, album: args.album));
|
||||||
|
},
|
||||||
HomeRoute.name: (routeData) {
|
HomeRoute.name: (routeData) {
|
||||||
return MaterialPageX<dynamic>(
|
return MaterialPageX<dynamic>(
|
||||||
routeData: routeData, child: const HomePage());
|
routeData: routeData, child: const HomePage());
|
||||||
@@ -149,7 +159,11 @@ class _$AppRouter extends RootStackRouter {
|
|||||||
path: '/album-viewer-page', guards: [authGuard]),
|
path: '/album-viewer-page', guards: [authGuard]),
|
||||||
RouteConfig(SelectAdditionalUserForSharingRoute.name,
|
RouteConfig(SelectAdditionalUserForSharingRoute.name,
|
||||||
path: '/select-additional-user-for-sharing-page',
|
path: '/select-additional-user-for-sharing-page',
|
||||||
guards: [authGuard])
|
guards: [authGuard]),
|
||||||
|
RouteConfig(BackupAlbumSelectionRoute.name,
|
||||||
|
path: '/backup-album-selection-page', guards: [authGuard]),
|
||||||
|
RouteConfig(AlbumPreviewRoute.name,
|
||||||
|
path: '/album-preview-page', guards: [authGuard])
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +372,40 @@ class SelectAdditionalUserForSharingRouteArgs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [BackupAlbumSelectionPage]
|
||||||
|
class BackupAlbumSelectionRoute extends PageRouteInfo<void> {
|
||||||
|
const BackupAlbumSelectionRoute()
|
||||||
|
: super(BackupAlbumSelectionRoute.name,
|
||||||
|
path: '/backup-album-selection-page');
|
||||||
|
|
||||||
|
static const String name = 'BackupAlbumSelectionRoute';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [AlbumPreviewPage]
|
||||||
|
class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> {
|
||||||
|
AlbumPreviewRoute({Key? key, required AssetPathEntity album})
|
||||||
|
: super(AlbumPreviewRoute.name,
|
||||||
|
path: '/album-preview-page',
|
||||||
|
args: AlbumPreviewRouteArgs(key: key, album: album));
|
||||||
|
|
||||||
|
static const String name = 'AlbumPreviewRoute';
|
||||||
|
}
|
||||||
|
|
||||||
|
class AlbumPreviewRouteArgs {
|
||||||
|
const AlbumPreviewRouteArgs({this.key, required this.album});
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
final AssetPathEntity album;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AlbumPreviewRouteArgs{key: $key, album: $album}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [HomePage]
|
/// [HomePage]
|
||||||
class HomeRoute extends PageRouteInfo<void> {
|
class HomeRoute extends PageRouteInfo<void> {
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
|
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
|
||||||
|
|
||||||
enum BackUpProgressEnum { idle, inProgress, done }
|
|
||||||
|
|
||||||
class BackUpState {
|
|
||||||
final BackUpProgressEnum backupProgress;
|
|
||||||
final int totalAssetCount;
|
|
||||||
final int assetOnDatabase;
|
|
||||||
final int backingUpAssetCount;
|
|
||||||
final double progressInPercentage;
|
|
||||||
final CancelToken cancelToken;
|
|
||||||
final ServerInfo serverInfo;
|
|
||||||
|
|
||||||
BackUpState({
|
|
||||||
required this.backupProgress,
|
|
||||||
required this.totalAssetCount,
|
|
||||||
required this.assetOnDatabase,
|
|
||||||
required this.backingUpAssetCount,
|
|
||||||
required this.progressInPercentage,
|
|
||||||
required this.cancelToken,
|
|
||||||
required this.serverInfo,
|
|
||||||
});
|
|
||||||
|
|
||||||
BackUpState copyWith({
|
|
||||||
BackUpProgressEnum? backupProgress,
|
|
||||||
int? totalAssetCount,
|
|
||||||
int? assetOnDatabase,
|
|
||||||
int? backingUpAssetCount,
|
|
||||||
double? progressInPercentage,
|
|
||||||
CancelToken? cancelToken,
|
|
||||||
ServerInfo? serverInfo,
|
|
||||||
}) {
|
|
||||||
return BackUpState(
|
|
||||||
backupProgress: backupProgress ?? this.backupProgress,
|
|
||||||
totalAssetCount: totalAssetCount ?? this.totalAssetCount,
|
|
||||||
assetOnDatabase: assetOnDatabase ?? this.assetOnDatabase,
|
|
||||||
backingUpAssetCount: backingUpAssetCount ?? this.backingUpAssetCount,
|
|
||||||
progressInPercentage: progressInPercentage ?? this.progressInPercentage,
|
|
||||||
cancelToken: cancelToken ?? this.cancelToken,
|
|
||||||
serverInfo: serverInfo ?? this.serverInfo,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'BackUpState(backupProgress: $backupProgress, totalAssetCount: $totalAssetCount, assetOnDatabase: $assetOnDatabase, backingUpAssetCount: $backingUpAssetCount, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo)';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
|
|
||||||
return other is BackUpState &&
|
|
||||||
other.backupProgress == backupProgress &&
|
|
||||||
other.totalAssetCount == totalAssetCount &&
|
|
||||||
other.assetOnDatabase == assetOnDatabase &&
|
|
||||||
other.backingUpAssetCount == backingUpAssetCount &&
|
|
||||||
other.progressInPercentage == progressInPercentage &&
|
|
||||||
other.cancelToken == cancelToken &&
|
|
||||||
other.serverInfo == serverInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode {
|
|
||||||
return backupProgress.hashCode ^
|
|
||||||
totalAssetCount.hashCode ^
|
|
||||||
assetOnDatabase.hashCode ^
|
|
||||||
backingUpAssetCount.hashCode ^
|
|
||||||
progressInPercentage.hashCode ^
|
|
||||||
cancelToken.hashCode ^
|
|
||||||
serverInfo.hashCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
class UploadProfileImageResponse {
|
||||||
|
final String userId;
|
||||||
|
final String profileImagePath;
|
||||||
|
UploadProfileImageResponse({
|
||||||
|
required this.userId,
|
||||||
|
required this.profileImagePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
UploadProfileImageResponse copyWith({
|
||||||
|
String? userId,
|
||||||
|
String? profileImagePath,
|
||||||
|
}) {
|
||||||
|
return UploadProfileImageResponse(
|
||||||
|
userId: userId ?? this.userId,
|
||||||
|
profileImagePath: profileImagePath ?? this.profileImagePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
final result = <String, dynamic>{};
|
||||||
|
|
||||||
|
result.addAll({'userId': userId});
|
||||||
|
result.addAll({'profileImagePath': profileImagePath});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory UploadProfileImageResponse.fromMap(Map<String, dynamic> map) {
|
||||||
|
return UploadProfileImageResponse(
|
||||||
|
userId: map['userId'] ?? '',
|
||||||
|
profileImagePath: map['profileImagePath'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory UploadProfileImageResponse.fromJson(String source) => UploadProfileImageResponse.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'UploadProfileImageReponse(userId: $userId, profileImagePath: $profileImagePath)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other is UploadProfileImageResponse && other.userId == userId && other.profileImagePath == profileImagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => userId.hashCode ^ profileImagePath.hashCode;
|
||||||
|
}
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:dio/dio.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/modules/login/providers/authentication.provider.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/backup_state.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
|
|
||||||
import 'package:immich_mobile/shared/services/backup.service.dart';
|
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
|
||||||
|
|
||||||
class BackupNotifier extends StateNotifier<BackUpState> {
|
|
||||||
BackupNotifier({this.ref})
|
|
||||||
: super(
|
|
||||||
BackUpState(
|
|
||||||
backupProgress: BackUpProgressEnum.idle,
|
|
||||||
backingUpAssetCount: 0,
|
|
||||||
assetOnDatabase: 0,
|
|
||||||
totalAssetCount: 0,
|
|
||||||
progressInPercentage: 0,
|
|
||||||
cancelToken: CancelToken(),
|
|
||||||
serverInfo: ServerInfo(
|
|
||||||
diskAvailable: "0",
|
|
||||||
diskAvailableRaw: 0,
|
|
||||||
diskSize: "0",
|
|
||||||
diskSizeRaw: 0,
|
|
||||||
diskUsagePercentage: 0.0,
|
|
||||||
diskUse: "0",
|
|
||||||
diskUseRaw: 0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ref? ref;
|
|
||||||
final BackupService _backupService = BackupService();
|
|
||||||
final ServerInfoService _serverInfoService = ServerInfoService();
|
|
||||||
final StreamController _onAssetBackupStreamCtrl =
|
|
||||||
StreamController.broadcast();
|
|
||||||
|
|
||||||
void getBackupInfo() async {
|
|
||||||
_updateServerInfo();
|
|
||||||
|
|
||||||
List<AssetPathEntity> list = await PhotoManager.getAssetPathList(
|
|
||||||
onlyAll: true, type: RequestType.common);
|
|
||||||
List<String> didBackupAsset = await _backupService.getDeviceBackupAsset();
|
|
||||||
|
|
||||||
if (list.isEmpty) {
|
|
||||||
debugPrint("No Asset On Device");
|
|
||||||
state = state.copyWith(
|
|
||||||
backupProgress: BackUpProgressEnum.idle,
|
|
||||||
totalAssetCount: 0,
|
|
||||||
assetOnDatabase: didBackupAsset.length);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int totalAsset = list[0].assetCount;
|
|
||||||
|
|
||||||
state = state.copyWith(
|
|
||||||
totalAssetCount: totalAsset, assetOnDatabase: didBackupAsset.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
void startBackupProcess() async {
|
|
||||||
_updateServerInfo();
|
|
||||||
|
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
|
|
||||||
|
|
||||||
var authResult = await PhotoManager.requestPermissionExtend();
|
|
||||||
if (authResult.isAuth) {
|
|
||||||
await PhotoManager.clearFileCache();
|
|
||||||
// await PhotoManager.presentLimited();
|
|
||||||
// Gather assets info
|
|
||||||
List<AssetPathEntity> list = await PhotoManager.getAssetPathList(
|
|
||||||
hasAll: true, onlyAll: true, type: RequestType.common);
|
|
||||||
|
|
||||||
// Get device assets info from database
|
|
||||||
// Compare and find different assets that has not been backing up
|
|
||||||
// Backup those assets
|
|
||||||
List<String> backupAsset = await _backupService.getDeviceBackupAsset();
|
|
||||||
|
|
||||||
if (list.isEmpty) {
|
|
||||||
debugPrint("No Asset On Device - Abort Backup Process");
|
|
||||||
state = state.copyWith(
|
|
||||||
backupProgress: BackUpProgressEnum.idle,
|
|
||||||
totalAssetCount: 0,
|
|
||||||
assetOnDatabase: backupAsset.length);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int totalAsset = list[0].assetCount;
|
|
||||||
List<AssetEntity> currentAssets =
|
|
||||||
await list[0].getAssetListRange(start: 0, end: totalAsset);
|
|
||||||
|
|
||||||
state = state.copyWith(
|
|
||||||
totalAssetCount: totalAsset, assetOnDatabase: backupAsset.length);
|
|
||||||
// Remove item that has already been backed up
|
|
||||||
for (var backupAssetId in backupAsset) {
|
|
||||||
currentAssets.removeWhere((e) => e.id == backupAssetId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentAssets.isEmpty) {
|
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
|
|
||||||
}
|
|
||||||
|
|
||||||
state = state.copyWith(backingUpAssetCount: currentAssets.length);
|
|
||||||
|
|
||||||
// Perform Backup
|
|
||||||
state = state.copyWith(cancelToken: CancelToken());
|
|
||||||
_backupService.backupAsset(currentAssets, state.cancelToken,
|
|
||||||
_onAssetUploaded, _onUploadProgress);
|
|
||||||
} else {
|
|
||||||
PhotoManager.openSetting();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void cancelBackup() {
|
|
||||||
state.cancelToken.cancel('Cancel Backup');
|
|
||||||
state = state.copyWith(
|
|
||||||
backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
|
||||||
state = state.copyWith(
|
|
||||||
backingUpAssetCount: state.backingUpAssetCount - 1,
|
|
||||||
assetOnDatabase: state.assetOnDatabase + 1);
|
|
||||||
|
|
||||||
if (state.backingUpAssetCount == 0) {
|
|
||||||
state = state.copyWith(
|
|
||||||
backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateServerInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onUploadProgress(int sent, int total) {
|
|
||||||
state = state.copyWith(
|
|
||||||
progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateServerInfo() async {
|
|
||||||
var serverInfo = await _serverInfoService.getServerInfo();
|
|
||||||
|
|
||||||
// Update server info
|
|
||||||
state = state.copyWith(
|
|
||||||
serverInfo: ServerInfo(
|
|
||||||
diskSize: serverInfo.diskSize,
|
|
||||||
diskUse: serverInfo.diskUse,
|
|
||||||
diskAvailable: serverInfo.diskAvailable,
|
|
||||||
diskSizeRaw: serverInfo.diskSizeRaw,
|
|
||||||
diskUseRaw: serverInfo.diskUseRaw,
|
|
||||||
diskAvailableRaw: serverInfo.diskAvailableRaw,
|
|
||||||
diskUsagePercentage: serverInfo.diskUsagePercentage,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void resumeBackup() {
|
|
||||||
var authState = ref?.read(authenticationProvider);
|
|
||||||
|
|
||||||
// Check if user is login
|
|
||||||
var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
|
|
||||||
|
|
||||||
// User has been logged out return
|
|
||||||
if (authState != null) {
|
|
||||||
if (accessKey == null || !authState.isAuthenticated) {
|
|
||||||
debugPrint("[resumeBackup] not authenticated - abort");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this device is enable backup by the user
|
|
||||||
if ((authState.deviceInfo.deviceId == authState.deviceId) &&
|
|
||||||
authState.deviceInfo.isAutoBackup) {
|
|
||||||
// check if backup is alreayd in process - then return
|
|
||||||
if (state.backupProgress == BackUpProgressEnum.inProgress) {
|
|
||||||
debugPrint("[resumeBackup] Backup is already in progress - abort");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run backup
|
|
||||||
debugPrint("[resumeBackup] Start back up");
|
|
||||||
startBackupProcess();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final backupProvider =
|
|
||||||
StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
|
||||||
return BackupNotifier(ref: ref);
|
|
||||||
});
|
|
||||||
@@ -2,8 +2,15 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/upload_profile_image_repsonse.model.dart';
|
||||||
import 'package:immich_mobile/shared/models/user_info.model.dart';
|
import 'package:immich_mobile/shared/models/user_info.model.dart';
|
||||||
import 'package:immich_mobile/shared/services/network.service.dart';
|
import 'package:immich_mobile/shared/services/network.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/dio_http_interceptor.dart';
|
||||||
|
import 'package:immich_mobile/utils/files_helper.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
|
||||||
class UserService {
|
class UserService {
|
||||||
final NetworkService _networkService = NetworkService();
|
final NetworkService _networkService = NetworkService();
|
||||||
@@ -21,4 +28,39 @@ class UserService {
|
|||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<UploadProfileImageResponse?> uploadProfileImage(XFile image) async {
|
||||||
|
var dio = Dio();
|
||||||
|
dio.interceptors.add(AuthenticatedRequestInterceptor());
|
||||||
|
String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
|
||||||
|
var mimeType = FileHelper.getMimeType(image.path);
|
||||||
|
|
||||||
|
final imageData = MultipartFile.fromBytes(
|
||||||
|
await image.readAsBytes(),
|
||||||
|
filename: image.name,
|
||||||
|
contentType: MediaType(
|
||||||
|
mimeType["type"],
|
||||||
|
mimeType["subType"],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final formData = FormData.fromMap({'file': imageData});
|
||||||
|
|
||||||
|
try {
|
||||||
|
Response res = await dio.post(
|
||||||
|
'$savedEndpoint/user/profile-image',
|
||||||
|
data: formData,
|
||||||
|
);
|
||||||
|
|
||||||
|
var payload = UploadProfileImageResponse.fromJson(res.toString());
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
} on DioError catch (e) {
|
||||||
|
debugPrint("Error uploading file: ${e.response}");
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error uploading file: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,28 +7,28 @@ packages:
|
|||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "34.0.0"
|
version: "38.0.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.4.1"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.11"
|
version: "3.3.0"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: args
|
name: args
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.1"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -42,14 +42,14 @@ packages:
|
|||||||
name: auto_route
|
name: auto_route
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.2"
|
version: "4.0.1"
|
||||||
auto_route_generator:
|
auto_route_generator:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: auto_route_generator
|
name: auto_route_generator
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "4.0.0"
|
||||||
badges:
|
badges:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -70,7 +70,7 @@ packages:
|
|||||||
name: build
|
name: build
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.3.0"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -84,21 +84,21 @@ packages:
|
|||||||
name: build_daemon
|
name: build_daemon
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.1.0"
|
||||||
build_resolvers:
|
build_resolvers:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_resolvers
|
name: build_resolvers
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.0.8"
|
||||||
build_runner:
|
build_runner:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.7"
|
version: "2.1.10"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -119,14 +119,14 @@ packages:
|
|||||||
name: built_value
|
name: built_value
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.4"
|
version: "8.3.0"
|
||||||
cached_network_image:
|
cached_network_image:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: cached_network_image
|
name: cached_network_image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.2.1"
|
||||||
cached_network_image_platform_interface:
|
cached_network_image_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -168,14 +168,7 @@ packages:
|
|||||||
name: chewie
|
name: chewie
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "1.3.2"
|
||||||
cli_util:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: cli_util
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.5"
|
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -196,7 +189,7 @@ packages:
|
|||||||
name: collection
|
name: collection
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0"
|
version: "1.16.0"
|
||||||
convert:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -204,13 +197,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.3+1"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2"
|
||||||
csslib:
|
csslib:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -231,14 +231,21 @@ packages:
|
|||||||
name: dart_style
|
name: dart_style
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.3"
|
||||||
dio:
|
dio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: dio
|
name: dio
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.4"
|
version: "4.0.6"
|
||||||
|
equatable:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.3"
|
||||||
exif:
|
exif:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -252,14 +259,14 @@ packages:
|
|||||||
name: fake_async
|
name: fake_async
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: ffi
|
name: ffi
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.2.1"
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -273,7 +280,7 @@ packages:
|
|||||||
name: fixnum
|
name: fixnum
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -285,7 +292,7 @@ packages:
|
|||||||
name: flutter_blurhash
|
name: flutter_blurhash
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "0.6.8"
|
||||||
flutter_cache_manager:
|
flutter_cache_manager:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -299,7 +306,7 @@ packages:
|
|||||||
name: flutter_hooks
|
name: flutter_hooks
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.18.2"
|
version: "0.18.4"
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -321,13 +328,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.14.0"
|
version: "0.14.0"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.6"
|
||||||
flutter_riverpod:
|
flutter_riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_riverpod
|
name: flutter_riverpod
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0-dev.0"
|
version: "2.0.0-dev.7"
|
||||||
flutter_spinkit:
|
flutter_spinkit:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -365,7 +379,7 @@ packages:
|
|||||||
name: fluttertoast
|
name: fluttertoast
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.0.8"
|
version: "8.0.9"
|
||||||
frontend_server_client:
|
frontend_server_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -393,7 +407,7 @@ packages:
|
|||||||
name: hive
|
name: hive
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.5"
|
version: "2.2.1"
|
||||||
hive_flutter:
|
hive_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -414,7 +428,7 @@ packages:
|
|||||||
name: hooks_riverpod
|
name: hooks_riverpod
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0-dev.0"
|
version: "2.0.0-dev.7"
|
||||||
html:
|
html:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -435,7 +449,7 @@ packages:
|
|||||||
name: http_multi_server
|
name: http_multi_server
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.2.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -449,7 +463,42 @@ packages:
|
|||||||
name: image
|
name: image
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.3"
|
||||||
|
image_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: image_picker
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.5+3"
|
||||||
|
image_picker_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_android
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.4+13"
|
||||||
|
image_picker_for_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_for_web
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
image_picker_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_ios
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.8.5+5"
|
||||||
|
image_picker_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image_picker_platform_interface
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.0"
|
||||||
intl:
|
intl:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -470,14 +519,14 @@ packages:
|
|||||||
name: js
|
name: js
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.3"
|
version: "0.6.4"
|
||||||
json_annotation:
|
json_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: json_annotation
|
name: json_annotation
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.4.0"
|
version: "4.5.0"
|
||||||
latlong2:
|
latlong2:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -519,7 +568,7 @@ packages:
|
|||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.3"
|
version: "0.1.4"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -540,7 +589,7 @@ packages:
|
|||||||
name: mime
|
name: mime
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.2"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -568,14 +617,14 @@ packages:
|
|||||||
name: package_info_plus
|
name: package_info_plus
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.2"
|
||||||
package_info_plus_linux:
|
package_info_plus_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: package_info_plus_linux
|
name: package_info_plus_linux
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.0.5"
|
||||||
package_info_plus_macos:
|
package_info_plus_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -596,70 +645,70 @@ packages:
|
|||||||
name: package_info_plus_web
|
name: package_info_plus_web
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.5"
|
||||||
package_info_plus_windows:
|
package_info_plus_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: package_info_plus_windows
|
name: package_info_plus_windows
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.5"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.0"
|
version: "1.8.1"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
version: "2.0.10"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.11"
|
version: "2.0.14"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_ios
|
name: path_provider_ios
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.7"
|
version: "2.0.9"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_linux
|
name: path_provider_linux
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.1.6"
|
||||||
path_provider_macos:
|
path_provider_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_macos
|
name: path_provider_macos
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.5"
|
version: "2.0.6"
|
||||||
path_provider_platform_interface:
|
path_provider_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_platform_interface
|
name: path_provider_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.0.4"
|
||||||
path_provider_windows:
|
path_provider_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_windows
|
name: path_provider_windows
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.5"
|
version: "2.0.6"
|
||||||
pedantic:
|
pedantic:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -673,21 +722,21 @@ packages:
|
|||||||
name: percent_indicator
|
name: percent_indicator
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.0"
|
version: "4.2.2"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: petitparser
|
name: petitparser
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.4.0"
|
version: "5.0.0"
|
||||||
photo_manager:
|
photo_manager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: photo_manager
|
name: photo_manager
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.1.0+2"
|
||||||
photo_view:
|
photo_view:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -743,14 +792,14 @@ packages:
|
|||||||
name: provider
|
name: provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "6.0.2"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: pub_semver
|
name: pub_semver
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.1"
|
||||||
pubspec_parse:
|
pubspec_parse:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -764,14 +813,14 @@ packages:
|
|||||||
name: quiver
|
name: quiver
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1+1"
|
version: "3.1.0"
|
||||||
riverpod:
|
riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: riverpod
|
name: riverpod
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0-dev.0"
|
version: "2.0.0-dev.7"
|
||||||
rxdart:
|
rxdart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -785,7 +834,7 @@ packages:
|
|||||||
name: shelf
|
name: shelf
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.3.0"
|
||||||
shelf_web_socket:
|
shelf_web_socket:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -804,7 +853,7 @@ packages:
|
|||||||
name: sliver_tools
|
name: sliver_tools
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.5"
|
version: "0.2.6"
|
||||||
socket_io_client:
|
socket_io_client:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -825,21 +874,21 @@ packages:
|
|||||||
name: source_gen
|
name: source_gen
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.2"
|
||||||
source_helper:
|
source_helper:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_helper
|
name: source_helper
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.2"
|
||||||
source_span:
|
source_span:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_span
|
name: source_span
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.1"
|
version: "1.8.2"
|
||||||
sprintf:
|
sprintf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -853,14 +902,14 @@ packages:
|
|||||||
name: sqflite
|
name: sqflite
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2+1"
|
||||||
sqflite_common:
|
sqflite_common:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_common
|
name: sqflite_common
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.1+1"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -902,7 +951,7 @@ packages:
|
|||||||
name: synchronized
|
name: synchronized
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.0+2"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -916,7 +965,7 @@ packages:
|
|||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.8"
|
version: "0.4.9"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -944,7 +993,7 @@ packages:
|
|||||||
name: typed_data
|
name: typed_data
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.1"
|
||||||
unicode:
|
unicode:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -972,49 +1021,56 @@ packages:
|
|||||||
name: uuid
|
name: uuid
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.5"
|
version: "3.0.6"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.2"
|
||||||
|
very_good_analysis:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: very_good_analysis
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.0"
|
||||||
video_player:
|
video_player:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: video_player
|
name: video_player
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.18"
|
version: "2.4.2"
|
||||||
video_player_android:
|
video_player_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_android
|
name: video_player_android
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.17"
|
version: "2.3.3"
|
||||||
video_player_avfoundation:
|
video_player_avfoundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_avfoundation
|
name: video_player_avfoundation
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.18"
|
version: "2.3.4"
|
||||||
video_player_platform_interface:
|
video_player_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_platform_interface
|
name: video_player_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.1"
|
version: "5.1.2"
|
||||||
video_player_web:
|
video_player_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_web
|
name: video_player_web
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.6"
|
version: "2.0.10"
|
||||||
visibility_detector:
|
visibility_detector:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1028,7 +1084,7 @@ packages:
|
|||||||
name: wakelock
|
name: wakelock
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.6"
|
version: "0.6.1+2"
|
||||||
wakelock_macos:
|
wakelock_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1070,14 +1126,14 @@ packages:
|
|||||||
name: web_socket_channel
|
name: web_socket_channel
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.2.0"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.8"
|
version: "2.5.2"
|
||||||
wkt_parser:
|
wkt_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1091,21 +1147,21 @@ packages:
|
|||||||
name: xdg_directories
|
name: xdg_directories
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.2.0+1"
|
||||||
xml:
|
xml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xml
|
name: xml
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.3.1"
|
version: "5.4.1"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: yaml
|
name: yaml
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0"
|
version: "3.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.15.1 <3.0.0"
|
dart: ">=2.17.0 <3.0.0"
|
||||||
flutter: ">=2.8.0"
|
flutter: ">=3.0.0"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.7.0+11
|
version: 1.10.0+15
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.15.1 <3.0.0"
|
sdk: ">=2.15.1 <3.0.0"
|
||||||
@@ -14,13 +14,13 @@ dependencies:
|
|||||||
photo_manager: ^2.0.6
|
photo_manager: ^2.0.6
|
||||||
flutter_hooks: ^0.18.0
|
flutter_hooks: ^0.18.0
|
||||||
hooks_riverpod: ^2.0.0-dev.0
|
hooks_riverpod: ^2.0.0-dev.0
|
||||||
hive:
|
hive: ^2.2.1
|
||||||
hive_flutter:
|
hive_flutter: ^1.1.0
|
||||||
dio: ^4.0.4
|
dio: ^4.0.4
|
||||||
cached_network_image: ^3.2.0
|
cached_network_image: ^3.2.1
|
||||||
percent_indicator: ^3.4.0
|
percent_indicator: ^4.2.2
|
||||||
intl: ^0.17.0
|
intl: ^0.17.0
|
||||||
auto_route: ^3.2.2
|
auto_route: ^4.0.1
|
||||||
exif: ^3.1.1
|
exif: ^3.1.1
|
||||||
transparent_image: ^2.0.0
|
transparent_image: ^2.0.0
|
||||||
visibility_detector: ^0.2.2
|
visibility_detector: ^0.2.2
|
||||||
@@ -37,6 +37,8 @@ dependencies:
|
|||||||
package_info_plus: ^1.4.0
|
package_info_plus: ^1.4.0
|
||||||
flutter_spinkit: ^5.1.0
|
flutter_spinkit: ^5.1.0
|
||||||
flutter_swipe_detector: ^2.0.0
|
flutter_swipe_detector: ^2.0.0
|
||||||
|
equatable: ^2.0.3
|
||||||
|
image_picker: ^0.8.5+3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -44,7 +46,7 @@ dev_dependencies:
|
|||||||
flutter_lints: ^1.0.0
|
flutter_lints: ^1.0.0
|
||||||
hive_generator: ^1.1.2
|
hive_generator: ^1.1.2
|
||||||
build_runner: ^2.1.7
|
build_runner: ^2.1.7
|
||||||
auto_route_generator: ^3.2.1
|
auto_route_generator: ^4.0.0
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"printWidth": 120
|
"printWidth": 120,
|
||||||
|
"semi": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ WORKDIR /usr/src/app
|
|||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
RUN apk add --update-cache build-base python3
|
RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
|
||||||
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
|
|||||||
637
server/package-lock.json
generated
@@ -33,6 +33,7 @@
|
|||||||
"@nestjs/platform-express": "^8.0.0",
|
"@nestjs/platform-express": "^8.0.0",
|
||||||
"@nestjs/platform-fastify": "^8.2.6",
|
"@nestjs/platform-fastify": "^8.2.6",
|
||||||
"@nestjs/platform-socket.io": "^8.2.6",
|
"@nestjs/platform-socket.io": "^8.2.6",
|
||||||
|
"@nestjs/schedule": "^2.0.1",
|
||||||
"@nestjs/typeorm": "^8.0.3",
|
"@nestjs/typeorm": "^8.0.3",
|
||||||
"@nestjs/websockets": "^8.2.6",
|
"@nestjs/websockets": "^8.2.6",
|
||||||
"@socket.io/redis-adapter": "^7.1.0",
|
"@socket.io/redis-adapter": "^7.1.0",
|
||||||
@@ -44,6 +45,7 @@
|
|||||||
"diskusage": "^1.1.3",
|
"diskusage": "^1.1.3",
|
||||||
"dotenv": "^14.2.0",
|
"dotenv": "^14.2.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
"joi": "^17.5.0",
|
"joi": "^17.5.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"passport": "^0.5.2",
|
"passport": "^0.5.2",
|
||||||
@@ -53,6 +55,7 @@
|
|||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.2.0",
|
||||||
|
"sharp": "^0.28.0",
|
||||||
"socket.io-redis": "^6.1.1",
|
"socket.io-redis": "^6.1.1",
|
||||||
"systeminformation": "^5.11.0",
|
"systeminformation": "^5.11.0",
|
||||||
"typeorm": "^0.2.41"
|
"typeorm": "^0.2.41"
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
"@nestjs/testing": "^8.0.0",
|
"@nestjs/testing": "^8.0.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/bull": "^3.15.7",
|
"@types/bull": "^3.15.7",
|
||||||
|
"@types/cron": "^2.0.0",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/imagemin": "^8.0.0",
|
"@types/imagemin": "^8.0.0",
|
||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.0.2",
|
||||||
@@ -70,6 +74,7 @@
|
|||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^16.0.0",
|
"@types/node": "^16.0.0",
|
||||||
"@types/passport-jwt": "^3.0.6",
|
"@types/passport-jwt": "^3.0.6",
|
||||||
|
"@types/sharp": "^0.30.2",
|
||||||
"@types/supertest": "^2.0.11",
|
"@types/supertest": "^2.0.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ import {
|
|||||||
Headers,
|
Headers,
|
||||||
Delete,
|
Delete,
|
||||||
Logger,
|
Logger,
|
||||||
|
Patch,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
import { FileFieldsInterceptor } from '@nestjs/platform-express';
|
import { FileFieldsInterceptor } from '@nestjs/platform-express';
|
||||||
import { multerOption } from '../../config/multer-option.config';
|
import { assetUploadOption } from '../../config/asset-upload.config';
|
||||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
import { ServeFileDto } from './dto/serve-file.dto';
|
import { ServeFileDto } from './dto/serve-file.dto';
|
||||||
@@ -38,7 +39,7 @@ export class AssetController {
|
|||||||
private wsCommunicateionGateway: CommunicationGateway,
|
private wsCommunicateionGateway: CommunicationGateway,
|
||||||
private assetService: AssetService,
|
private assetService: AssetService,
|
||||||
private backgroundTaskService: BackgroundTaskService,
|
private backgroundTaskService: BackgroundTaskService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
@Post('upload')
|
@Post('upload')
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
@@ -47,7 +48,7 @@ export class AssetController {
|
|||||||
{ name: 'assetData', maxCount: 1 },
|
{ name: 'assetData', maxCount: 1 },
|
||||||
{ name: 'thumbnailData', maxCount: 1 },
|
{ name: 'thumbnailData', maxCount: 1 },
|
||||||
],
|
],
|
||||||
multerOption,
|
assetUploadOption,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async uploadFile(
|
async uploadFile(
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import { BadRequestException, Injectable, Logger, StreamableFile } from '@nestjs/common';
|
import { BadRequestException, Injectable, Logger, StreamableFile } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { MoreThan, Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
import { AssetEntity, AssetType } from './entities/asset.entity';
|
import { AssetEntity, AssetType } from './entities/asset.entity';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
|
|
||||||
import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto';
|
|
||||||
import { createReadStream, stat } from 'fs';
|
import { createReadStream, stat } from 'fs';
|
||||||
import { ServeFileDto } from './dto/serve-file.dto';
|
import { ServeFileDto } from './dto/serve-file.dto';
|
||||||
import { Response as Res } from 'express';
|
import { Response as Res } from 'express';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
||||||
import { SearchAssetDto } from './dto/search-asset.dto';
|
import { SearchAssetDto } from './dto/search-asset.dto';
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
const fileInfo = promisify(stat);
|
const fileInfo = promisify(stat);
|
||||||
|
|
||||||
@@ -22,7 +19,7 @@ export class AssetService {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(AssetEntity)
|
@InjectRepository(AssetEntity)
|
||||||
private assetRepository: Repository<AssetEntity>,
|
private assetRepository: Repository<AssetEntity>,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
public async updateThumbnailInfo(assetId: string, path: string) {
|
public async updateThumbnailInfo(assetId: string, path: string) {
|
||||||
return await this.assetRepository.update(assetId, {
|
return await this.assetRepository.update(assetId, {
|
||||||
@@ -114,34 +111,66 @@ export class AssetService {
|
|||||||
public async getAssetThumbnail(assetId: string) {
|
public async getAssetThumbnail(assetId: string) {
|
||||||
const asset = await this.assetRepository.findOne({ id: assetId });
|
const asset = await this.assetRepository.findOne({ id: assetId });
|
||||||
|
|
||||||
return new StreamableFile(createReadStream(asset.resizePath));
|
if (asset.webpPath != '') {
|
||||||
|
return new StreamableFile(createReadStream(asset.webpPath));
|
||||||
|
} else {
|
||||||
|
return new StreamableFile(createReadStream(asset.resizePath));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) {
|
public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) {
|
||||||
let file = null;
|
let file = null;
|
||||||
const asset = await this.findOne(query.did, query.aid);
|
const asset = await this.findOne(query.did, query.aid);
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
throw new BadRequestException('Asset does not exist');
|
throw new BadRequestException('Asset does not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Handle Sending Images
|
// Handle Sending Images
|
||||||
if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
|
if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
|
||||||
res.set({
|
/**
|
||||||
'Content-Type': asset.mimeType,
|
* Serve file viewer on the web
|
||||||
});
|
*/
|
||||||
|
if (query.isWeb) {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'image/jpeg',
|
||||||
|
});
|
||||||
|
return new StreamableFile(createReadStream(asset.resizePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serve thumbnail image for both web and mobile app
|
||||||
|
*/
|
||||||
if (query.isThumb === 'false' || !query.isThumb) {
|
if (query.isThumb === 'false' || !query.isThumb) {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': asset.mimeType,
|
||||||
|
});
|
||||||
file = createReadStream(asset.originalPath);
|
file = createReadStream(asset.originalPath);
|
||||||
} else {
|
} else {
|
||||||
file = createReadStream(asset.resizePath);
|
if (asset.webpPath != '') {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'image/webp',
|
||||||
|
});
|
||||||
|
file = createReadStream(asset.webpPath);
|
||||||
|
} else {
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'image/jpeg',
|
||||||
|
});
|
||||||
|
file = createReadStream(asset.resizePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
file.on('error', (error) => {
|
file.on('error', (error) => {
|
||||||
Logger.log(`Cannot create read stream ${error}`);
|
Logger.log(`Cannot create read stream ${error}`);
|
||||||
return new BadRequestException('Cannot Create Read Stream');
|
return new BadRequestException('Cannot Create Read Stream');
|
||||||
});
|
});
|
||||||
|
|
||||||
return new StreamableFile(file);
|
return new StreamableFile(file);
|
||||||
|
|
||||||
} else if (asset.type == AssetType.VIDEO) {
|
} else if (asset.type == AssetType.VIDEO) {
|
||||||
// Handle Handling Video
|
// Handle Video
|
||||||
const { size } = await fileInfo(asset.originalPath);
|
const { size } = await fileInfo(asset.originalPath);
|
||||||
const range = headers.range;
|
const range = headers.range;
|
||||||
|
|
||||||
@@ -183,6 +212,8 @@ export class AssetService {
|
|||||||
const videoStream = createReadStream(asset.originalPath, { start: start, end: end });
|
const videoStream = createReadStream(asset.originalPath, { start: start, end: end });
|
||||||
|
|
||||||
return new StreamableFile(videoStream);
|
return new StreamableFile(videoStream);
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': asset.mimeType,
|
'Content-Type': asset.mimeType,
|
||||||
|
|||||||
@@ -13,4 +13,8 @@ export class ServeFileDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBooleanString()
|
@IsBooleanString()
|
||||||
isThumb: string;
|
isThumb: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBooleanString()
|
||||||
|
isWeb: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ export class AssetEntity {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
resizePath: string;
|
resizePath: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
webpPath: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ import { SignUpDto } from './dto/sign-up.dto';
|
|||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private readonly authService: AuthService) {}
|
constructor(private readonly authService: AuthService) { }
|
||||||
|
|
||||||
@Post('/login')
|
@Post('/login')
|
||||||
async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto) {
|
async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto) {
|
||||||
return await this.authService.login(loginCredential);
|
return await this.authService.login(loginCredential);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/signUp')
|
@Post('/admin-sign-up')
|
||||||
async signUp(@Body(ValidationPipe) signUpCrendential: SignUpDto) {
|
async adminSignUp(@Body(ValidationPipe) signUpCrendential: SignUpDto) {
|
||||||
return await this.authService.signUp(signUpCrendential);
|
return await this.authService.adminSignUp(signUpCrendential);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ export class AuthService {
|
|||||||
@InjectRepository(UserEntity)
|
@InjectRepository(UserEntity)
|
||||||
private userRepository: Repository<UserEntity>,
|
private userRepository: Repository<UserEntity>,
|
||||||
private immichJwtService: ImmichJwtService,
|
private immichJwtService: ImmichJwtService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
private async validateUser(loginCredential: LoginCredentialDto): Promise<UserEntity> {
|
private async validateUser(loginCredential: LoginCredentialDto): Promise<UserEntity> {
|
||||||
const user = await this.userRepository.findOne(
|
const user = await this.userRepository.findOne(
|
||||||
{ email: loginCredential.email },
|
{ email: loginCredential.email },
|
||||||
{ select: ['id', 'email', 'password', 'salt'] },
|
{ select: ['id', 'email', 'password', 'salt', 'firstName', 'lastName', 'isAdmin', 'profileImagePath', 'isFirstLoggedIn'] },
|
||||||
);
|
);
|
||||||
|
|
||||||
const isAuthenticated = await this.validatePassword(user.password, loginCredential.password, user.salt);
|
const isAuthenticated = await this.validatePassword(user.password, loginCredential.password, user.salt);
|
||||||
@@ -44,32 +44,45 @@ export class AuthService {
|
|||||||
accessToken: await this.immichJwtService.generateToken(payload),
|
accessToken: await this.immichJwtService.generateToken(payload),
|
||||||
userId: validatedUser.id,
|
userId: validatedUser.id,
|
||||||
userEmail: validatedUser.email,
|
userEmail: validatedUser.email,
|
||||||
|
firstName: validatedUser.firstName,
|
||||||
|
lastName: validatedUser.lastName,
|
||||||
|
isAdmin: validatedUser.isAdmin,
|
||||||
|
profileImagePath: validatedUser.profileImagePath,
|
||||||
|
isFirstLogin: validatedUser.isFirstLoggedIn
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async signUp(signUpCrendential: SignUpDto) {
|
|
||||||
const registerUser = await this.userRepository.findOne({ email: signUpCrendential.email });
|
|
||||||
|
|
||||||
if (registerUser) {
|
public async adminSignUp(signUpCrendential: SignUpDto) {
|
||||||
throw new BadRequestException('User exist');
|
const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } });
|
||||||
|
|
||||||
|
if (adminUser) {
|
||||||
|
throw new BadRequestException('The server already has an admin')
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUser = new UserEntity();
|
|
||||||
newUser.email = signUpCrendential.email;
|
const newAdminUser = new UserEntity();
|
||||||
newUser.salt = await bcrypt.genSalt();
|
newAdminUser.email = signUpCrendential.email;
|
||||||
newUser.password = await this.hashPassword(signUpCrendential.password, newUser.salt);
|
newAdminUser.salt = await bcrypt.genSalt();
|
||||||
|
newAdminUser.password = await this.hashPassword(signUpCrendential.password, newAdminUser.salt);
|
||||||
|
newAdminUser.firstName = signUpCrendential.firstName;
|
||||||
|
newAdminUser.lastName = signUpCrendential.lastName;
|
||||||
|
newAdminUser.isAdmin = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const savedUser = await this.userRepository.save(newUser);
|
const savedNewAdminUserUser = await this.userRepository.save(newAdminUser);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: savedUser.id,
|
id: savedNewAdminUserUser.id,
|
||||||
email: savedUser.email,
|
email: savedNewAdminUserUser.email,
|
||||||
createdAt: savedUser.createdAt,
|
firstName: savedNewAdminUserUser.firstName,
|
||||||
|
lastName: savedNewAdminUserUser.lastName,
|
||||||
|
createdAt: savedNewAdminUserUser.createdAt,
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger.error('e', 'signUp');
|
Logger.error('e', 'signUp');
|
||||||
throw new InternalServerErrorException('Failed to register new user');
|
throw new InternalServerErrorException('Failed to register new admin user');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,4 +6,10 @@ export class SignUpDto {
|
|||||||
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
lastName: string;
|
||||||
}
|
}
|
||||||
|
|||||||
12
server/src/api-v1/sharing/dto/update-shared-album.dto.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateShareAlbumDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
albumId: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
albumName: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
ownerId: string;
|
||||||
|
}
|
||||||
@@ -2,10 +2,11 @@ import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Validatio
|
|||||||
import { SharingService } from './sharing.service';
|
import { SharingService } from './sharing.service';
|
||||||
import { CreateSharedAlbumDto } from './dto/create-shared-album.dto';
|
import { CreateSharedAlbumDto } from './dto/create-shared-album.dto';
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { GetAuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
import { AddAssetsDto } from './dto/add-assets.dto';
|
||||||
import { AddUsersDto } from './dto/add-users.dto';
|
import { AddUsersDto } from './dto/add-users.dto';
|
||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||||
|
import { UpdateShareAlbumDto } from './dto/update-shared-album.dto';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('shared')
|
@Controller('shared')
|
||||||
@@ -52,4 +53,9 @@ export class SharingController {
|
|||||||
async leaveAlbum(@GetAuthUser() authUser, @Param('albumId') albumId: string) {
|
async leaveAlbum(@GetAuthUser() authUser, @Param('albumId') albumId: string) {
|
||||||
return await this.sharingService.leaveAlbum(authUser, albumId);
|
return await this.sharingService.leaveAlbum(authUser, albumId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch('/updateInfo')
|
||||||
|
async updateAlbumInfo(@GetAuthUser() authUser, @Body(ValidationPipe) updateAlbumInfoDto: UpdateShareAlbumDto) {
|
||||||
|
return await this.sharingService.updateAlbumTitle(authUser, updateAlbumInfoDto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { UserSharedAlbumEntity } from './entities/user-shared-album.entity';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { AddUsersDto } from './dto/add-users.dto';
|
import { AddUsersDto } from './dto/add-users.dto';
|
||||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||||
|
import { UpdateShareAlbumDto } from './dto/update-shared-album.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SharingService {
|
export class SharingService {
|
||||||
@@ -184,4 +185,15 @@ export class SharingService {
|
|||||||
|
|
||||||
return await this.assetSharedAlbumRepository.save([...newRecords]);
|
return await this.assetSharedAlbumRepository.save([...newRecords]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateAlbumTitle(authUser: AuthUserDto, updateShareAlbumDto: UpdateShareAlbumDto) {
|
||||||
|
if (authUser.id != updateShareAlbumDto.ownerId) {
|
||||||
|
throw new BadRequestException('Unauthorized to change album info');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharedAlbum = await this.sharedAlbumRepository.findOne({ where: { id: updateShareAlbumDto.albumId } });
|
||||||
|
sharedAlbum.albumName = updateShareAlbumDto.albumName;
|
||||||
|
|
||||||
|
return await this.sharedAlbumRepository.save(sharedAlbum);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,27 @@
|
|||||||
export class CreateUserDto {}
|
import { IsNotEmpty, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateUserDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
profileImagePath: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
isAdmin: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
isFirstLoggedIn: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ export class UserEntity {
|
|||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
isAdmin: boolean;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@@ -14,6 +23,12 @@ export class UserEntity {
|
|||||||
@Column({ select: false })
|
@Column({ select: false })
|
||||||
salt: string;
|
salt: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
profileImagePath: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
isFirstLoggedIn: boolean;
|
||||||
|
|
||||||
@CreateDateColumn()
|
@CreateDateColumn()
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,54 @@
|
|||||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common';
|
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, ValidationPipe, Put, Query, UseInterceptors, UploadedFile, Response } from '@nestjs/common';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
|
||||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||||
|
import { CreateUserDto } from './dto/create-user.dto';
|
||||||
|
import { AdminRolesGuard } from '../../middlewares/admin-role-guard.middleware';
|
||||||
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
|
import { profileImageUploadOption } from '../../config/profile-image-upload.config';
|
||||||
|
import { Response as Res } from 'express';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Controller('user')
|
@Controller('user')
|
||||||
export class UserController {
|
export class UserController {
|
||||||
constructor(private readonly userService: UserService) {}
|
constructor(private readonly userService: UserService) { }
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get()
|
@Get()
|
||||||
async getAllUsers(@GetAuthUser() authUser: AuthUserDto) {
|
async getAllUsers(@GetAuthUser() authUser: AuthUserDto, @Query('isAll') isAll: boolean) {
|
||||||
return await this.userService.getAllUsers(authUser);
|
return await this.userService.getAllUsers(authUser, isAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@UseGuards(AdminRolesGuard)
|
||||||
|
@Post()
|
||||||
|
async createNewUser(@Body(ValidationPipe) createUserDto: CreateUserDto) {
|
||||||
|
return await this.userService.createUser(createUserDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/count')
|
||||||
|
async getUserCount(@Query('isAdmin') isAdmin: boolean) {
|
||||||
|
|
||||||
|
return await this.userService.getUserCount(isAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Put()
|
||||||
|
async updateUser(@Body(ValidationPipe) updateUserDto: UpdateUserDto) {
|
||||||
|
return await this.userService.updateUser(updateUserDto)
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@UseInterceptors(FileInterceptor('file', profileImageUploadOption))
|
||||||
|
@Post('/profile-image')
|
||||||
|
async createProfileImage(@GetAuthUser() authUser: AuthUserDto, @UploadedFile() fileInfo: Express.Multer.File) {
|
||||||
|
return await this.userService.createProfileImage(authUser, fileInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/profile-image/:userId')
|
||||||
|
async getProfileImage(@Param('userId') userId: string,
|
||||||
|
@Response({ passthrough: true }) res: Res,
|
||||||
|
) {
|
||||||
|
return await this.userService.getUserProfileImage(userId, res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||